--- name: 连线定长配置面板 description: 悬浮面板:开启或关闭所选连线的定长旋转模式。 author: ymjr version: 1.0.0 license: MIT usage: 当需要将连线或箭头的长度锁定(如绘制圆规效果),只允许旋转而不改变长度时使用。 features: - 提供可拖拽的定长控制悬浮面板 - 支持一键开启或解除选中连线的定长锁定状态 dependencies: - 需要配合 Feature-Fixed-Length-Engine 引擎使用以实现拦截和控制 autorun: false --- /* ```javascript */ var locales = { zh: { notice_no_arrow: "⚠️ 请先选中至少一根连线或箭头", notice_locked: "📏 已开启定长锁定", notice_unlocked: "🔓 已解除定长限制", panel_title: "📏 定长旋转控制", panel_desc: "开启后,拖拽连线端点时将固定其长度,仅允许改变旋转角度 (圆规模式)。", btn_enable: "开启锁定", btn_disable: "解除锁定" }, en: { notice_no_arrow: "⚠️ Please select at least one line or arrow", notice_locked: "📏 Fixed length locked", notice_unlocked: "🔓 Fixed length unlocked", panel_title: "📏 Fixed Length Control", panel_desc: "When enabled, dragging line endpoints will fix their length, allowing only rotation (compass mode).", btn_enable: "Enable Lock", btn_disable: "Disable Lock" } }; const { Notice } = ea.obsidian; const api = ea.getExcalidrawAPI(); // ========================================== // 1. 核心业务逻辑 // ========================================== function toggleFixedLength(enable) { const selectedEls = ea.getViewSelectedElements().filter(el => ["arrow", "line"].includes(el.type)); if (selectedEls.length === 0) { new Notice(t("notice_no_arrow")); return; } selectedEls.forEach((el) => { if (enable) { el.customData = { ...el.customData, fixedLength: true }; } else { if (el.customData && el.customData.fixedLength) { delete el.customData.fixedLength; } } }); ea.copyViewElementsToEAforEditing(selectedEls); ea.addElementsToView(false, false, false); new Notice(enable ? t("notice_locked") : t("notice_unlocked")); } // 检查当前选中元素的状态,用于初始化 UI function checkCurrentState() { const selectedEls = ea.getViewSelectedElements().filter(el => ["arrow", "line"].includes(el.type)); if (selectedEls.length === 0) return false; return selectedEls.some(el => el.customData?.fixedLength); } // ========================================== // 2. UI 构建与交互逻辑 // ========================================== function createFixedLengthPanel() { const PANEL_ID = "ea-fixed-length-adv-panel"; if (document.getElementById(PANEL_ID)) return; // 主体容器 const panel = document.createElement('div'); panel.id = PANEL_ID; panel.style.cssText = `position:fixed; top:150px; left:80px; width:240px; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 10px 30px rgba(0,0,0,0.25); border-radius:8px; z-index:9999; display:flex; flex-direction:column; font-family: var(--font-interface);`; // 头部区域 (支持拖拽) const header = document.createElement('div'); header.style.cssText = `padding:10px 14px; 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;`; header.innerHTML = `${t("panel_title")}`; panel.appendChild(header); // 内容区域 const content = document.createElement('div'); content.style.cssText = `padding:16px 14px; display:flex; flex-direction:column; gap:12px;`; const description = document.createElement('div'); description.style.cssText = `font-size: 12px; color: var(--text-muted); line-height: 1.4;`; description.innerText = t("panel_desc"); content.appendChild(description); // 状态与按钮区 const actionArea = document.createElement('div'); actionArea.style.cssText = "display:flex; justify-content:space-between; gap: 8px; margin-top: 4px;"; // 初始化状态判定 const isCurrentlyActive = checkCurrentState(); const btnEnable = document.createElement('button'); btnEnable.innerText = t("btn_enable"); btnEnable.style.cssText = `flex: 1; padding: 6px; cursor: pointer; border-radius: 4px; font-size: 13px; ${isCurrentlyActive ? 'background: var(--interactive-accent); color: var(--text-on-accent); border: none;' : ''}`; const btnDisable = document.createElement('button'); btnDisable.innerText = t("btn_disable"); btnDisable.style.cssText = `flex: 1; padding: 6px; cursor: pointer; border-radius: 4px; font-size: 13px; ${!isCurrentlyActive ? 'background: var(--background-modifier-error); color: var(--text-on-accent); border: none;' : ''}`; // 点击事件处理 btnEnable.onclick = () => { toggleFixedLength(true); btnEnable.style.cssText = "flex: 1; padding: 6px; cursor: pointer; border-radius: 4px; font-size: 13px; background: var(--interactive-accent); color: var(--text-on-accent); border: none;"; btnDisable.style.cssText = "flex: 1; padding: 6px; cursor: pointer; border-radius: 4px; font-size: 13px;"; }; btnDisable.onclick = () => { toggleFixedLength(false); btnDisable.style.cssText = "flex: 1; padding: 6px; cursor: pointer; border-radius: 4px; font-size: 13px; background: var(--background-modifier-error); color: var(--text-on-accent); border: none;"; btnEnable.style.cssText = "flex: 1; padding: 6px; cursor: pointer; border-radius: 4px; font-size: 13px;"; }; actionArea.appendChild(btnEnable); actionArea.appendChild(btnDisable); content.appendChild(actionArea); panel.appendChild(content); // 挂载到 DOM document.body.appendChild(panel); // 关闭逻辑 header.querySelector('#close-btn').onclick = () => panel.remove(); // 拖拽逻辑 (无全局变量污染,使用闭包变量) 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; }; }; } createFixedLengthPanel();