-
嵌入式系列插件第一弹:TikZ 挂件升级为 TikZ 插件
2025-12-04 14:05\usetikzlibrary{calc} \tikzcdset{ curve/.style args={height=#1}{ to path={ (\tikztostart) .. controls ($(\tikztostart)!0.5!(\tikztotarget) - (0,#1)$) and ($(\tikztostart)!0.5!(\tikztotarget) - (0,#1)$) .. (\tikztotarget) } } }@ringx 加上上面这个自定义样式可以接近原本的效果,不过也不完全一致,你可以试试再调整调整,如果效果可以的话我就把它加插件里
-
嵌入式系列插件重大更新:Excalidraw 代码片段功能上线(脚本 / 样式)
2025-11-27 09:34选中元素 Tab 切换形状:
// 图形切换顺序 const SHAPE_CYCLE = ['rectangle', 'ellipse', 'diamond']; const SHAPE_NAMES = { rectangle: '矩形', ellipse: '椭圆', diamond: '菱形' }; // 切换选中元素的图形类型 const cycleSelectedElementShape = () => { const appState = window.excalidrawAPI.getAppState(); const selectedIds = Object.keys(appState.selectedElementIds || {}); // 必须只选中一个元素 if (selectedIds.length !== 1) { return false; } const allElements = window.excalidrawAPI.getSceneElements(); const targetId = selectedIds[0]; const targetElement = allElements.find(el => el.id === targetId); if (!targetElement) return false; const currentType = targetElement.type; if (!SHAPE_CYCLE.includes(currentType)) { return false; // 不是可切换的图形 } // 找到下一个类型(循环) const currentIndex = SHAPE_CYCLE.indexOf(currentType); const nextType = SHAPE_CYCLE[(currentIndex + 1) % SHAPE_CYCLE.length]; // 创建新元素(保持位置、尺寸、样式) const newElement = { ...targetElement, type: nextType, version: (targetElement.version || 0) + 1, // 触发更新 versionNonce: Math.floor(Math.random() * 1000) // 避免缓存 }; // 更新场景 const updatedElements = allElements.map(el => el.id === targetId ? newElement : el ); window.excalidrawAPI.updateScene({ elements: updatedElements }); console.log(`🔄 图形已切换为:${SHAPE_NAMES[nextType]}`); return true; }; // 全局 Tab 键监听(仅在 Excalidraw 画布有焦点时响应) const handleKeyDown = (e) => { // 只处理 Tab 键,且未组合其他修饰键 if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) { e.preventDefault(); // 阻止默认 Tab 行为(如焦点跳转) const handled = cycleSelectedElementShape(); if (handled) { // 可选:阻止冒泡 e.stopImmediatePropagation(); } } }; // 将监听器绑定到 Excalidraw 容器(更精准,避免全局污染) const attachTabListener = () => { const excalidrawContainer = document.querySelector('.excalidraw'); if (excalidrawContainer && !excalidrawContainer.hasTabListener) { excalidrawContainer.addEventListener('keydown', handleKeyDown, true); // useCapture=true 更早拦截 excalidrawContainer.hasTabListener = true; console.log('✅ Tab 图形切换监听器已启用'); } }; // 同时尝试立即绑定(如果已存在) attachTabListener(); -
嵌入式系列插件重大更新:Excalidraw 代码片段功能上线(脚本 / 样式)
2025-11-27 09:23选中元素网格布局:
const arrangeSelectedElementsInGrid = (spacing = 10) => { const appState = window.excalidrawAPI.getAppState(); const selectedIds = Object.keys(appState.selectedElementIds || {}); if (!selectedIds || selectedIds.length === 0) { console.log("⚠️ 请先选择至少一个元素"); return; } const allElements = window.excalidrawAPI.getSceneElements(); const selectedElements = allElements.filter(el => selectedIds.includes(el.id)); if (selectedElements.length === 0) { console.log("⚠️ 未找到选中的元素"); return; } // === 1. 按 y 聚类成行 === const sortedByY = [...selectedElements].sort((a, b) => a.y - b.y); const avgHeight = selectedElements.reduce((sum, el) => sum + el.height, 0) / selectedElements.length; const rowThreshold = Math.max(avgHeight * 0.6, 20); // 行容差 const rows = []; let currentRow = [sortedByY[0]]; for (let i = 1; i < sortedByY.length; i++) { const prev = currentRow[currentRow.length - 1]; const curr = sortedByY[i]; if (Math.abs(curr.y - prev.y) <= rowThreshold) { currentRow.push(curr); } else { rows.push(currentRow); currentRow = [curr]; } } rows.push(currentRow); // 每行内部按 x 排序(从左到右) rows.forEach(row => row.sort((a, b) => a.x - b.x)); // === 2. 计算每列最大宽度、每行最大高度 === const maxColumns = Math.max(...rows.map(row => row.length)); const columnWidths = new Array(maxColumns).fill(0); const rowHeights = rows.map(row => Math.max(...row.map(el => el.height)) ); // 填充 columnWidths for (let colIndex = 0; colIndex < maxColumns; colIndex++) { for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const row = rows[rowIndex]; if (colIndex < row.length) { columnWidths[colIndex] = Math.max(columnWidths[colIndex], row[colIndex].width); } } } // === 3. 重新定位:每个元素左上角 = 网格单元格左上角 === const updates = new Map(); // 起点:以整个选区最左上角元素的原始位置为基准(保持整体位置不变) const globalMinX = Math.min(...selectedElements.map(el => el.x)); const globalMinY = Math.min(...selectedElements.map(el => el.y)); let currentY = globalMinY; for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { let currentX = globalMinX; // 每行都从全局最左开始 for (let colIndex = 0; colIndex < rows[rowIndex].length; colIndex++) { const el = rows[rowIndex][colIndex]; // ✅ 关键:元素左上角直接设为 (currentX, currentY) updates.set(el.id, { x: currentX, y: currentY }); // 移动到下一列:加上该列统一宽度 + 间距 currentX += columnWidths[colIndex] + spacing; } // 移动到下一行:加上该行统一高度 + 间距 currentY += rowHeights[rowIndex] + spacing; } // === 4. 应用更新 === const updatedElements = allElements.map(el => { if (updates.has(el.id)) { const { x, y } = updates.get(el.id); return { ...el, x, y }; } return el; }); window.excalidrawAPI.updateScene({ elements: updatedElements }); console.log(`✅ 已将 ${selectedElements.length} 个元素按网格左上对齐`); }; const mutationObserver = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const menuElement = node.querySelector(".popover .context-menu"); if (menuElement) { const itemElement = document.createElement("li"); itemElement.setAttribute("data-testid", "arrangeSelectedElementsInRow"); itemElement.innerHTML = ` <button type="button" class="context-menu-item"> <div class="context-menu-item__label">将选中的元素网格布局</div> <kbd class="context-menu-item__shortcut"></kbd> </button>`; itemElement.addEventListener("click", () => { arrangeSelectedElementsInGrid(10); }) menuElement.querySelector(`[data-testid="wrapSelectionInFrame"]`)?.insertAdjacentElement('afterend', itemElement); } } }); } } }); mutationObserver.observe(document.body, { childList: true, subtree: true }); ``` -
开发思源笔记思维导图插件之 simple mindmap 坑
2025-11-26 22:23我的想法是把它做成类似我的 嵌入式系列 伪代码 插件的形式,和思源块内容是绑定的,可以随意开关 思维导图 视图。这样子需要的时候可以切换回列表形式
-
开发思源笔记思维导图插件之 simple mindmap 坑
2025-11-26 20:05看起来 simple mind map 确实挺好用的,等我最近论文写完就回来做 嵌入式系列 的思维导图插件。其实 simple mind map 原作者更新频率不高在我看来也有好处,方便 fork 过来自己魔改,也不用太考虑兼容问题。tikz 那个用的 tikzjax 库就是这个情况,这个库已经经过三个人转手了,到我这里才通过魔改解决中文显示问题。
-
开发思源笔记思维导图插件之 simple mindmap 坑
2025-11-26 19:39如果 Excalidraw 能做到这样是不是也可以平替思维导图的功能了:https://www.bilibili.com/video/BV1Je411e7Qx
-
嵌入式系列插件第三弹:draw.io 插件
2025-11-24 20:07如果你只是想要图片背景不透明的话可以使用以下 css 代码片段:
.viewer-canvas img[src^="assets/drawio-"] { background-color: white; }如果你想要图片查看器整个背景都不是透明的可以用:
.viewer-canvas { background-color: white; }灯箱主要是服务于 draw.io 具有多页的情况的,并不打算让它覆盖思源本身的图像查看器
-
嵌入式系列插件第四弹:Excalidraw 插件
2025-11-21 18:11两个插件最近会把明暗模式和 Tab 编辑都加上。另外,draw.io 除了明暗模式,其他功能应该是全量的了,如果发现缺什么了可以详细说说,可能我没有发现
-
有没有插件开发大佬能集成一下 quiver ?
2025-11-14 19:44嵌入到图片包没问题的,我看看后续做一下,加入到 嵌入式系列 里,目前的安排是先做完 Excalidraw,然后再做这个。中间视情况可能会插入个思维导图,不过应该一个月以内会有吧(没意外的话)
-
STtools 插件:白板改进(v0.22.x)
2025-11-14 14:38这白板无敌了 👍,请问考虑拆分成单独的插件吗,我个人觉得整个插件多了太多用不到的功能会有点重/冗余,如果想保留个人品牌也可以考虑像我这样统一为一个 嵌入式系列
-
嵌入式系列插件第三弹:draw.io 插件
2025-11-14 11:24
我把你的 svg 拖进思源里是正常的,会不会是你的主题或者 css 代码片段设置了图像圆角,我看那个位置刚好在角落。你可以创建只有以一个矩形的图像测试一下是不是这个原因。还可以创建一个新的工作空间把图像放进去看看是不是思源/系统本身问题。
-
mermaid 块不支持鼠标缩放其内容吗?
2025-11-10 14:59我写了个 draw.io 插件,里面可以写 mermaid,然后图像会以 SVG 图像形式存储于思源,因此可以借助思源本身的图像查看器缩放查看 mermaid 图片,算是个官方修改前的解决方案。详情看 嵌入式系列插件第三弹:draw.io 插件
-
希望增加一个 mermaid 的缩放拖拽功能
2025-11-10 14:59我写了个 draw.io 插件,里面可以写 mermaid,然后图像会以 SVG 图像形式存储于思源,因此可以借助思源本身的图像查看器缩放查看 mermaid 图片,算是个官方修改前的解决方案。详情看 嵌入式系列插件第三弹:draw.io 插件
-
嵌入式系列插件第三弹:draw.io 插件
2025-11-10 14:54意外发现有个额外用途:思源目前的 mermaid 渲染出的图存在无法放大查看细节的问题,而刚好 draw.io 里面可以写 mermaid,于是借助这个插件编辑后得到的图像是可以用思源本身的图片查看器查看细节的


