[{"data":1,"prerenderedAt":7},["ShallowReactive",2],{"example-markdown-content:zh:compare-element-to-node-and-element-to-element-links":3},{"markdown":4,"isPaidExample":5,"isTruncated":5,"charLimit":6},"# 对比 Element-to-Node 与 Element-to-Element 连线\n\n## 这个示例构建了什么\n\n这个示例在单个 relation-graph canvas 内构建了一个三栏对比面板。左侧面板将位置标记显示为叠加在地图上的真实图节点，中间面板显示可选择的兴趣小组卡片，右侧面板则将相同的位置显示为普通 HTML 标记。当用户选中某个小组时，示例会从同一个列表项重新绘制两条带动画的连接线：一条连到左侧的图节点，另一条连到右侧的 HTML 标记。\n\n最终呈现的效果，与其说是在展示图拓扑，不如说是在对比端点策略。它让开发者可以在相同坐标和相同样式下，对比目标端保留在图模型内部，和变成普通 DOM 目标之后会有什么差异。一个浮动辅助窗口补充了运行时 canvas 设置和图片导出能力，但这个示例最核心的亮点，是同步进行的并排连接器对比。\n\n## 数据如何组织\n\n这个示例在 `MyGraph.tsx` 中使用了一组很小的内联数据集。每条记录都包含 `groupId`、`groupName`，以及一个带有明确 `x` 和 `y` 坐标的 `location` 对象。这里没有调用 `setJsonData()`。相反，初始化流程会接收这组内联数组，把它存入 React state `interestGroups`，再通过 `addNodes(...)` 将其映射为图节点。\n\n同一个 `interestGroups` state 被复用了三次：\n\n- 用来创建固定位置的图节点，id 形如 `node-location-a`\n- 用来渲染中间列的列表项，并赋予它们类似 `group-a` 的 connect-target id\n- 用来渲染右侧面板的 HTML 标记，并赋予它们类似 `el-location-a` 的 connect-target id\n\n这是该示例最重要的数据层启示：同一条源记录同时驱动图原生端点和普通 DOM 端点。在生产场景中，这些记录可以表示校园地图中的商店、仓储站点、维护点、活动展位或服务台，此时同一个界面需要在同一组物理坐标上对比多种锚点策略。\n\n## 如何使用 relation-graph\n\n`RGProvider` 包裹了整个 demo，以便 hooks 可以访问当前激活的图上下文。图本身运行在 `layout.layoutName = 'fixed'` 模式下，这意味着节点会停留在 `addNodes(...)` 提供的精确坐标上。选项里还关闭了调试模式，将边框交点设为默认连接模式，并让 canvas 初始进入滚轮缩放和拖拽移动模式。\n\n这个示例以四种不同方式使用 relation-graph：\n\n- `RGHooks.useGraphInstance()` 用于驱动图的初始化和运行时控制。它负责添加固定位置节点、居中视口、适配场景、清除已有 fake line、添加替换后的 fake line、更新运行时选项，并为图片导出准备 canvas。\n- `RGSlotOnNode` 用 `MapPin` 标记替换默认的节点渲染器。节点插槽使用 `checked` 样式，并通过 `node.data.myGroupId` 将点击事件转发回共享的小组选择处理函数。\n- `RGSlotOnCanvas` 是外围场景的组合位置。这个 demo 并没有渲染成普通的节点连线图，而是把左侧地图、中间列表和右侧地图一起组织成一个由 canvas 承载的界面。\n- `RGConnectTarget` 为列表卡片和右侧 HTML 标记提供可挂接的 DOM 端点。左侧连接器的目标是真实节点，因此那条 fake line 需要设置 `toType = RGInnerConnectTargetType.Node`。右侧连接器则使用普通的 connect-target id。\n\n浮动辅助窗口是一个共享子组件，而不是这个示例独有的功能，但它在技术上仍然很重要。它的设置面板使用 `RGHooks.useGraphStore()` 反映当前 canvas 模式，并通过 `graphInstance.setOptions(...)` 在运行时切换滚轮和拖拽行为。导出时，它会调用 `prepareForImageGeneration()`，使用 `modern-screenshot` 捕获 canvas DOM，下载生成的 blob，然后再恢复图状态。\n\n样式定义位于本地 SCSS 文件中。样式表为小组卡片提供了紫色选中光晕，把两种标记都做成带动画的图钉式目标，并让线条标签通过 `var(--rg-line-color)` 继承当前连接器颜色。\n\n## 关键交互\n\n- 挂载阶段的 effect 会初始化节点、居中视口、适配场景，并在短暂延迟后自动选中小组 `a`，从而在页面一加载时就展示对比效果。\n- 点击列表卡片、左侧图标记或右侧 HTML 标记，都会调用同一个 `onGroupClick(...)` 函数。\n- 每次选中都会更新 `activeGroupId`、清除旧的 fake line，并为当前小组插入一对新的带动画曲线连接器。\n- 辅助窗口可以被拖动、最小化、切换到设置覆盖层，并用于修改滚轮模式、修改拖拽模式，或下载当前 canvas 的图片。\n\n## 关键代码片段\n\n这个片段展示了示例如何从一组内联数据集开始，并在任何连接器逻辑运行之前，先将其映射为固定位置的图节点。\n\n```tsx\nconst myGroups = [\n  { groupId: 'a', groupName: 'Sports Group', location: { x: 260, y: 300 } },\n  // ...\n  { groupId: 'f', groupName: 'Science Research Group', location: { x: 600, y: 240 } }\n];\nsetInterestGroups(myGroups);\ngraphInstance.addNodes(myGroups.map(n => ({\n  id: 'node-location-' + n.groupId,\n  text: n.groupName,\n  x: n.location.x,\n  y: n.location.y,\n  disableDrag: true,\n  data: { myGroupId: n.groupId }\n})));\n```\n\n这个片段证明了一次选择会重新生成两种不同的 fake line：一条连到真实节点，另一条连到 DOM 标记。\n\n```tsx\nconst myFakeLines: JsonLine[] = [\n  {\n    from: 'group-' + groupId,\n    to: 'node-location-' + groupId,\n    toType: RGInnerConnectTargetType.Node,\n    lineShape: RGLineShape.StandardCurve,\n    text: 'Element To Node',\n    animation: 2\n  },\n  {\n    from: 'group-' + groupId,\n    to: 'el-location-' + groupId,\n    lineShape: RGLineShape.StandardCurve,\n    text: 'Element To Element',\n    animation: 2\n  }\n];\ngraphInstance.clearFakeLines();\ngraphInstance.addFakeLines(myFakeLines);\n```\n\n这个片段展示了图侧目标如何被渲染成自定义节点标记，并如何回流到共享的选择状态中。\n\n```tsx\n\u003CRGSlotOnNode>\n  {({ node, checked }: RGNodeSlotProps) => (\n    \u003Cdiv\n      className={`pointer-events-auto cursor-point c-i-location ${checked ? 'c-i-location-active' : ''}`}\n      onClick={() => onGroupClick(node.data.myGroupId)}\n    >\n      \u003CMapPin className=\"transform translate-y-[-25px] translate-x-[-5px]\" size={24} />\n    \u003C/div>\n  )}\n\u003C/RGSlotOnNode>\n```\n\n这个片段展示了同一份 state 如何同时驱动右侧对比面板中的普通 HTML connect target。\n\n```tsx\n{interestGroups.map(group => (\n  \u003Cdiv\n    key={group.groupId}\n    className=\"absolute\"\n    style={{ left: group.location.x + 'px', top: group.location.y + 'px' }}\n  >\n    \u003CRGConnectTarget targetId={`el-location-${group.groupId}`} junctionPoint={RGJunctionPoint.lr}>\n      \u003Cdiv onClick={() => onGroupClick(group.groupId)} className=\"pointer-events-auto cursor-point c-i-location\">\n        \u003CMapPin className=\"transform translate-y-[-25px] translate-x-[-5px]\" size={24} />\n      \u003C/div>\n    \u003C/RGConnectTarget>\n  \u003C/div>\n))}\n```\n\n## 这个示例的独特之处\n\n从对比角度来看，这个示例并不只是一个基础的 element-to-node demo。它最鲜明的特点在于：同一个被选中的源项会同时重绘两条同步连接器，一条终止于真实的 relation-graph 节点，另一条终止于普通 HTML 标记。因此，与只展示单一模式的示例相比，它更适合作为端点挂接策略决策时的参考。\n\n与 `element-line-edit` 相比，这个 demo 的重点不再是单个由节点承载的目标，而是并排对照。与 `element-lines` 相比，它保留了一个位于图模型中的目标端，而不是把两个端点都放在普通 HTML 中。与 `interest-group` 和 `inventory-structure-diagram` 这类更广义的地图式面板相比，它在领域结构上的投入更少，而更强调让端点差异易于观察。\n\n从另一层对比意义上说，这个示例还保留了空间等价性。同一组 `interestGroups` 坐标同时映射到了左侧图节点和右侧 HTML 标记，因此视觉差异来自端点类型，而不是布局漂移。固定布局节点投影、`RGSlotOnNode`、`RGSlotOnCanvas`、成对的 `RGConnectTarget` 用法、自动默认选择，以及同步 fake line 替换这些组合，在整个示例集合中都相对少见。\n\n## 这种模式还能用在哪里\n\n这种模式非常适合用于那些需要评估锚点应继续保留为图原生对象，还是迁移为普通 DOM 的产品。典型场景包括校园与设施地图、物流看板、座位或展位规划器、零售平面系统，以及监控面板。在这些场景中，同一个实体往往需要同时在列表和空间表面上被高亮。\n\n它同样适合作为一种迁移模式。团队可以把一个端点保留在图模型中，以获得视口感知行为、选中状态和 slot 渲染能力，同时把另一个端点暴露为普通 HTML，以便更容易与现有卡片、面板或覆盖层集成。因此，这个示例既适合作为 UI 对比面板，也适合作为构建 graph-plus-DOM 混合界面的起点。\n",false,500,1782615413070]