JavaScript is required

手绘线条生成图谱节点

这个示例在 relation-graph 上叠加手绘画布,并将每条完成的笔画转换为新的可编辑图节点。它演示了与缩放联动的笔画归一化、用于存储 SVG 路径数据的自定义节点渲染,以及插入后的基于选中态缩放与删除流程。

将徒手笔画转换为可编辑的图节点

这个示例构建了什么

这个示例构建了一个小型图编辑器,允许用户直接在图画布上绘制,并将每一条完成的笔画转换成一个新节点。页面从一个固定位置的初始图开始,然后叠加一个全屏绘图层、一个悬浮工具窗口、用于已选节点的尺寸调整手柄,以及一个用于删除单个已选节点的一键操作。

可见结果并不是一个临时批注层。每一条完成的笔画都会变成一个真实的图节点,拥有自己的位置、宽度、高度、已存储的 SVG 路径数据、描边颜色和描边宽度。插入后,这个绘制出来的节点可以进入与初始节点相同的选择流程,因此这里最重要的要点是:将白板式输入转换为普通的 relation-graph 内容。

数据如何组织

初始图是一个本地 RGJsonData 对象,包含三个节点和两条连线。由于该图使用固定布局,每个初始节点在 setJsonData(...) 运行之前就已经包含了明确的 xy 坐标。初始数据集没有外部获取过程,也没有复杂的预处理。

运行时生成的节点采用不同的结构。当绘图叠加层完成一条笔画时,MyGraph 会把发出的 M/L 路径字符串解析为点,计算一个带内边距的包围盒,将包围盒原点从视口坐标转换为图画布坐标,再把路径重写为节点局部坐标。插入的节点会把这种归一化后的几何信息存入 node.data.path,同时保存 strokeColorstrokeWidth,而节点本身则保留图级别的位置和尺寸。

在真实产品中,这种模式同样可以表示手写便签、粗略的图形草图、操作员标记、地图涂画,或需要保持为可编辑图实体而不是扁平图片像素的触笔轨迹。

relation-graph 如何使用

该图使用 layoutName: 'fixed',这与初始数据中显式提供的坐标相匹配,也让运行时插入行为更可预测。选项还定义了默认的矩形节点形状、基础节点和连线尺寸,以及 dragEventAction: 'selection',因此画布启动时处于面向选择的编辑模式,而不是只读视口模式。

RGProvider 为整个示例中使用的 hooks 提供上下文。RGHooks.useGraphInstance() 是主要控制面:它加载初始 JSON、将图居中、通过 getCanvasXyByViewXy(...) 转换笔画坐标、从 getOptions() 读取缩放、通过 addNodes(...) 插入新节点、清理编辑状态、删除节点、更新运行时选项,并支持共享悬浮窗口中的图像导出准备与恢复。RGHooks.useEditingNodes() 驱动自定义工具栏,使其只在恰好选中一个节点时出现;而 DraggableWindow 中的 RGHooks.useGraphStore() 则在面板 UI 中反映当前的拖拽和滚轮设置。

两个插槽承载了大部分自定义渲染与编辑行为。RGSlotOnView 直接在图表面上挂载 RGEditingNodeControllerRGEditingResizeMyNodeToolbarRGEditingConnectController,因此选择和编辑都保持在画布上完成。RGSlotOnNode 则为 svg-path 节点覆盖默认节点渲染,将保存下来的路径数据绘制为内联 SVG,而普通节点仍然回退为文本渲染。

本地样式文件在这个示例中存在,但基本只是一个占位符。大部分可见的自定义都来自 SmoothCanvas.tsxMyGraph.tsxMyNodeToolbar.tsx 中的内联样式与工具类,而不是更深层的样式表覆盖。

关键交互

  • DraggableWindow 中的一个悬浮开关通过挂载或卸载 SmoothCanvas 叠加层来开启或关闭自由绘制模式。
  • 叠加层同时接受鼠标和触摸输入,实时预览笔画,并在绘制结束时发出最终的 SVG 路径字符串。
  • 底部工具栏会在笔画提交前修改颜色和线宽,这些值会持久化到生成的图节点上。
  • 点击一个节点会让它进入编辑模式,从而显示尺寸调整手柄和自定义删除工具栏。
  • 点击空白画布会清空编辑节点、清空当前活动的编辑连线,并移除选中高亮。
  • 共享设置面板可以在运行时切换滚轮和拖拽行为,并将当前图导出为图像。

关键代码片段

这一段展示了该示例如何在本地 React state 中,将固定布局图选项与自由绘制模式开关结合起来。

const graphInstance = RGHooks.useGraphInstance();
const [freelyDrawMode, setFreelyDrawMode] = useState(true);

const graphOptions: RGOptions = {
    debug: false,
    defaultNodeShape: RGNodeShape.rect,
    defaultNodeWidth: 100,
    defaultNodeHeight: 40,
    defaultLineWidth: 2,
    layout: {
        layoutName: 'fixed',
    },
    dragEventAction: 'selection',
    disableDragNode: false,
};

这一段是将笔画转换为节点的核心步骤:它把视口坐标转换成图坐标,并将笔画重写为节点局部 SVG 数据。

const canvasPos = graphInstance.getCanvasXyByViewXy({ x: minX, y: minY });

const zoom = graphInstance.getOptions().canvasZoom / 100;
const nodeW = Math.max(viewWidth / zoom, 20);
const nodeH = Math.max(viewHeight / zoom, 20);

const relativePath = coords.map((p, i) => {
    const relX = (p.x - minX) / zoom;
    const relY = (p.y - minY) / zoom;
    return `${i === 0 ? 'M' : 'L'}${relX.toFixed(1)} ${relY.toFixed(1)}`;
}).join(' ');

这一段展示了新创建的节点如何把草绘作为图数据保存下来,而不是把它压平成一张截图。

const newNode = {
    id: newNodeId,
    type: 'svg-path',
    x: canvasPos.x,
    y: canvasPos.y,
    width: nodeW,
    height: nodeH,
    text: '',
    color: 'transparent',
    borderColor: 'transparent',
    data: {
        path: relativePath,
        originalViewBox: `0 0 ${nodeW} ${nodeH}`,
        strokeColor: color,
        strokeWidth: width
    }
};

graphInstance.addNodes([newNode]);

这一段展示了自定义节点插槽如何将已保存的笔画几何数据作为内联 SVG 渲染给 svg-path 节点。

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => {
        if (node.type === 'svg-path') {
            return (
                <div style={{ width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none' }}>
                    <svg width="100%" height="100%" style={{ display: 'block' }}>
                        <path
                            d={node.data?.path}
                            fill="none"
                            stroke={node.data?.strokeColor || '#FF0000'}
                            strokeWidth={node.data?.strokeWidth || 4}
                        />
                    </svg>
                </div>
            );
        }

这一段展示了该叠加层是一个真正的输入表面,支持实时绘制,并在底部提供用于设置笔画样式的工具栏。

<canvas
    ref={canvasRef}
    onMouseDown={startDrawing}
    onMouseMove={draw}
    onMouseUp={stopDrawing}
    onMouseLeave={stopDrawing}
    onTouchStart={startDrawing}
    onTouchMove={draw}
    onTouchEnd={stopDrawing}
    style={{
        position: 'absolute',
        top: 0,
        left: 0,
        zIndex: 2,
        touchAction: 'none',
        cursor: 'crosshair',
        background: 'transparent',
        pointerEvents: 'auto'
    }}
/>

这个示例的独特之处

它的稀有点并不只是“在画布上绘图”。对比数据表明,这个示例的特别之处在于:它在 relation-graph 之上放置了一个全画布徒手绘制层,同时接受鼠标和触摸输入,允许用户选择描边颜色和宽度,然后再把完成的笔迹转换为一个普通图节点。

canvas-selection 相比,这个示例会把实际的草绘几何形状和描边样式保存在节点数据中,而不是将手势简化成内置矩形或圆形。与 gee-node-resize 这类以尺寸调整为重点的示例相比,这里的主要经验并不只是尺寸调整控制器本身,而是绘制、插入、选择、调整尺寸和删除这一整套工作流。与 line-vertex-on-nodechange-line-path 这类连接或连线路径编辑示例相比,这里的重点是基于草绘的节点创作,而不是边的创建或路径编辑。

最强的可复用组合是:自由绘制叠加层、具备缩放感知的笔画归一化、用于渲染已存储 SVG 路径的 RGSlotOnNode、基于选择状态的编辑叠加层,以及挂载在选择态上的删除操作入口,并且这些都集中在同一个紧凑的界面里。现有对比数据支持将这一组合描述为稀有;但并不支持声称这是唯一一个带有手势驱动节点创建的示例。

这种模式还适用于哪里

这种模式很适合那些用户先草绘、后组织的产品。示例包括:把笔触提升为持久图元素的白板式头脑风暴工具、允许现场服务或运维人员绘制粗略路径并将其保留为结构化节点的工具、手绘图形后再进入常规编辑流程的轻量流程映射工具,以及把触笔输入转换为图对象而不是静态图像的教育或批注工具。

同样的方法还可以扩展到持久化和更丰富的编辑能力。生产版本可以把生成的 svg-path 节点数据保存到后端,在插入后附加标签,按形状类型对笔画进行分类,或者增加一个用于重塑现有路径的二阶段编辑器,同时保持 relation-graph 的选择模型不变。