---
name: 智能手绘工具箱 (整合版)
description: 提供自动识别画笔锁定开关,以及针对选中元素的手动强制转换面板。
author: ymjr
version: 1.0.0
license: MIT
usage: 呼出工具箱。点击顶部开关可锁定画笔,接下来画的手绘线延迟 0.5s 自动变规则图形;选中已有手绘也可手动转换。
dependencies:
- 必须依赖 [Feature-Shape-Engine] 核心引擎常驻后台
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
notice_no_engine: "⚠️ 找不到识别引擎,请先运行 Feature-Shape-Engine!",
notice_select: "请先选中需要转换的线条!",
ui_title: "✨ 图形转换工具箱",
ui_auto_mode: "画笔锁定 (延迟0.5s自动识别)",
ui_auto: "智能识别",
ui_rect: "矩形",
ui_ellipse: "椭圆",
ui_diamond: "菱形",
ui_line: "直线",
ui_cancel: "关闭",
ui_convert: "转换选中项",
notice_success: "🎉 转换成功!"
},
en: {
notice_no_engine: "⚠️ Engine not found! Run Feature-Shape-Engine first.",
notice_select: "Please select elements first!",
ui_title: "✨ Shape Toolbox",
ui_auto_mode: "Pen Lock (Auto-detect after 0.5s)",
ui_auto: "Auto Detect",
ui_rect: "Rect",
ui_ellipse: "Ellipse",
ui_diamond: "Diamond",
ui_line: "Line",
ui_cancel: "Close",
ui_convert: "Convert Selected",
notice_success: "🎉 Converted!"
}
};
const api = ea.getExcalidrawAPI();
if (!ea.plugin._ymjr_shape || !ea.plugin._ymjr_shape.convertElements) {
new Notice(t("notice_no_engine"));
return;
}
const ShapeCore = ea.plugin._ymjr_shape;
if (document.getElementById("ea-shape-toolbox-panel")) {
document.getElementById("ea-shape-toolbox-panel").remove();
}
// ---------------------------
// 渲染 HTML 整合面板 UI
// ---------------------------
const panel = document.createElement('div');
panel.id = "ea-shape-toolbox-panel";
panel.style.cssText = `position:fixed; top:15vh; right:5vw; width:300px; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 8px 24px rgba(0,0,0,0.2); border-radius:10px; z-index:9999; display:flex; flex-direction:column; overflow:hidden; font-family:var(--font-interface);`;
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; font-weight:bold; color:var(--text-normal);`;
header.innerHTML = `${t("ui_title")}`;
panel.appendChild(header);
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;
panel.style.left = (initLeft + e.clientX - startX) + 'px';
panel.style.top = (initTop + e.clientY - startY) + 'px';
panel.style.right = "auto"; // 清除 right 避免冲突
};
document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; };
};
const content = document.createElement('div');
content.style.cssText = `padding:16px; display:flex; flex-direction:column; gap:16px;`;
// ---- 区块 1:自动模式锁定开关 ----
const autoCard = document.createElement('div');
autoCard.style.cssText = `padding:12px; border-radius:8px; border:1px solid var(--background-modifier-border); background:var(--background-primary-alt); display:flex; justify-content:space-between; align-items:center; cursor:pointer;`;
const autoLabel = document.createElement('span');
autoLabel.innerHTML = `🔒 ${t("ui_auto_mode")}`;
autoLabel.style.fontSize = "13px";
const toggleInput = document.createElement('input');
toggleInput.type = "checkbox";
toggleInput.checked = ShapeCore.autoMode; // 初始化读取引擎状态
toggleInput.style.cssText = `cursor:pointer; width:16px; height:16px;`;
autoCard.onclick = (e) => {
if (e.target !== toggleInput) toggleInput.checked = !toggleInput.checked;
ShapeCore.autoMode = toggleInput.checked; // 实时写回引擎状态
autoCard.style.borderColor = toggleInput.checked ? "var(--interactive-accent)" : "var(--background-modifier-border)";
};
autoCard.style.borderColor = toggleInput.checked ? "var(--interactive-accent)" : "var(--background-modifier-border)";
autoCard.appendChild(autoLabel);
autoCard.appendChild(toggleInput);
content.appendChild(autoCard);
// ---- 分割线 ----
const divider = document.createElement('div');
divider.style.cssText = `height:1px; background:var(--background-modifier-border); width:100%;`;
content.appendChild(divider);
// ---- 区块 2:手动转换选中项 ----
const manualSection = document.createElement('div');
manualSection.style.cssText = `display:flex; flex-direction:column; gap:8px;`;
const modes = [
{ id: "auto", label: t("ui_auto") },
{ id: "rectangle", label: t("ui_rect") },
{ id: "ellipse", label: t("ui_ellipse") },
{ id: "diamond", label: t("ui_diamond") },
{ id: "line", label: t("ui_line") }
];
let selectedMode = "auto";
const radioGroup = document.createElement('div');
radioGroup.style.cssText = `display:flex; flex-wrap:wrap; gap:8px;`;
modes.forEach(mode => {
const label = document.createElement('label');
label.style.cssText = `display:flex; align-items:center; gap:6px; cursor:pointer; padding:4px 8px; border-radius:4px; font-size:13px; background:var(--background-secondary); border:1px solid transparent; transition:all 0.2s;`;
const radio = document.createElement('input');
radio.type = "radio"; radio.name = "shapeMode"; radio.value = mode.id;
if(mode.id === "auto") { radio.checked = true; label.style.borderColor = "var(--interactive-accent)"; }
radio.onchange = (e) => {
selectedMode = e.target.value;
Array.from(radioGroup.children).forEach(l => l.style.borderColor = "transparent");
label.style.borderColor = "var(--interactive-accent)";
};
label.appendChild(radio); label.appendChild(document.createTextNode(mode.label));
radioGroup.appendChild(label);
});
manualSection.appendChild(radioGroup);
const convertBtn = document.createElement('button');
convertBtn.innerText = t("ui_convert");
convertBtn.style.cssText = `margin-top:8px; background:var(--interactive-accent); color:var(--text-on-accent); border:none; padding:8px 14px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%;`;
convertBtn.onclick = async () => {
const selectedEls = ea.getViewSelectedElements().filter(el => el.type === "freedraw");
if (selectedEls.length === 0) {
new Notice(t("notice_select"));
return;
}
await ShapeCore.convertElements(selectedEls, ea, selectedMode);
new Notice(t("notice_success"));
};
manualSection.appendChild(convertBtn);
content.appendChild(manualSection);
panel.appendChild(content);
document.body.appendChild(panel);
header.querySelector('#st-close-btn').onclick = () => panel.remove();