--- name: 初始化脑图配置 description: 将普通的连线节点组合转化为受引擎控制的脑图结构,并赋予默认样式。 author: ymjr version: 1.1.2 license: MIT usage: 选中希望作为脑图根节点的元素(也可以只选中一个),运行脚本后在弹窗中配置排版方向与主题,即可一键生成结构化脑图。 features: - 自动递归遍历与根节点相连的元素,注入 mindmap 配置与层级结构 - 提供可视化的脑图初始化配置面板 - 基于所选主题自动应用节点颜色与连线样式 dependencies: - 依赖 Feature-Mindmap_Engine 引擎进行排版 autorun: false --- /* ```javascript */ var locales = { zh: { theme_rainbow: "🌈 彩虹分枝", theme_morandi: "🎨 莫兰迪色", theme_business: "💼 商务蓝调", theme_custom: "⚙️ 自定义色", notice_select_root: "请先选中作为根节点的元素!", notice_cancelled_mindmap: "已取消元素的脑图状态。", ui_title: "初始化脑图配置", ui_theme: "主题配色", ui_direction: "排版方向", ui_dir_lr: "向右展开", ui_dir_rl: "向左展开", ui_dir_td: "向下展开", ui_dir_bu: "向上展开", ui_arrow_style: "连线样式", ui_arrow_normal: "平滑曲线", ui_arrow_elbow: "折线", ui_arrow_curve: "S型曲线", ui_fixed_center: "固定在中点", ui_auto_slide: "自动滑动", ui_fixed_point: "箭头固定中点", ui_node_gap: "节点间距", ui_confirm: "确认生成", notice_cancelled_init: "取消脑图初始化。", notice_success: "✨ 成功将元素初始化为脑图并赋予样式!", notice_no_engine: "⚠️ 脑图引擎未运行,无法自动排版,请开启 Engine!" }, en: { theme_rainbow: "🌈 Rainbow", theme_morandi: "🎨 Morandi", theme_business: "💼 Business Blue", theme_custom: "⚙️ Custom", notice_select_root: "Please select elements to be the root node first!", notice_cancelled_mindmap: "Mindmap state removed from elements.", ui_title: "Initialize Mindmap Config", ui_theme: "Theme", ui_direction: "Direction", ui_dir_lr: "Expand Right", ui_dir_rl: "Expand Left", ui_dir_td: "Expand Down", ui_dir_bu: "Expand Up", ui_arrow_style: "Connector Style", ui_arrow_normal: "Normal Curve", ui_arrow_elbow: "Elbow Connector", ui_arrow_curve: "S-Curve", ui_fixed_center: "Fixed at Center", ui_auto_slide: "Auto Sliding", ui_fixed_point: "Fixed Point Arrow", ui_node_gap: "Node Gap", ui_confirm: "Confirm Generate", notice_cancelled_init: "Mindmap initialization cancelled.", notice_success: "✨ Mindmap initialized successfully!", notice_no_engine: "⚠️ Mindmap engine not running, layout skipped. Enable Engine!" } }; locales["zh-cn"] = locales.zh; locales["zh-tw"] = locales.zh; const { Notice } = ea.obsidian; const DEFAULT_LEVELS = [ { level: 1, strokeColor: "black", textColor: "#ffffff", fontSize: 24, strokeWidth: 2 }, { level: 2, strokeColor: "black", textColor: "black", fontSize: 20, strokeWidth: 1 }, { level: 3, strokeColor: "black", textColor: "black", fontSize: 16, strokeWidth: 1 }, { level: 4, strokeColor: "black", textColor: "black", fontSize: 14, strokeWidth: 1 } ]; const FALLBACK_THEMES = { "rainbow": { name: t("theme_rainbow"), rootBg: "#343a40", colors: ["#e03131", "#1971c2", "#099268", "#e8590c", "#9c36b5", "#fcc419", "#3bc9db"], levels: DEFAULT_LEVELS }, "morandi": { name: t("theme_morandi"), rootBg: "#495057", colors: ["#5c7a82", "#b38269", "#788560", "#a68894", "#7a6c5d"], levels: DEFAULT_LEVELS }, "business": { name: t("theme_business"), rootBg: "#0b2545", colors: ["#1864ab", "#0b7285", "#087f5b", "#5f3dc4", "#c92a2a"], levels: DEFAULT_LEVELS }, "custom": { name: t("theme_custom"), rootBg: "#1e1e1e", colors: ["#e03131", "#1971c2", "#099268", "#e8590c", "#9c36b5", "#fcc419"], levels: DEFAULT_LEVELS } }; const FALLBACK_SETTINGS = { direction: "LR", arrowType: "normal", defaultGap: 15, curveLength: 40, lengthBetweenElAndLine: 100, enableFixedPoint: true, theme: "rainbow", customTheme: { rootBg: FALLBACK_THEMES["custom"].rootBg, colors: [...FALLBACK_THEMES["custom"].colors], levels: JSON.parse(JSON.stringify(DEFAULT_LEVELS)) } }; // --- Settings Initial Load (跨脚本共享读取) --- let engineSettings = ExcalidrawAutomate.plugin.settings.scriptEngineSettings["Mindmap_Engine"] || {}; let mindmapConfig; try { if (engineSettings["Mindmap Configuration"] && engineSettings["Mindmap Configuration"].value) { mindmapConfig = JSON.parse(engineSettings["Mindmap Configuration"].value); } else { mindmapConfig = { themes: FALLBACK_THEMES, defaultSettings: FALLBACK_SETTINGS }; } } catch (e) { mindmapConfig = { themes: FALLBACK_THEMES, defaultSettings: FALLBACK_SETTINGS }; } const THEMES = mindmapConfig.themes || FALLBACK_THEMES; let pluginSettings = mindmapConfig.defaultSettings || FALLBACK_SETTINGS; // -------------------------------- function lightenHex(hex, percent) { if (!hex || hex === "transparent") return hex; let num = parseInt(hex.replace("#",""), 16); if (isNaN(num)) return hex; let amt = Math.round(2.55 * percent); let R = (num >> 16) + amt; let G = (num >> 8 & 0x00FF) + amt; let B = (num & 0x0000FF) + amt; return "#" + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1); } function resolveStyle(level, branchIndex, settings) { const themeKey = settings?.theme || "rainbow"; let theme = THEMES[themeKey] || THEMES["rainbow"]; if (themeKey === "custom" && settings?.customTheme) theme = { ...theme, ...settings.customTheme }; const levels = theme.levels || DEFAULT_LEVELS; const lvlConfig = level <= levels.length ? levels[level - 1] : levels[levels.length - 1]; let bg = "transparent"; if (level === 1) bg = theme.rootBg; else { const colors = theme.colors && theme.colors.length > 0 ? theme.colors : THEMES["rainbow"].colors; const baseColor = colors[branchIndex % colors.length]; bg = level > 2 ? lightenHex(baseColor, Math.min((level - 2) * 15, 60)) : baseColor; } return { backgroundColor: bg, textColor: lvlConfig.textColor, strokeColor: lvlConfig.strokeColor, fontSize: lvlConfig.fontSize, strokeWidth: lvlConfig.strokeWidth }; } const selectedEls = ea.getViewSelectedElements(); if (selectedEls.length === 0) { new Notice(t("notice_select_root")); return; } if (selectedEls[0]?.customData?.mindmap) { selectedEls.forEach((el) => { if (el?.customData?.mindmap) delete el.customData.mindmap; }); new Notice(t("notice_cancelled_mindmap")); return; } function getMindmapSettingsViaUI() { return new Promise((resolve) => { if (document.getElementById("ea-mindmap-init-panel")) return resolve(null); const panel = document.createElement('div'); panel.id = "ea-mindmap-init-panel"; panel.style.cssText = `position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); width:min(320px, 92vw); max-height:85vh; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 8px 24px rgba(0,0,0,0.25); border-radius:8px; z-index:9999; display:flex; flex-direction:column; overflow:hidden;`; 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; border-radius: 8px 8px 0 0;`; header.innerHTML = `${t("ui_title")}`; header.querySelector('button').onclick = () => { panel.remove(); resolve(null); }; panel.appendChild(header); const content = document.createElement('div'); content.style.cssText = `padding:15px; display:flex; flex-direction:column; gap:12px;`; const createRow = (label, element) => { 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);"; div.appendChild(lbl); div.appendChild(element); return div; }; const themeSelect = document.createElement('select'); themeSelect.style.width = "120px"; Object.keys(THEMES).forEach(k => { const o = document.createElement('option'); o.value = k; o.innerText = ["rainbow", "morandi", "business", "custom"].includes(k) ? t("theme_" + k) : THEMES[k].name; if (pluginSettings.theme === k) o.selected = true; themeSelect.appendChild(o); }); content.appendChild(createRow(t("ui_theme"), themeSelect)); const dirSelect = document.createElement('select'); dirSelect.style.width = "120px"; dirSelect.innerHTML = ``; dirSelect.value = pluginSettings.direction; content.appendChild(createRow(t("ui_direction"), dirSelect)); const arrowSelect = document.createElement('select'); arrowSelect.style.width = "120px"; arrowSelect.innerHTML = ``; arrowSelect.value = pluginSettings.arrowType; content.appendChild(createRow(t("ui_arrow_style"), arrowSelect)); const fixedPointSelect = document.createElement('select'); fixedPointSelect.style.width = "120px"; fixedPointSelect.innerHTML = ``; fixedPointSelect.value = pluginSettings.enableFixedPoint ? "true" : "false"; content.appendChild(createRow(t("ui_fixed_point"), fixedPointSelect)); const gapInput = document.createElement('input'); gapInput.type = "number"; gapInput.value = pluginSettings.defaultGap; gapInput.style.width = "80px"; content.appendChild(createRow(t("ui_node_gap"), gapInput)); 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 confirmBtn = document.createElement('button'); confirmBtn.innerText = t("ui_confirm"); confirmBtn.className = "mod-cta"; confirmBtn.onclick = () => { panel.remove(); resolve({ direction: dirSelect.value, arrowType: arrowSelect.value, defaultGap: Number(gapInput.value), curveLength: Number(pluginSettings.curveLength), lengthBetweenElAndLine: Number(pluginSettings.lengthBetweenElAndLine), enableFixedPoint: fixedPointSelect.value === "true", theme: themeSelect.value, customTheme: pluginSettings.customTheme }); }; footer.appendChild(confirmBtn); panel.appendChild(footer); document.body.appendChild(panel); let isDragging = false, startX, startY, initLeft, initTop; const startDrag = (clientX, clientY, target) => { if (target && target.tagName === 'BUTTON') return; isDragging = true; startX = clientX; startY = clientY; const rect = panel.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top; panel.style.transform = 'none'; panel.style.left = `${initLeft}px`; panel.style.top = `${initTop}px`; }; const doDrag = (clientX, clientY) => { if (!isDragging) return; const newLeft = Math.max(10, Math.min(window.innerWidth - panel.offsetWidth - 10, initLeft + clientX - startX)); const newTop = Math.max(10, Math.min(window.innerHeight - panel.offsetHeight - 10, initTop + clientY - startY)); panel.style.left = `${newLeft}px`; panel.style.top = `${newTop}px`; }; const endDrag = () => { isDragging = false; }; header.onmousedown = (e) => { startDrag(e.clientX, e.clientY, e.target); e.preventDefault(); document.onmousemove = (ev) => doDrag(ev.clientX, ev.clientY); document.onmouseup = () => { endDrag(); document.onmousemove = null; document.onmouseup = null; }; }; header.ontouchstart = (e) => { if (e.touches && e.touches.length === 1) { startDrag(e.touches[0].clientX, e.touches[0].clientY, e.target); } }; header.ontouchmove = (e) => { if (isDragging && e.touches && e.touches.length === 1) { doDrag(e.touches[0].clientX, e.touches[0].clientY); e.preventDefault(); } }; header.ontouchend = () => endDrag(); header.ontouchcancel = () => endDrag(); }); } (async () => { const settings = await getMindmapSettingsViaUI(); if (!settings) { new Notice(t("notice_cancelled_init")); return; } await ea.copyViewElementsToEAforEditing(selectedEls); function getRoot(element, recursion = true) { let arrowElements = selectedEls.filter((el) => el.type == "arrow" && el.endBinding && el.endBinding.elementId == element.id); if (arrowElements.length == 0) return ea.getElement(element.id); let parentElements = arrowElements.map((el) => ea.getElement(el.startBinding.elementId)); if (recursion) return getRoot(parentElements[0], true); } let rootEl = getRoot(selectedEls[0]); let branchCounter = 0; function traverse(element, parentId, level, branchIndex = 0) { let arrowElements = selectedEls.filter((el) => el.type == "arrow" && el.startBinding && el.startBinding.elementId == element.id); let childrenElements = arrowElements.map((el) => ea.getElement(el.endBinding.elementId)); if (level === 2) branchIndex = branchCounter++; element.customData = { ...element.customData, mindmap: { status: "open", root: rootEl.id, parent: parentId, level: level } }; const targetStyle = resolveStyle(level, branchIndex, settings); if (targetStyle) { // 设置节点样式 element.backgroundColor = targetStyle.backgroundColor; element.strokeColor = targetStyle.strokeColor || "black"; element.strokeWidth = targetStyle.strokeWidth || 1; if (element.type === "text") element.fontSize = targetStyle.fontSize; else if (element.boundElements) { element.boundElements.forEach(b => { if (b.type === "text") { const textEl = ea.getElement(b.id); if (textEl) { textEl.strokeColor = targetStyle.textColor || "black"; textEl.fontSize = targetStyle.fontSize; } } }); } // 同步修改连接到该节点的箭头样式 const incomingArrow = selectedEls.find(a => a.type === "arrow" && a.endBinding && a.endBinding.elementId === element.id); if (incomingArrow) { incomingArrow.strokeColor = targetStyle.strokeColor || "black"; incomingArrow.strokeWidth = targetStyle.strokeWidth || 1; incomingArrow.customData = incomingArrow.customData || {}; if (settings.arrowType === "curve") { incomingArrow.customData.curveArrow = true; } else { delete incomingArrow.customData.curveArrow; } if (settings.enableFixedPoint) { if (incomingArrow.startBinding) incomingArrow.startBinding.mode = "inside"; if (incomingArrow.endBinding) incomingArrow.endBinding.mode = "inside"; } else { if (incomingArrow.startBinding) { delete incomingArrow.startBinding.mode; delete incomingArrow.startBinding.fixedPoint; } if (incomingArrow.endBinding) { delete incomingArrow.endBinding.mode; delete incomingArrow.endBinding.fixedPoint; } } } } childrenElements.forEach((el) => traverse(el, element.id, level + 1, branchIndex)); } traverse(rootEl, null, 1); rootEl.customData.mindmap.settings = settings; await ea.addElementsToView(); new Notice(t("notice_success")); if (window.MindmapAPI && window.MindmapAPI.runLayout) { setTimeout(async () => { const updatedEls = ea.getViewElements().filter(el => el.customData?.mindmap?.root === rootEl.id || (el.type === "arrow" && el.startBinding)); await window.MindmapAPI.runLayout(updatedEls, true, ea); }, 100); } else { new Notice(t("notice_no_engine")); } })();