--- name: 连线测量与精控面板 description: 悬浮面板:开启/关闭测量标签、设置比例尺单位,以及精确设置连线长度。 author: ymjr version: 1.0.0 license: MIT usage: 用于精确测量和控制画板中的线条长度,并可随时添加或取消数值标注。 features: - 提供带有精美 SVG 图标的可拖拽面板 - 支持自定义比例尺和单位(如 1px = 10mm),并应用测量标注 - 支持输入具体数值,自动按当前角度伸缩选中连线的精确长度 dependencies: - 需要配合 Feature-Measure-Engine 连线实时测量引擎运行 autorun: false --- /* ```javascript */ var locales = { zh: { notice_no_engine: "⚠️ 测量引擎未运行,请先启动『连线实时测量引擎』", panel_title: "测量与长度设置", section_measure: "测量属性", label_ratio: "比例 (1px = ?)", label_unit: "单位", btn_toggle: "应用 / 取消标注", section_exact: "精确长度 (像素)", placeholder_read: "读取选中线段...", btn_apply_length: "设为该长度", notice_no_selection: "⚠️ 请先选中至少一根连线或箭头", notice_removed: "🧹 已取消测量标注", notice_added: "✨ 已添加测量标注", notice_no_line: "⚠️ 请先选中连线", notice_invalid_length: "请输入有效的长度值", notice_updated: "✅ 已更新连线长度" }, en: { notice_no_engine: "⚠️ Measure engine not running, please start it first", panel_title: "Measurement & Length Settings", section_measure: "Measurement Properties", label_ratio: "Ratio (1px = ?)", label_unit: "Unit", btn_toggle: "Apply / Remove Annotation", section_exact: "Exact Length (px)", placeholder_read: "Reading selected line...", btn_apply_length: "Set to this length", notice_no_selection: "⚠️ Please select at least one line or arrow", notice_removed: "🧹 Measurement annotation removed", notice_added: "✨ Measurement annotation added", notice_no_line: "⚠️ Please select a line", notice_invalid_length: "Please enter a valid length value", notice_updated: "✅ Line length updated" } }; const { Notice } = ea.obsidian; if (!ExcalidrawAutomate.plugin.measureEngine) { new Notice(t("notice_no_engine")); return; } // 获取当前选中对象作为初始状态 const getValidSelection = () => ea.getViewSelectedElements().filter(el => ["line", "arrow"].includes(el.type)); function createMeasurePanel() { const PANEL_ID = "ea-measure-adv-panel"; if (document.getElementById(PANEL_ID)) return; // --- 计算居中位置 --- const panelWidth = 280; const panelHeight = 260; // 预估高度 const startLeft = Math.max(10, (window.innerWidth - panelWidth) / 2); const startTop = Math.max(10, (window.innerHeight - panelHeight) / 2); // 统一的输入框与按钮样式 (解决溢出问题的关键:box-sizing: border-box 和 min-width: 0) const inputStyle = `width: 100%; box-sizing: border-box; background: var(--background-modifier-form-field); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; color: var(--text-normal); font-size: 13px; outline: none;`; const btnStyle = `width: 100%; box-sizing: border-box; padding: 6px 12px; font-size: 13px; cursor: pointer; border-radius: 4px; background: var(--interactive-normal); border: 1px solid var(--background-modifier-border); color: var(--text-normal); transition: background 0.2s;`; const ctaBtnStyle = `width: 100%; box-sizing: border-box; padding: 8px 12px; font-size: 13px; cursor: pointer; border-radius: 4px; background: var(--interactive-accent); color: var(--text-on-accent); border: none; transition: opacity 0.2s;`; // 1. 主容器 const panel = document.createElement('div'); panel.id = PANEL_ID; panel.style.cssText = `position: fixed; top: ${startTop}px; left: ${startLeft}px; width: ${panelWidth}px; background: var(--background-primary); border: 1px solid var(--background-modifier-border); box-shadow: 0 10px 30px rgba(0,0,0,0.3); border-radius: 8px; z-index: 9999; display: flex; flex-direction: column; font-family: var(--font-interface); box-sizing: border-box;`; // 2. 头部区域 const header = document.createElement('div'); header.style.cssText = `padding: 12px 16px; background: var(--background-secondary); cursor: move; border-bottom: 1px solid var(--background-modifier-border); display: flex; justify-content: space-between; align-items: center; user-select: none; border-radius: 8px 8px 0 0; box-sizing: border-box;`; // 使用 SVG 图标提升质感 const titleHtml = `
${t("panel_title")}
`; header.innerHTML = `${titleHtml}`; panel.appendChild(header); // 3. 内容容器 const content = document.createElement('div'); content.style.cssText = `padding: 16px; display: flex; flex-direction: column; gap: 20px; box-sizing: border-box; width: 100%;`; // --- 测量标签配置区 --- const measureSection = document.createElement('div'); measureSection.style.cssText = `display: flex; flex-direction: column; gap: 12px; border-bottom: 1px dashed var(--background-modifier-border); padding-bottom: 20px;`; measureSection.innerHTML = `
${t("section_measure")}
`; content.appendChild(measureSection); // --- 精确长度控制区 --- const exactSection = document.createElement('div'); exactSection.style.cssText = `display: flex; flex-direction: column; gap: 12px;`; exactSection.innerHTML = `
${t("section_exact")}
`; content.appendChild(exactSection); panel.appendChild(content); document.body.appendChild(panel); // ================= 绑定事件 ================= // 悬浮样式增强 panel.querySelector('#close-btn').onmouseover = (e) => e.target.style.background = 'var(--background-modifier-hover)'; panel.querySelector('#close-btn').onmouseout = (e) => e.target.style.background = 'none'; panel.querySelector('#btn-apply-length').onmouseover = (e) => e.target.style.background = 'var(--background-modifier-hover)'; panel.querySelector('#btn-apply-length').onmouseout = (e) => e.target.style.background = 'var(--interactive-normal)'; panel.querySelector('#btn-toggle-measure').onmouseover = (e) => e.target.style.opacity = '0.9'; panel.querySelector('#btn-toggle-measure').onmouseout = (e) => e.target.style.opacity = '1'; // 关闭按钮 header.querySelector('#close-btn').onclick = () => panel.remove(); // 绑定【应用/取消标注】 panel.querySelector('#btn-toggle-measure').onclick = () => { const els = getValidSelection(); if (!els.length) return new Notice(t("notice_no_selection")); const ratioInput = parseFloat(panel.querySelector('#msr-ratio').value) || 10; const unitInput = panel.querySelector('#msr-unit').value || "mm"; const isAlreadyMeasured = els[0].customData?.measure; els.forEach(el => { if (isAlreadyMeasured) { delete el.customData.measure; } else { el.customData = { ...el.customData, measure: { ratio: ratioInput, unit: unitInput } }; } }); if (!isAlreadyMeasured) { ExcalidrawAutomate.plugin.measureEngine.updateMeasureText(els); } else { ea.copyViewElementsToEAforEditing(els); ea.addElementsToView(false, false, false); } new Notice(isAlreadyMeasured ? t("notice_removed") : t("notice_added")); }; // 绑定【设置精确长度】 panel.querySelector('#btn-apply-length').onclick = () => { const els = getValidSelection(); if (!els.length) return new Notice(t("notice_no_line")); const targetLength = parseFloat(panel.querySelector('#exact-length').value); if (isNaN(targetLength) || targetLength <= 0) return new Notice(t("notice_invalid_length")); els.forEach(el => { const pts = el.points; if (!pts || pts.length < 2) return; const p1 = pts[0]; const p2 = pts[pts.length - 1]; // 控制终点 const currentAngle = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]); const new_x = p1[0] + targetLength * Math.cos(currentAngle); const new_y = p1[1] + targetLength * Math.sin(currentAngle); pts[pts.length - 1] = [new_x, new_y]; }); // 统一提交更新,并联动刷新文本 ExcalidrawAutomate.plugin.measureEngine.updateMeasureText(els); new Notice(t("notice_updated")); }; // 初始读取长度逻辑 const initEls = getValidSelection(); if (initEls.length > 0 && initEls[0].points?.length >= 2) { const pts = initEls[0].points; const currentLength = Math.hypot(pts[pts.length-1][0] - pts[0][0], pts[pts.length-1][1] - pts[0][1]); panel.querySelector('#exact-length').value = currentLength.toFixed(2); } // 面板拖拽逻辑 (无全局污染) let isDragging = false, startX, startY, initLeft, initTop; header.onmousedown = (e) => { if(e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top; e.preventDefault(); document.onmousemove = (moveEvent) => { if(!isDragging) return; panel.style.left = (initLeft + moveEvent.clientX - startX) + 'px'; panel.style.top = (initTop + moveEvent.clientY - startY) + 'px'; }; document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; }; }; } createMeasurePanel();