--- name: 生成平行四边形 description: 在鼠标位置生成可调节角度和边长的平行四边形,支持整体旋转。 author: ymjr version: 1.0.0 license: MIT usage: 运行脚本,打开“平行四边形生成器”面板,调整两条相邻边的长度、内角大小以及形状整体旋转角度。 features: - 实时更新的悬浮 UI 面板 - 基于三角函数精确定位顶点并转换为闭合线段(line polygon) dependencies: - 无前置依赖 autorun: false --- /* ```javascript */ var locales = { zh: { ui_title: "平行四边形生成器", ui_side_a: "底边长度", ui_side_b: "斜边长度", ui_angle: "形状夹角 (°)", ui_rotation: "整体旋转 (°)", ui_confirm: "确认生成", notice_success: "平行四边形已生成", notice_cancelled: "已取消" }, en: { ui_title: "Parallelogram Generator", ui_side_a: "Side A", ui_side_b: "Side B", ui_angle: "Angle (°)", ui_rotation: "Global Rotation (°)", ui_confirm: "Confirm Generate", notice_success: "Parallelogram generated", notice_cancelled: "Cancelled" } }; const { Notice } = ea.obsidian; // --- 1. 锁定锚点 --- const origin = ea.getViewLastPointerPosition(); const centerX = origin.x; const centerY = origin.y; // --- 2. 配置与状态 --- let settings = { sideA: 150, sideB: 100, angle: 60, globalRotation: 0 }; let previewIDs = []; // --- 3. 核心绘图逻辑 --- async function runDraw(isPreview = true) { if (previewIDs.length > 0) { const api = ea.getExcalidrawAPI(); const next = api.getSceneElements().filter(el => !previewIDs.includes(el.id)); ea.targetView.updateScene({ elements: next, storeAction: "none" }); previewIDs = []; } ea.clear(); ea.style.strokeWidth = 2; ea.style.roughness = 0; ea.style.backgroundColor = "transparent"; ea.style.fillStyle = "solid"; const { sideA, sideB, angle, globalRotation } = settings; const rad = (angle * Math.PI) / 180; const p0 = [0, 0]; const p1 = [sideA, 0]; const p2 = [sideA + sideB * Math.cos(rad), -sideB * Math.sin(rad)]; const p3 = [sideB * Math.cos(rad), -sideB * Math.sin(rad)]; let rawPoints = [p0, p1, p2, p3, p0]; let sumX = 0, sumY = 0; for(let i = 0; i < 4; i++) { sumX += rawPoints[i][0]; sumY += rawPoints[i][1]; } const centroidX = sumX / 4; const centroidY = sumY / 4; const globalRad = (globalRotation * Math.PI) / 180; const cosRot = Math.cos(globalRad); const sinRot = Math.sin(globalRad); const absolutePoints = rawPoints.map(p => { const dx = p[0] - centroidX; const dy = p[1] - centroidY; const rx = dx * cosRot - dy * sinRot; const ry = dx * sinRot + dy * cosRot; return [centerX + rx, centerY + ry]; }); const polyId = ea.addLine(absolutePoints); const el = ea.getElement(polyId); if (el) { el.polygon = true; el.type = "line"; } previewIDs.push(polyId); await ea.addElementsToView(false, !isPreview, false); } // --- 4. 统一 UI 构建 --- function createFloatingPanel() { if (document.getElementById("ea-parallelogram-panel")) return; const panelWidth = 280; const panelHeight = 260; const startLeft = Math.max(0, (window.innerWidth - panelWidth) / 2); const startTop = Math.max(0, (window.innerHeight - panelHeight) / 2); const panel = document.createElement('div'); panel.id = "ea-parallelogram-panel"; 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 4px 12px rgba(0,0,0,0.2); border-radius:8px; z-index:9999; display:flex; flex-direction:column;`; const header = document.createElement('div'); header.style.cssText = `padding:10px 15px; 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;`; header.innerHTML = `${t("ui_title")}`; header.querySelector('button').onclick = () => closePanel(false); panel.appendChild(header); const content = document.createElement('div'); content.style.cssText = `padding:15px; display:flex; flex-direction:column; gap:12px;`; const createRow = (label, key, min, max, step = 1) => { const div = document.createElement('div'); div.style.cssText = "display:flex; justify-content:space-between; align-items:center;"; const lbl = document.createElement('label'); lbl.innerText = label; lbl.style.cssText = "font-size:12px; color:var(--text-muted);"; const input = document.createElement('input'); input.type = "number"; input.value = settings[key]; input.style.width = "80px"; input.step = step; if (min !== undefined) input.min = min; if (max !== undefined) input.max = max; input.onchange = (e) => { let val = parseFloat(e.target.value) || 0; settings[key] = val; runDraw(true); }; div.appendChild(lbl); div.appendChild(input); return div; }; content.appendChild(createRow(t("ui_side_a"), "sideA", 1)); content.appendChild(createRow(t("ui_side_b"), "sideB", 1)); content.appendChild(createRow(t("ui_angle"), "angle", -360, 360)); content.appendChild(createRow(t("ui_rotation"), "globalRotation", -360, 360, 15)); panel.appendChild(content); const footer = document.createElement('div'); footer.style.cssText = `padding:10px 15px; border-top:1px solid var(--background-modifier-border); display:flex; justify-content:flex-end;`; const btn = document.createElement('button'); btn.innerText = t("ui_confirm"); btn.className = "mod-cta"; btn.onclick = () => closePanel(true); footer.appendChild(btn); panel.appendChild(footer); document.body.appendChild(panel); // --- 防越界拖拽逻辑 --- 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 = (e) => { if(!isDragging) return; let newLeft = initLeft + e.clientX - startX; let newTop = initTop + e.clientY - startY; // 边界约束 newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - rect.width)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - rect.height)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; }; document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; }; }; runDraw(true); } function closePanel(confirm) { const panel = document.getElementById("ea-parallelogram-panel"); if (panel) panel.remove(); if (confirm) { runDraw(false); new Notice(t("notice_success")); } else { if (previewIDs.length > 0) { const api = ea.getExcalidrawAPI(); const next = api.getSceneElements().filter(el => !previewIDs.includes(el.id)); ea.targetView.updateScene({ elements: next, storeAction: "none" }); new Notice(t("notice_cancelled")); } } } createFloatingPanel();