JavaScript is required

节点拖拽碰撞与投放规则

这个示例展示一个紧凑拖拽规则工作区:节点重叠可被解释为允许投放、拒绝投放或受保护碰撞。它结合固定布局图数据、拖拽生命周期回调、节点内联策略开关,以及释放时创建连线,演示了无需完整编辑器壳层的碰撞感知创作流程。

具备碰撞感知的节点拖拽与放置规则

这个示例构建了什么

这个示例构建了一个紧凑的关系工作区,在这里,把一个节点拖到另一个节点上会成为一种由规则驱动的编排手势。界面初始是一个固定位置布局的图,包含矩形和圆形两类节点,背景是点状画布,同时还有一个浮动帮助窗口和一个固定图例,用于说明拖拽状态。

用户可以拖拽节点,观察目标节点在绿色、红色和紫色反馈之间切换,在每个目标节点的卡片内修改其放置策略,并在允许的目标上释放以创建一条新连线。重点不在于自由式布局编辑,而在于展示 relation-graph 如何将重叠检测转化为拖拽过程中的接受、拒绝或受限移动。

数据是如何组织的

这个图以内联方式声明为一个 RGJsonData 对象,包含 rootId、扁平的 nodes 数组以及扁平的 lines 数组。由于该示例使用固定布局,每个节点都带有固定的 xy 坐标,另外有些节点还会在 node.data 中携带运行时策略标记,例如 allowDropdisallowDroppingNodesInside

在调用 setJsonData(...) 之前没有做任何预处理。代码会直接加载这份数据集,然后让视口居中并适配显示。在交互过程中,示例还会给 node.data 增加 startXstartY 以及已保存颜色等临时字段,因此拖拽规则是直接挂在节点自身上的,而不是保存在单独的编辑器 store 中。

在真实应用里,同样的数据结构可以表示角色与权限、工作流步骤、存储区域,或者只能附着到已批准父节点上的资产。本地的重叠检测工具通过矩形相交来完成命中测试,因此即使渲染出的图混合了不同节点形状,判定逻辑依然保持简单。

relation-graph 的使用方式

index.tsxRGProvider 包裹整个示例,而 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(...) 更新 wheelEventActiondragEventAction,并通过 prepareForImageGeneration()restoreAfterImageGeneration() 导出图像。配套的 SCSS 则用可随缩放变化的点状图案覆盖画布背景,并定义图例面板的样式。

关键交互

  • 拖拽节点时,会持续检查它与图中其他所有节点的重叠情况。
  • 当与带有 allowDrop 的目标发生重叠时,该目标会变为绿色;若目标没有 allowDrop,则会变为红色。
  • 当与带有 disallowDroppingNodesInside 的目标发生重叠时,该目标会变为紫色,并返回一个受限位置,使被拖拽节点保持在受保护节点之外。
  • 拖拽开始时,会在 node.data 中记录原始坐标;拖拽结束时,会先恢复这些坐标,然后再创建一条新的关系线。
  • Allow DropDisallow 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-actionsbatch-operations-on-nodes 相比,这里的自定义节点 UI 不是一个用于选择或批量工具的动作启动器。每个节点都携带自己的规则开关,因此图内容本身直接决定一次碰撞应被接受、拒绝,还是视为受保护重叠。

gee-node-resizegee-node-alignment-guides 相比,这里的额外拖拽反馈强调的是语义,而不是几何。这里少见的组合是固定布局场景、内联策略卡片、视图级图例、手写重叠工具,以及在被拖拽节点回弹到原始位置之后再于释放时创建连线。这使它成为基于规则的放置行为的良好起点,而不是通用编辑器外壳的起点。

这种模式还适用于哪里

这种模式非常适合工作流或审批设计器,在这类场景中,把一个步骤拖到另一个步骤上时,只有目标允许时才应该提出一条有效迁移。

它也适用于访问控制映射、数据血缘梳理以及依赖关系编排工具,在这些工具中,对象可以连接到某些类别的节点,但必须避开受保护的容器或区域。

另一个相关用法是运维规划界面,例如存储布局或设备地图,在这些场景里,重叠可以作为一种快速分配手势,而受保护区域会拒绝进入,只记录这层关系。