框选创建或选择节点
这个示例展示同一次框选手势可根据当前模式实现“创建圆形/矩形节点”或“选择已有节点”。它结合 `onCanvasSelectionEnd` 与键盘/面板模式启用、实时选区 HUD 反馈、虚线预览覆盖层,以及共享悬浮设置面板。
通过拖拽框选来创建或选择节点
这个示例构建了什么
这个示例构建了一个全高度的 relation-graph 工作区,初始放置了 14 个彼此独立的字母节点,并叠加了两个编辑控件:一个可拖拽的辅助窗口和一个位于顶部中间的紧凑形状面板。用户无需离开画布,就可以查看当前选择框、切换画布行为并导出图片。
这个示例的核心结果是一个双用途的拖拽手势。当未启用创建模式时,拖出选择框会选中区域内已有的节点。当启用了圆形或矩形模式时,同样的手势会变成一次性节点创建操作,拖拽过程中会显示虚线预览覆盖层,并在手势完成时弹出提示消息。
数据是如何组织的
初始图数据在 initializeGraph() 中以内联方式组装为一个本地 RGJsonData 对象。它包含一个扁平的 nodes 数组,节点 id 从 a 到 n,以及一个空的 lines 数组,因此这个示例从一个中性的画布开始,而不是从某个特定领域的图开始。
在调用 setJsonData(...) 之前,没有任何 fetch 步骤,也没有任何预处理,只有在内存中构造这个对象。数据加载完成后,示例会通过 moveToCenter() 和 zoomToFit() 将视口居中并适配到合适大小。
在交互过程中,选择框会成为临时输入数据。在创建模式下,代码会把选择视图重新转换为画布坐标,并使用选择框的宽高来决定新节点的尺寸。在普通模式下,同一个选择视图会被传给 getNodesInSelectionView(...),返回的每个节点都会被更新为 selected: true。
在真实产品中,这种数据结构同样可以表示白板上的便签、拓扑草图中的设备、平面图编辑器中的房间,或标注画布上的通用资源。
relation-graph 是如何使用的
这个 demo 由 RGProvider 包裹,MyGraph 依赖 RGHooks.useGraphInstance() 来完成初始数据加载、选择处理、节点插入以及运行时选项更新。它还使用 RGHooks.useSelection(),使当前选择框既能驱动浮动读数,也能驱动虚线预览层。在共享设置面板中,RGHooks.useGraphStore() 用来读取当前启用的拖拽模式和滚轮模式。
图配置保持精简但有明确目的:showToolBar 保持内置工具栏可见,checkedItemBackgroundColor 为已勾选项提供半透明绿色高亮,defaultLineWidth 保持为 1,defaultJunctionPoint 则设置为 RGJunctionPoint.border。这个示例没有定义自定义布局;它加载的是简单的离散数据,并在挂载后规范化视口。
RelationGraph 是事件枢纽。onCanvasClick 会清除 checked 和 selected 状态,onCanvasSelectionEnd 会在节点创建和节点选择之间分支处理,onKeyboardUp 则会为 Q 和 W 启用临时创建模式。RGSlotOnNode 用一个居中文本容器替换了默认节点主体。MyCreatingShapeLayer 添加了一个带有 pointerEvents: 'none' 的 SVG 覆盖层,因此预览图形不会阻挡画布交互。
外围编辑器外壳由本地辅助组件构建,而不是通过额外的 graph slot 实现。DraggableWindow 提供了浮动说明面板,并且可以打开一个共享设置视图,用于修改 wheelEventAction、修改 dragEventAction,以及通过 prepareForImageGeneration()、domToImageByModernScreenshot(...) 和 restoreAfterImageGeneration() 导出当前图像。
关键交互
- 点击顶部中间面板中的圆形或矩形按钮,会启用一个临时创建模式,并将画布拖拽行为切换为
selection。 RelationGraph上的onKeyboardUp也会通过KeyQ和KeyW启用创建模式,因此当图接收到键盘事件时,用户可以通过快捷键走同一套流程。- 当创建模式处于激活状态时,拖出选择框会显示虚线预览覆盖层,并在拖拽结束时创建一个新的圆形或矩形节点。
- 当未启用任何创建模式时,拖出同样的选择框会解析出被包围的节点,显示数量提示,并将这些节点标记为已选中。
- 点击画布空白区域会清除 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-actions 和 create-line-from-node,这里真正的编辑工具就是选择框本身。那些示例使用选择操作来决定哪些现有节点显示上下文控件,而这个示例则直接把拖出的区域作为创建几何。
稀有度数据同样表明,这个示例组合了一组在整个示例集中并不常见的能力:RGHooks.useSelection()、虚线形状预览覆盖层、键盘与面板模式启用、在选择结束时创建节点、在选择结束时选择节点,以及在同一个轻量工作区中提供共享设置或导出面板。
这种模式还能应用到哪里
这种模式非常适合轻量级图表编辑器,用户需要在选择和有边界形状创建之间快速切换,而不必停留在永久工具模式中。它适用于白板、流程草绘工具、拓扑草图以及平面图工具,在这些场景中,拖出的区域既应定义位置,也应定义尺寸。
它也适用于以标注为主的产品。评审工具、地图标记工具或设计反馈界面都可以复用这套流程,用一次拖拽手势来创建标注区域、占位区域、分组容器,或特定领域中的有边界对象。