节点拖拽碰撞与投放规则
这个示例展示一个紧凑拖拽规则工作区:节点重叠可被解释为允许投放、拒绝投放或受保护碰撞。它结合固定布局图数据、拖拽生命周期回调、节点内联策略开关,以及释放时创建连线,演示了无需完整编辑器壳层的碰撞感知创作流程。
具备碰撞感知的节点拖拽与放置规则
这个示例构建了什么
这个示例构建了一个紧凑的关系工作区,在这里,把一个节点拖到另一个节点上会成为一种由规则驱动的编排手势。界面初始是一个固定位置布局的图,包含矩形和圆形两类节点,背景是点状画布,同时还有一个浮动帮助窗口和一个固定图例,用于说明拖拽状态。
用户可以拖拽节点,观察目标节点在绿色、红色和紫色反馈之间切换,在每个目标节点的卡片内修改其放置策略,并在允许的目标上释放以创建一条新连线。重点不在于自由式布局编辑,而在于展示 relation-graph 如何将重叠检测转化为拖拽过程中的接受、拒绝或受限移动。
数据是如何组织的
这个图以内联方式声明为一个 RGJsonData 对象,包含 rootId、扁平的 nodes 数组以及扁平的 lines 数组。由于该示例使用固定布局,每个节点都带有固定的 x 和 y 坐标,另外有些节点还会在 node.data 中携带运行时策略标记,例如 allowDrop 和 disallowDroppingNodesInside。
在调用 setJsonData(...) 之前没有做任何预处理。代码会直接加载这份数据集,然后让视口居中并适配显示。在交互过程中,示例还会给 node.data 增加 startX、startY 以及已保存颜色等临时字段,因此拖拽规则是直接挂在节点自身上的,而不是保存在单独的编辑器 store 中。
在真实应用里,同样的数据结构可以表示角色与权限、工作流步骤、存储区域,或者只能附着到已批准父节点上的资产。本地的重叠检测工具通过矩形相交来完成命中测试,因此即使渲染出的图混合了不同节点形状,判定逻辑依然保持简单。
relation-graph 的使用方式
index.tsx 用 RGProvider 包裹整个示例,而 MyGraph 通过 RGHooks.useGraphInstance() 读取当前实例。这个实例通过 setJsonData(...)、moveToCenter() 和 zoomToFit() 完成初始化,然后再借助 getNodes()、updateNodeData()、updateNode()、updateNodePosition() 和 addLines() 驱动整个拖拽工作流。
RelationGraph 被配置为 layout.layoutName = "fixed"、defaultJunctionPoint = RGJunctionPoint.border,并使用白色节点填充、琥珀色边框与连线,以及位于右侧的工具栏位置。重要的运行时行为并不来自自定义布局引擎,而是来自三个节点拖拽回调:onNodeDragStart 记录原始位置,onNodeDragging 检查重叠并可返回一个受限坐标,而 onNodeDragEnd 则决定是否创建一条关系线。
这个示例还使用了两个插槽。RGSlotOnNode 用一个卡片替换默认节点主体,卡片中包含两个实时生效的 SimpleUIBoolean 控件;RGSlotOnView 则在画布中放置一个固定图例,让三种拖拽结果可以直接在工作区内看到。
一个共享的 DraggableWindow 提供了附加工具。它的设置面板通过 RGHooks.useGraphStore() 读取数据,使用 setOptions(...) 更新 wheelEventAction 和 dragEventAction,并通过 prepareForImageGeneration() 和 restoreAfterImageGeneration() 导出图像。配套的 SCSS 则用可随缩放变化的点状图案覆盖画布背景,并定义图例面板的样式。
关键交互
- 拖拽节点时,会持续检查它与图中其他所有节点的重叠情况。
- 当与带有
allowDrop的目标发生重叠时,该目标会变为绿色;若目标没有allowDrop,则会变为红色。 - 当与带有
disallowDroppingNodesInside的目标发生重叠时,该目标会变为紫色,并返回一个受限位置,使被拖拽节点保持在受保护节点之外。 - 拖拽开始时,会在
node.data中记录原始坐标;拖拽结束时,会先恢复这些坐标,然后再创建一条新的关系线。 Allow Drop和Disallow Dropping Nodes Inside复选框会立即更新当前节点的策略,因此下一次拖拽会直接使用新规则,而不需要重建数据集。- 浮动帮助窗口可以被拖动、最小化、切换到设置面板,并用于导出当前图像。
关键代码片段
这个片段展示了该示例如何以内联方式初始化一个固定位置的图,并在加载后立即执行居中和适配显示。
const myJsonData: RGJsonData = {
rootId: 'SYS_ROLE',
nodes: [
{ id: 'SYS_USER', text: 'SYS_USER', nodeShape: RGNodeShape.rect, x: -32, y: -427, data: { allowDrop: true } },
{ id: 'SYS_DEPT', text: 'SYS_DEPT', nodeShape: RGNodeShape.circle, x: -244, y: -283, data: { allowDrop: true } },
{ id: 'SYS_ROLE', text: 'SYS_ROLE', nodeShape: RGNodeShape.rect, x: 0, y: 0, width: 300, height: 200, data: { disallowDroppingNodesInside: true } }
]
};
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
这个片段展示了拖拽期间的规则引擎如何为目标重新着色,并为受保护重叠计算一个受限位置。
const overlap = MyRelationGraphUtils.shapesOverlap(nodeBox, n, shapeA, shapeB);
if (overlap) {
let newColor = n.data!.allowDrop ? 'rgba(212,252,189,0.82)' : 'rgba(252,189,189,0.82)';
if (n.data.disallowDroppingNodesInside) {
rejectOverlaped = true;
limitedPosition = MyRelationGraphUtils.getNoOverlapLimitedPosition(node, newX, newY, n);
newColor = 'rgba(214,165,246,0.82)';
}
graphInstance.updateNode(n, { color: newColor });
}
这个片段展示了一个被接受的放置操作会先恢复节点位置,然后再新增一条关系,而不是把该节点嵌套到目标节点内部。
const updateNodeAsChildren = (node: RGNode, parentNode: RGNode) => {
graphInstance.updateNodePosition(node, node.data!.startX, node.data!.startY);
graphInstance.addLines([{
id: `new_l_${Date.now()}`,
from: parentNode.id,
to: node.id,
text: 'New Relationship',
color: 'rgba(214,165,246,0.82)',
lineWidth: 3
}]);
};
这个片段展示了 RGSlotOnNode 如何把每个节点都变成一个实时策略编辑器。
<SimpleUIBoolean
label="Disallow Dropping Nodes Inside"
currentValue={node.data.disallowDroppingNodesInside}
onChange={(newValue) => {graphInstance.updateNodeData(node, {disallowDroppingNodesInside: newValue})}}
/>
<SimpleUIBoolean
label="Allow Drop"
currentValue={node.data.allowDrop}
onChange={(newValue) => {graphInstance.updateNodeData(node, {allowDrop: newValue})}}
/>
这个片段展示了共享的浮动面板如何读取实时图 store,并把运行时画布模式变更回推到图实例中。
const { options } = RGHooks.useGraphStore();
const dragMode = options.dragEventAction;
const wheelMode = options.wheelEventAction;
<SettingRow
label="Wheel Event:"
value={wheelMode}
onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>
这个片段展示了样式表如何把图区域变成一个点状编辑表面。
.relation-graph {
--rg-canvas-scale: 1;
--rg-canvas-offset-x: 0px;
--rg-canvas-offset-y: 0px;
background-position: var(--rg-canvas-offset-x) var(--rg-canvas-offset-y);
background-size: calc(var(--rg-canvas-scale) * 15px) calc(var(--rg-canvas-scale) * 15px);
background-image: radial-gradient(circle, rgb(197, 197, 197) calc(var(--rg-canvas-scale) * 1px), transparent 0);
}
这个示例的独特之处
这个示例在附近几个编辑器演示中之所以突出,是因为节点拖拽生命周期本身就是它要讲解的核心。与 create-line-from-node 相比,这里的关系创建不是由选择触发,而是由重叠触发:用户把一个节点拖到当前允许放置的目标上,示例才会新增一条连线。
与 custom-node-quick-actions 和 batch-operations-on-nodes 相比,这里的自定义节点 UI 不是一个用于选择或批量工具的动作启动器。每个节点都携带自己的规则开关,因此图内容本身直接决定一次碰撞应被接受、拒绝,还是视为受保护重叠。
与 gee-node-resize 和 gee-node-alignment-guides 相比,这里的额外拖拽反馈强调的是语义,而不是几何。这里少见的组合是固定布局场景、内联策略卡片、视图级图例、手写重叠工具,以及在被拖拽节点回弹到原始位置之后再于释放时创建连线。这使它成为基于规则的放置行为的良好起点,而不是通用编辑器外壳的起点。
这种模式还适用于哪里
这种模式非常适合工作流或审批设计器,在这类场景中,把一个步骤拖到另一个步骤上时,只有目标允许时才应该提出一条有效迁移。
它也适用于访问控制映射、数据血缘梳理以及依赖关系编排工具,在这些工具中,对象可以连接到某些类别的节点,但必须避开受保护的容器或区域。
另一个相关用法是运维规划界面,例如存储布局或设备地图,在这些场景里,重叠可以作为一种快速分配手势,而受保护区域会拒绝进入,只记录这层关系。