diff --git a/plugins/canvas/registry/build.mjs b/plugins/canvas/registry/build.mjs index dc7a92e..cb9befd 100644 --- a/plugins/canvas/registry/build.mjs +++ b/plugins/canvas/registry/build.mjs @@ -16,7 +16,7 @@ const nodeModules = join(root, "node_modules"); // 与插件 src 里 definePlugin({version}) 保持一致,避免清单与产物版本脱节。 const OFFICIAL = [ { id: "markdown", dir: "markdown", name: "Markdown 节点", description: "在画布中编辑与渲染 Markdown", icon: "📝" }, - { id: "svg", dir: "svg", name: "SVG 节点", description: "编辑与渲染 SVG,可接收上游文本节点的 SVG 源码", icon: "🔷" }, + { id: "svg", dir: "svg", name: "SVG 节点", description: "透明背景渲染 SVG,可接收上游文本节点的 SVG 源码", icon: "🔷" }, { id: "html", dir: "html", name: "HTML 节点", description: "沙箱 iframe 渲染 HTML,支持 {{input}} 注入上游文本", icon: "🌐" }, { id: "panorama", dir: "panorama", name: "3D 全景节点", description: "查看 360° 等距柱状全景图,可从上游图片节点取图", icon: "🧭" }, { id: "sticky-note", dir: "sticky-note", name: "便利贴节点", description: "可自选颜色、双击编辑、拖动即可移动的便利贴", icon: "📌" }, diff --git a/plugins/canvas/sdk/src/types.ts b/plugins/canvas/sdk/src/types.ts index 32edf76..ab605bc 100644 --- a/plugins/canvas/sdk/src/types.ts +++ b/plugins/canvas/sdk/src/types.ts @@ -275,6 +275,8 @@ export type CanvasNodeDefinition = { showInCreateMenu?: boolean; // 默认 true hasSourceHandle?: boolean; // 右侧输出连接点,默认 true hidePanel?: boolean; // 为 true 时:点击/新建不弹出下方面板(含内置生图面板),纯展示型节点用 + // 为 true 时:节点卡片背景与边框透明,内容直接融入画布(如 SVG/矢量图);选中时仍显示选中描边 + transparentBackground?: boolean; autoOpenPanel?: boolean; // 为 true 时:单击节点自动打开自定义 Panel(默认仅内置节点单击自动打开) // 复用宿主内置生成面板;与自定义 Panel 二选一(同时提供时优先 Panel) useBuiltinPanel?: CanvasBuiltinPanelConfig; diff --git a/plugins/canvas/svg/package.json b/plugins/canvas/svg/package.json index 8a520cf..5a432f5 100644 --- a/plugins/canvas/svg/package.json +++ b/plugins/canvas/svg/package.json @@ -1,6 +1,6 @@ { "name": "canvas-plugin-svg", - "version": "1.0.0", + "version": "1.1.0", "private": true, "type": "module", "description": "Infinite Canvas SVG 节点插件", diff --git a/plugins/canvas/svg/src/index.tsx b/plugins/canvas/svg/src/index.tsx index 7d12671..b2c5ead 100644 --- a/plugins/canvas/svg/src/index.tsx +++ b/plugins/canvas/svg/src/index.tsx @@ -1,34 +1,75 @@ -// SVG 节点:编辑与渲染 SVG,无自身内容时可取上游文本节点里的 SVG 源码。 -import { definePlugin, useEffect, useState } from "@infinite-canvas/plugin-sdk"; +// SVG 节点:编辑与渲染 SVG,透明背景直接融入画布;无自身内容时可取上游文本节点里的 SVG 源码。 +import { definePlugin, useEffect, useRef, useState } from "@infinite-canvas/plugin-sdk"; import type { CanvasNodeContentProps } from "@infinite-canvas/plugin-sdk"; -const DEFAULT_SVG = ''; - function SvgContent({ ctx }: CanvasNodeContentProps) { const [editing, setEditing] = useState(false); - const stored = ctx.node.metadata?.content; + const rootRef = useRef(null); + + const stored = ctx.node.metadata?.content as string | undefined; const upstream = ctx .getUpstream() .map((node) => node.metadata?.content) - .find((text) => text?.trim().startsWith(" typeof text === "string" && text.trim().startsWith(" { if (stored === undefined && upstream) ctx.updateMetadata({ content: upstream }); }, [ctx, stored, upstream]); + // 点击节点外部时退出编辑。用 pointerdown 的 capture 阶段:宿主画布在 pointerdown 上 + // preventDefault 会抑制 mousedown,capture 又能先于宿主 stopPropagation 触发。 + useEffect(() => { + if (!editing) return; + const onDocDown = (e: PointerEvent) => { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) setEditing(false); + }; + document.addEventListener("pointerdown", onDocDown, true); + return () => document.removeEventListener("pointerdown", onDocDown, true); + }, [editing]); + + // 交互控件上按下时阻止冒泡,避免误触发节点拖动/双击编辑 + const stop = (e: { stopPropagation: () => void }) => e.stopPropagation(); + const toggle = { position: "absolute", right: 8, top: 8, zIndex: 20, width: 32, height: 32, display: "grid", placeItems: "center", borderRadius: 8, border: `1px solid ${ctx.theme.node.stroke}`, background: `${ctx.theme.toolbar.panel}dd`, color: ctx.theme.node.text, cursor: "pointer" } as const; return ( - e.stopPropagation()} style={{ position: "relative", height: "100%", width: "100%", display: "flex", flexDirection: "column" }}> - setEditing((v) => !v)} title={editing ? "预览" : "编辑源码"}> + { + e.stopPropagation(); + setEditing(true); + }} + > + setEditing((v) => !v)} title={editing ? "预览" : "编辑源码"}> {editing ? "👁" : "✎"} {editing ? ( - ctx.updateMetadata({ content: e.target.value })} onWheel={(e) => e.stopPropagation()} style={{ height: "100%", width: "100%", resize: "none", background: "transparent", padding: 16, fontFamily: "monospace", fontSize: 12, outline: "none", border: "none", color: ctx.theme.node.text }} /> + ctx.updateMetadata({ content: e.target.value })} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.stopPropagation(); + setEditing(false); + } + }} + onMouseDown={stop} + onPointerDown={stop} + onWheel={stop} + style={{ height: "100%", width: "100%", resize: "none", background: ctx.theme.node.fill, borderRadius: 16, padding: 16, boxSizing: "border-box", fontFamily: "monospace", fontSize: 12, outline: "none", border: `1px solid ${ctx.theme.node.stroke}`, color: ctx.theme.node.text }} + /> + ) : svg ? ( + // pointerEvents:none 让整块可拖动移动,双击/拖拽都命中外层节点 + ) : ( - + 双击编辑,粘贴 SVG 源码 )} ); @@ -37,8 +78,8 @@ function SvgContent({ ctx }: CanvasNodeContentProps) { export default definePlugin({ id: "svg", name: "SVG 节点", - version: "1.0.0", - description: "编辑与渲染 SVG,可接收上游文本节点的 SVG 源码", + version: "1.1.0", + description: "透明背景渲染 SVG 矢量图,可接收上游文本节点的 SVG 源码", nodes: [ { type: "svg:vector", @@ -48,6 +89,8 @@ export default definePlugin({ defaultSize: { width: 320, height: 320 }, defaultMetadata: {}, minimapColor: "#14b8a6", + // 背景/边框透明,矢量图直接融入画布 + transparentBackground: true, Content: SvgContent, }, ], diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index a0d5164..76f4d07 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -135,6 +135,8 @@ export const CanvasNode = React.memo(function CanvasNode({ const forceInteractive = supportsInteractionToggle ? Boolean(definition?.forceInteractive?.(data)) : false; const contentInteractive = !supportsInteractionToggle || forceInteractive || !data.metadata?.content ? true : Boolean(data.metadata?.interactive); const isBatchChild = data.type === CanvasNodeType.Image && Boolean(data.metadata?.batchRootId); + // 透明背景节点(如 SVG):卡片背景/边框透明,直接融入画布;选中/关联态仍显示描边以便定位 + const transparentBg = Boolean(definition?.transparentBackground); const isActive = isConnectionTarget || isSelected || isFocusRelated; const imageBorderColor = isActive ? selectionBlue : isRelated && !isBatchChild ? theme.node.muted : "transparent"; const textareaRef = useRef(null); @@ -348,8 +350,8 @@ export const CanvasNode = React.memo(function CanvasNode({