JavaScript is required

框选创建或选择节点

这个示例展示同一次框选手势可根据当前模式实现“创建圆形/矩形节点”或“选择已有节点”。它结合 `onCanvasSelectionEnd` 与键盘/面板模式启用、实时选区 HUD 反馈、虚线预览覆盖层,以及共享悬浮设置面板。

通过拖拽框选来创建或选择节点

这个示例构建了什么

这个示例构建了一个全高度的 relation-graph 工作区,初始放置了 14 个彼此独立的字母节点,并叠加了两个编辑控件:一个可拖拽的辅助窗口和一个位于顶部中间的紧凑形状面板。用户无需离开画布,就可以查看当前选择框、切换画布行为并导出图片。

这个示例的核心结果是一个双用途的拖拽手势。当未启用创建模式时,拖出选择框会选中区域内已有的节点。当启用了圆形或矩形模式时,同样的手势会变成一次性节点创建操作,拖拽过程中会显示虚线预览覆盖层,并在手势完成时弹出提示消息。

数据是如何组织的

初始图数据在 initializeGraph() 中以内联方式组装为一个本地 RGJsonData 对象。它包含一个扁平的 nodes 数组,节点 id 从 an,以及一个空的 lines 数组,因此这个示例从一个中性的画布开始,而不是从某个特定领域的图开始。

在调用 setJsonData(...) 之前,没有任何 fetch 步骤,也没有任何预处理,只有在内存中构造这个对象。数据加载完成后,示例会通过 moveToCenter()zoomToFit() 将视口居中并适配到合适大小。

在交互过程中,选择框会成为临时输入数据。在创建模式下,代码会把选择视图重新转换为画布坐标,并使用选择框的宽高来决定新节点的尺寸。在普通模式下,同一个选择视图会被传给 getNodesInSelectionView(...),返回的每个节点都会被更新为 selected: true

在真实产品中,这种数据结构同样可以表示白板上的便签、拓扑草图中的设备、平面图编辑器中的房间,或标注画布上的通用资源。

relation-graph 是如何使用的

这个 demo 由 RGProvider 包裹,MyGraph 依赖 RGHooks.useGraphInstance() 来完成初始数据加载、选择处理、节点插入以及运行时选项更新。它还使用 RGHooks.useSelection(),使当前选择框既能驱动浮动读数,也能驱动虚线预览层。在共享设置面板中,RGHooks.useGraphStore() 用来读取当前启用的拖拽模式和滚轮模式。

图配置保持精简但有明确目的:showToolBar 保持内置工具栏可见,checkedItemBackgroundColor 为已勾选项提供半透明绿色高亮,defaultLineWidth 保持为 1defaultJunctionPoint 则设置为 RGJunctionPoint.border。这个示例没有定义自定义布局;它加载的是简单的离散数据,并在挂载后规范化视口。

RelationGraph 是事件枢纽。onCanvasClick 会清除 checked 和 selected 状态,onCanvasSelectionEnd 会在节点创建和节点选择之间分支处理,onKeyboardUp 则会为 QW 启用临时创建模式。RGSlotOnNode 用一个居中文本容器替换了默认节点主体。MyCreatingShapeLayer 添加了一个带有 pointerEvents: 'none' 的 SVG 覆盖层,因此预览图形不会阻挡画布交互。

外围编辑器外壳由本地辅助组件构建,而不是通过额外的 graph slot 实现。DraggableWindow 提供了浮动说明面板,并且可以打开一个共享设置视图,用于修改 wheelEventAction、修改 dragEventAction,以及通过 prepareForImageGeneration()domToImageByModernScreenshot(...)restoreAfterImageGeneration() 导出当前图像。

关键交互

  • 点击顶部中间面板中的圆形或矩形按钮,会启用一个临时创建模式,并将画布拖拽行为切换为 selection
  • RelationGraph 上的 onKeyboardUp 也会通过 KeyQKeyW 启用创建模式,因此当图接收到键盘事件时,用户可以通过快捷键走同一套流程。
  • 当创建模式处于激活状态时,拖出选择框会显示虚线预览覆盖层,并在拖拽结束时创建一个新的圆形或矩形节点。
  • 当未启用任何创建模式时,拖出同样的选择框会解析出被包围的节点,显示数量提示,并将这些节点标记为已选中。
  • 点击画布空白区域会清除 checked 和 selected 状态。
  • 打开可拖拽辅助窗口中的设置面板后,用户可以修改滚轮行为、修改画布拖拽行为,并下载当前图的图片。

节点点击和连线点击处理器是存在的,但在这个示例里,它们只会记录被点击的对象,不会改变可见行为。

关键代码片段

该组件通过 hook 和 React state 保存图实例、实时选择框以及当前创建模式。

const graphInstance = RGHooks.useGraphInstance();
const selectionView = RGHooks.useSelection();
const [selectionForCreateNode, setSelectionForCreateNode] = React.useState<'' | 'circle' | 'rect'>('');

const graphOptions: RGOptions = {
    debug: false,
    showToolBar: true,
    checkedItemBackgroundColor: 'rgba(0, 128, 0, 0.2)',
    defaultLineWidth: 1,
    defaultJunctionPoint: RGJunctionPoint.border
};

这个示例有意从一组较小的内联数据集开始,并在加载后立即规范化视口。

const myJsonData: RGJsonData = {
    nodes: [
        {id: 'a', text: 'A'},
        {id: 'b', text: 'B'},
        {id: 'c', text: 'C'},
        // ...
    ],
    lines: []
};

await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();

当启用了创建模式时,onCanvasSelectionEnd 会把拖出的区域转换成一个新节点。

if (selectionForCreateNode === 'circle') {
    const xyOnCanvas = graphInstance.getCanvasXyByViewXy(userSelectionView);
    graphInstance.addNode({
        id: graphInstance.generateNewNodeId(),
        nodeShape: RGNodeShape.circle,
        width: userSelectionView.width,
        height: userSelectionView.height,
        x: xyOnCanvas.x,
        y: xyOnCanvas.y
    });
}

非创建分支会把同一个选择框作为选择查询来使用,然后把画布恢复为普通拖拽状态。

graphInstance.clearChecked();
graphInstance.clearSelected();
const nodesInSelection = graphInstance.getNodesInSelectionView(userSelectionView);
SimpleGlobalMessage.success(`Select [${nodesInSelection.length}] Nodes`);
nodesInSelection.forEach(node => {
    graphInstance.updateNode(node, {selected: true})
});

graphInstance.updateOptions({
    dragEventAction: 'move'
});
setSelectionForCreateNode('');

一个小型辅助函数会让创建模式保持为瞬时状态,只有当用户显式启用某个形状时,才将画布切换到选择模式。

const onKeyboardUp = (e: KeyboardEvent) => {
    if (e.code === 'KeyQ') {
        setSelectionForCreateNodeMode('circle');
    } else if (e.code === 'KeyW') {
        setSelectionForCreateNodeMode('rect');
    }
};

const setSelectionForCreateNodeMode = (nodeShape: 'circle' | 'rect') => {
    graphInstance.updateOptions({ dragEventAction: 'selection' });
    setSelectionForCreateNode(nodeShape);
};

预览覆盖层会为渲染统一负方向拖拽的情况,并保持对指针事件透明。

const rectX = width > 0 ? x : x + width;
const rectY = height > 0 ? y : y + height;
const absWidth = Math.abs(width);
const absHeight = Math.abs(height);

<svg
    style={{
        position: 'absolute',
        width: '100%',
        height: '100%',
        pointerEvents: 'none',
    }}
>

共享的浮动设置面板复用了 relation-graph API 来暴露运行时画布控制和图像导出能力。

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();

这个示例的独特之处

相较于 canvas-selection,这个示例更强调瞬时编辑,而不是持久的模式切换。比较数据表明,canvas-selection-pro 增加了 onKeyboardUp 快捷键和一个专门的顶部中间面板,并在每次手势完成后清除当前激活的形状,同时将 dragEventAction 恢复为 move。这让创建行为更像一次性命令,而不是一种持续粘滞的画布状态。

相较于 selections,这里的拖拽框不仅仅是一个选择工具。该示例会把选择几何重新转换为画布坐标,并用它来决定新节点的尺寸和位置,因此同一个手势可以根据当前启用的模式,选择已有内容或创建新内容。

相较于 custom-node-quick-actionscreate-line-from-node,这里真正的编辑工具就是选择框本身。那些示例使用选择操作来决定哪些现有节点显示上下文控件,而这个示例则直接把拖出的区域作为创建几何。

稀有度数据同样表明,这个示例组合了一组在整个示例集中并不常见的能力:RGHooks.useSelection()、虚线形状预览覆盖层、键盘与面板模式启用、在选择结束时创建节点、在选择结束时选择节点,以及在同一个轻量工作区中提供共享设置或导出面板。

这种模式还能应用到哪里

这种模式非常适合轻量级图表编辑器,用户需要在选择和有边界形状创建之间快速切换,而不必停留在永久工具模式中。它适用于白板、流程草绘工具、拓扑草图以及平面图工具,在这些场景中,拖出的区域既应定义位置,也应定义尺寸。

它也适用于以标注为主的产品。评审工具、地图标记工具或设计反馈界面都可以复用这套流程,用一次拖拽手势来创建标注区域、占位区域、分组容器,或特定领域中的有边界对象。