--- name: 高级尺寸变换 (Advanced Size Transform) description: 提供美观的悬浮面板,用于一键统一元素宽度、等比例缩放、对齐盒模型尺寸及修改选中文字大小。 author: ymjr version: 1.0.0 license: MIT usage: 选中元素后在悬浮面板中点击对应的操作按钮。面板支持拖拽移动,再次运行此脚本可关闭面板。 features: - 美观的 HTML 悬浮面板(不阻挡原生右键菜单) - 完美解决多屏/多视图焦点切换导致 ea 作用域丢失的问题 - 支持快捷缩放(±10%)、按最大/小宽度统一、矩形/菱形/椭圆完全统一、字号快捷设定 dependencies: [] autorun: false --- /* ```javascript */ const SCRIPT_ID = "ymjr.feature.size.transform"; const PANEL_ID = "ymjr-advanced-size-panel"; // 国际化 const locales = { zh: { title: "📐 尺寸与排版", scale: "快捷缩放", scale_up: "+10% 放大", scale_down: "-10% 缩小", width: "等宽对齐", width_max: "对齐最大宽", width_min: "对齐最小宽", uniform: "盒模型统一", uniform_btn: "统一选中图形 (取最大值)", font: "字号设置", font_btn: "应用", notice_no_sel: "请先选中需要变换的元素!", notice_no_box: "未选中矩形、菱形或椭圆!", notice_success: "✅ 变换成功", notice_closed: "🔌 尺寸变换面板已关闭" }, en: { title: "📐 Transform", scale: "Quick Scale", scale_up: "+10% Size", scale_down: "-10% Size", width: "Match Width", width_max: "Max Width", width_min: "Min Width", uniform: "Uniform Boxes", uniform_btn: "Uniform Shapes (Max)", font: "Font Size", font_btn: "Apply", notice_no_sel: "Please select elements first!", notice_no_box: "No rect, diamond, or ellipse selected!", notice_success: "✅ Success", notice_closed: "🔌 Panel closed" } }; // 1. 清理旧面板 (Toggle逻辑:如果已存在则关闭并退出) if (ExcalidrawAutomate.plugin[SCRIPT_ID]) { ExcalidrawAutomate.plugin[SCRIPT_ID].remove(); delete ExcalidrawAutomate.plugin[SCRIPT_ID]; new Notice(t("notice_closed")); return; } // 2. 获取当前视图容器 (最关键的一步:将 UI 绑定到当前具体视图,防止作用域飘逸) const viewContainer = ea.targetView.containerEl.querySelector(".excalidraw-wrapper") || ea.targetView.containerEl; // 3. 构建 UI DOM const panel = document.createElement("div"); panel.id = PANEL_ID; ExcalidrawAutomate.plugin[SCRIPT_ID] = panel; // 注入 Obsidian 原生 CSS 变量与玻璃拟态效果 panel.innerHTML = `
${t("title")} ✖
${t("scale")}
${t("width")}
${t("uniform")}
${t("font")}
`; viewContainer.appendChild(panel); // 4. 拖拽功能实现 const header = panel.querySelector(".ymjr-header"); let isDragging = false, startX, startY, initialLeft, initialTop; header.addEventListener("mousedown", (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); const containerRect = viewContainer.getBoundingClientRect(); initialLeft = rect.left - containerRect.left; initialTop = rect.top - containerRect.top; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.left = `${initialLeft + dx}px`; panel.style.top = `${initialTop + dy}px`; panel.style.right = "auto"; // 拖拽后解除 right 定位 }); document.addEventListener("mouseup", () => { isDragging = false; }); // 5. 核心逻辑引擎 (由于我们把事件绑定在当前View的DOM中,使用传参或闭包中的 ea 是安全的,但为了最强容错,每次执行动作重新获取 Elements) const performAction = async (actionCallback) => { // 强制使用触发时视图最新的 ea 与 api const currentEa = ea; const api = currentEa.getExcalidrawAPI(); const selectedEls = currentEa.getViewSelectedElements(); if (selectedEls.length === 0) { new Notice(t("notice_no_sel")); return; } // 执行具体回调逻辑,传入必要参数 actionCallback(selectedEls, currentEa); // 统一提交变更 currentEa.copyViewElementsToEAforEditing(selectedEls); await currentEa.addElementsToView(false, false); // 刷新容器大小,避免文字框或图形缩放后边界残影 api.updateContainerSize(selectedEls); new Notice(t("notice_success")); }; // --- 功能绑定 --- // 关闭面板 panel.querySelector(".ymjr-close-btn").onclick = () => { panel.remove(); delete ExcalidrawAutomate.plugin[SCRIPT_ID]; }; // 快捷缩放 (+10% / -10%) const scaleElements = (els, currentEa, factor) => { for (const el of els) { el.width *= factor; el.height *= factor; if (el.type === "text" || el.boundElements?.some(b => b.type === "text")) { el.fontSize *= factor; } } }; panel.querySelector("#ymjr-scale-up").onclick = () => performAction((els, ea) => scaleElements(els, ea, 1.1)); panel.querySelector("#ymjr-scale-down").onclick = () => performAction((els, ea) => scaleElements(els, ea, 0.9)); // 等宽对齐 (最大/最小) const matchWidth = (els, currentEa, mode) => { const widths = els.map(el => el.width); const targetW = mode === 'max' ? Math.max(...widths) : Math.min(...widths); for (let el of els) { const ratio = targetW / el.width; el.width = targetW; el.height *= ratio; // 保持比例缩放高度 } }; panel.querySelector("#ymjr-width-max").onclick = () => performAction((els, ea) => matchWidth(els, ea, 'max')); panel.querySelector("#ymjr-width-min").onclick = () => performAction((els, ea) => matchWidth(els, ea, 'min')); // 盒模型统一 (矩形、菱形、椭圆全等) panel.querySelector("#ymjr-uniform").onclick = async () => { const boxShapes = ["ellipse", "rectangle", "diamond"]; const currentEa = ea; const api = currentEa.getExcalidrawAPI(); const allSelected = currentEa.getViewSelectedElements(); const els = allSelected.filter(el => boxShapes.includes(el.type)); if (els.length === 0) { new Notice(t("notice_no_box")); return; } let maxW = 0, maxH = 0; els.forEach(el => { if (el.width > maxW) maxW = el.width; if (el.height > maxH) maxH = el.height; }); els.forEach(el => { el.width = maxW; el.height = maxH; }); currentEa.copyViewElementsToEAforEditing(els); await currentEa.addElementsToView(false, false); api.updateContainerSize(els); new Notice(t("notice_success")); }; // 设定字号 panel.querySelector("#ymjr-font-btn").onclick = () => performAction((els, currentEa) => { const inputVal = Number(panel.querySelector("#ymjr-font-input").value); if (!inputVal || isNaN(inputVal)) return; els.forEach(el => { // 更新自身文字 if (el.type === "text") el.fontSize = inputVal; // 如果是带文字的容器,也要顺便更新其容器内文字 el.fontSize = inputVal; }); });