diff --git a/src/components/canvas/canvas-icon.tsx b/src/components/canvas/canvas-icon.tsx index ea58c69..c334c0b 100644 --- a/src/components/canvas/canvas-icon.tsx +++ b/src/components/canvas/canvas-icon.tsx @@ -19,6 +19,8 @@ export function CanvasIcon({ name = "Star", size = 24, width = size, height = si useEffect(() => { const controller = new AbortController(); setResult({ name, paths: peekIcon(name) }); + // 清理通过订阅 signal 取消,但 then 没有额外 active/代次检查;已兑现 Promise 的回调仍可能运行。 + // result.name 只能隔离不同名称,同名重试之间不靠此字段区分代次。 loadIcon(name, controller.signal).then((paths) => setResult({ name, paths }), (error) => { if (error.name !== "AbortError") setResult({ name, error: true }); }); @@ -27,8 +29,10 @@ export function CanvasIcon({ name = "Star", size = 24, width = size, height = si // effect可能还没处理新name,故读取结果前再次比对名称;失败标记也必须属于当前图标。 const paths = result.name === name ? result.paths : undefined; const failed = result.name === name && result.error; + // retry 不清浏览器 HTTP 缓存;同名已有成功分片时,loadIcon 仍可能直接复用共享缓存。 // 以24×24为标准坐标;absoluteStrokeWidth在size为数值时按尺寸反向补偿描边宽度。 // 成功时保留调用方onClick;失败时点击改为停止冒泡并重试,避免同时触发外层节点点击。 + // props 在默认 SVG 属性之后展开,可覆盖 viewBox 等;data-icon-* 和最终 onClick 在其后固定。 // 加载中用虚线方框,失败用叉号与title,data-icon-state供样式或检查观察显示状态。 return ; loaded export function CanvasImage({ id, src, title, className }: { id: string; src?: string; title: string; className: string }) { // 本地源按元素id而非远端URL索引,同一资源在不同节点中可拥有各自的显示交接状态。 const previews = useContext(ImagePreviews), local = previews.sources.get(id); + // onLoad 只携带元素 id,不附带 src/资源代次;迟到加载能否清掉当前预览需由 Provider 的 loaded 实现判定。 const remote = {title} previews.loaded(id)} />; + // 没有 onError 状态或自动重试;底图失败时若上层仍保留 local,覆盖图可继续显示。 // 没有预览或预览已等于正式源时,只绘制一张图片,避免冗余叠层。 if (!local || local === src) return remote; // 正式图片继续在底层加载,本地预览绝对定位覆盖在上方;Provider清掉该id后自然移除覆盖。 diff --git a/src/components/canvas/canvas-renderers.tsx b/src/components/canvas/canvas-renderers.tsx index e667edd..56be7c8 100644 --- a/src/components/canvas/canvas-renderers.tsx +++ b/src/components/canvas/canvas-renderers.tsx @@ -21,4 +21,5 @@ const nodes={material:MaterialNode,plainText:PlainTextNode,plainImage:PlainImage /** 每项保留原始类型/结构描述,node按rendererId查表,connection绑定CanvasEdge。 * render与preview目前共用同一个组件;createReactRenderers负责生成供ReactFlow使用的注册结果。 * 本表没有自定义类型的通配组件,兼容占位由投影/渲染选择链路处理。 */ +// 模块级初始化一次,供多个宿主复用;preview 选择同一组件不代表自动只读,交互权限仍由外层上下文限制。 export const defaultCanvasRenderers=createReactRenderers(builtinRendererDefinitions.map(definition=>{const render=definition.structure==='node'?nodes[definition.rendererId as keyof typeof nodes]:CanvasEdge;return{...definition,render,preview:render} as ReactRendererDefinition;})); diff --git a/src/components/canvas/content-handles.tsx b/src/components/canvas/content-handles.tsx index b950fcd..455ebb3 100644 --- a/src/components/canvas/content-handles.tsx +++ b/src/components/canvas/content-handles.tsx @@ -7,5 +7,6 @@ import styles from "./canvas.module.css"; /** 四边均使用source Handle以配合画布连接模式;selected控制能否开始连线以及读屏可见性。 * 不在这里创建端点记录,也不判断跨分区连线的归属。 */ export function ContentHandles({ selected }: { selected: boolean }) { + // selected 只设置 isConnectableStart/aria-hidden;未显式设置结束端能力或 tabIndex,仍须遵守外层 ReactFlow 配置。 return <>{([["top", Position.Top], ["right", Position.Right], ["bottom", Position.Bottom], ["left", Position.Left]] as const).map(([id, position]) => )}; } diff --git a/src/components/canvas/material-node.tsx b/src/components/canvas/material-node.tsx index ac15077..9c1f045 100644 --- a/src/components/canvas/material-node.tsx +++ b/src/components/canvas/material-node.tsx @@ -31,12 +31,14 @@ export function MaterialNode(props: NodeProps) { const editing = useScopedNodeEditing(id,data.__headlessTarget); const { zoom } = useViewport(); // 以卡片宽度(默认260)×视口缩放×设备像素比估算显示成本;1024以内优先预览源。 + // 像素比在 render 时读取,不单独监听其变化;zoom 订阅或其它重渲染才重新计算此启发式。 const preview = (props.width || 260) * zoom * (typeof window === "undefined" ? 1 : window.devicePixelRatio || 1) <= 1024; // 仅记录连接点按下的屏幕坐标,用小于5px的位移区分点击与拖动;不把它写进文档或history。 const pointerStart = useRef({ x: 0, y: 0 }); const Icon = data.kind === "image" ? ImageIcon : data.kind === "script" ? FileText : Type; // 旧target/source Handle禁用新连接但保留定位;四向Handle使用方向作ID,选中时允许开始连线。 // 指针点击还检查branchable能力和位移;键盘Enter/空格使用Handle中心打开菜单,由动作层再处理合法性。 + // pointerStart 不按 pointerId 分组,属于点击位移辅助;键盘入口也不在这里检查 branchable,由动作实现兜底。 // 点击/按键停止冒泡,避免分支操作同时触发节点或画布交互;文本内容的编辑生命周期由子组件负责。 return ( diff --git a/src/components/canvas/plain-image-node.tsx b/src/components/canvas/plain-image-node.tsx index 5bfbe05..99cb084 100644 --- a/src/components/canvas/plain-image-node.tsx +++ b/src/components/canvas/plain-image-node.tsx @@ -14,6 +14,7 @@ export function PlainImageNode(props: NodeProps) { // 节点宽度未提供时按320估算,再乘zoom与设备像素比;不超过1024时请求预览源。 // 这是显示宽度启发式,不检查图片原始尺寸;无window时像素比按1处理。 const preview = (props.width || 320) * zoom * (typeof window === "undefined" ? 1 : window.devicePixelRatio || 1) <= 1024; + // 该估算只影响托管 URL 的 variant;不是独立下载任务,不会为普通外部图片生成预览资源。 // managedImageSource按资源格式选择可用地址,移动/缩放等编辑行为仍由TransformableNode处理。 return ; } diff --git a/src/components/canvas/unsupported-elements.tsx b/src/components/canvas/unsupported-elements.tsx index c0eb586..2c11899 100644 --- a/src/components/canvas/unsupported-elements.tsx +++ b/src/components/canvas/unsupported-elements.tsx @@ -17,6 +17,7 @@ const positions={top:Position.Top,right:Position.Right,bottom:Position.Bottom,le * 仍绘制已知连接点供现有连线定位,但禁止从这些Handle发起或接收新连接;不推断未知内容字段。 */ export function UnsupportedNode({data,selected}:NodeProps){ // fallback来自bindings投影;缺失时仍显示通用提示,不尝试从data猜测组件结构。 + // 只读取公共投影信封;此组件本身没有 TransformableNode,也不安装任何编辑动作处理器。 const fallback=data.__headlessFallback; return
{data.title}此元素需要兼容的组件版本才能编辑 @@ -27,6 +28,7 @@ export function UnsupportedNode({data,selected}:NodeProps){ /** 直接使用ReactFlow推导的端点生成贝塞尔路径,附可访问标题;不调用未知类型的编辑/样式逻辑。 * BaseEdge绘制统一颜色的可见线条,数据保留与版本兼容仍归模型层负责。 */ export function UnsupportedEdge(props:EdgeProps){ + // 采用端点派生的通用曲线,不重放未知模型的自定义 path/marker;保留数据与视觉完全还原是两件事。 const [path]=getBezierPath(props); return 原始连线内容已保留; }