[{"data":1,"prerenderedAt":7},["ShallowReactive",2],{"example-markdown-content:zh:canvas-center-offset":3},{"markdown":4,"isPaidExample":5,"isTruncated":5,"charLimit":6},"# 使用挂载在插槽中的坐标网格重新居中 Canvas\n\n## 这个示例构建了什么\n\n这个示例构建的是一个 canvas 定位演示环境，而不是图数据展示案例。页面会渲染一个全高的 `RelationGraph` canvas，在 canvas 图层内挂载一个带有标签的大型 SVG 坐标网格，并在其上方放置一个浮动工具窗口。\n\n用户可以拖动这个窗口、将其最小化、打开一个共享设置面板、导出图片，以及最重要的，通过两个滑块在 X 和 Y 轴上移动 canvas 中心。这个示例的重点在于把 canvas 重新居中这一行为单独抽离出来，因此无需节点数据、连线走线或自动布局变化的干扰，就能更容易观察效果。\n\n## 数据是如何组织的\n\n这个演示中没有图数据集。`MyGraph.tsx` 引入了 `RGJsonData`，但它从未调用 `setJsonData`，也没有向 `RelationGraph` 提供节点或连线，并且让 `initializeGraph()` 保持为空。运行时的状态只有两个数字：`canvasOffsetX` 和 `canvasOffsetY`，它们被用作目标 canvas 中心。\n\n可视化参照来自 `CoordinateGrid.tsx`。这个组件在两个轴上都定义了从 `-1000` 到 `1000` 的固定图空间范围，使用 `50` 的步长，并在渲染 SVG 之前通过 memoized 方式预先计算每个网格分段的文本标签位置。在生产工具中，同样的结构可以表示车间坐标、仓库巷道、平面图参考线、CAD 标尺、座位图，或任何其他需要始终与图空间对齐的测量型覆盖层。\n\n## relation-graph 是如何使用的\n\n`index.tsx` 用 `RGProvider` 包裹整个示例，因此 relation-graph 的 hooks 可以解析当前活动图的上下文。在 `MyGraph.tsx` 中，`RelationGraph` 使用 `layoutName: 'fixed'` 运行，启用了位于右下角的横向工具栏，并设置了预定义的节点和连线默认值。由于没有加载任何图数据，这些节点和连线默认值更多只是潜在配置，而不是这个示例可见的核心内容。\n\n关键的运行时集成点是 `RGHooks.useGraphInstance()`。一个 React effect 监听 `canvasOffsetX` 和 `canvasOffsetY`，然后在任一值变化时调用 `graphInstance.setCanvasCenter(...)`。这让一对普通的 React 控件变成了 relation-graph 的外部视角控制界面。\n\n另一个重要的集成点是 `RGSlotOnCanvas`。示例没有通过 CSS 将网格画在图容器之外，而是把 SVG 直接挂载到 canvas 图层中，并将其包裹元素偏移到 `left: -1000px` 和 `top: -1000px`。这种放置方式会让网格的 `viewBox` 与图空间坐标对齐，因此移动 canvas 中心时会产生可测量的可视化结果。\n\n浮动辅助窗口来自一个共享的 `DraggableWindow` 组件。在这个示例里，它承载了 X/Y 滑块；其内置的设置覆盖层通过 `RGHooks.useGraphStore()` 以及 `graphInstance.setOptions(...)` 在运行时切换滚轮和拖拽行为。这个辅助组件还会使用 `prepareForImageGeneration()`、`getOptions()` 和 `restoreAfterImageGeneration()`，通过 `modern-screenshot` 导出当前 canvas。\n\n样式刻意保持轻量。`my-relation-graph.scss` 为工具栏、节点和连线提供了选择器骨架，但没有加入具体覆盖样式，因此图表表面基本保持库默认外观，而坐标网格和浮动面板承担了主要的视觉含义。\n\n## 关键交互\n\n主要交互是由滑块驱动的重新居中。每个滑块都会以 `10` 为步进更新本地 React 状态，而 effect hook 会立即把新值传给 `setCanvasCenter(...)`。这些滑块不会移动节点，也不会改变布局；它们移动的是 canvas 中心本身。\n\n浮动窗口本身也可交互。用户可以通过标题栏拖动它、将其最小化，并打开设置覆盖层。这个覆盖层并不是此示例独有的，但在这里它很重要，因为它允许用户在测试这个空白工作区时，把滚轮行为在滚动、缩放和禁用之间切换，并把拖拽行为在框选、移动和禁用之间切换。\n\n最后一个有意义的交互是图片导出。辅助面板可以为截图准备当前图 canvas，将其渲染为 blob 并下载，因此在检查坐标对齐时，这个演示也能作为一个小型调试或文档辅助工具。\n\n## 关键代码片段\n\n这个 effect 是核心证据，说明该演示是通过图实例来控制 canvas 位置，而不是通过布局数据来控制。\n\n```tsx\nconst [canvasOffsetX, setCanvasOffsetX] = useState(0);\nconst [canvasOffsetY, setCanvasOffsetY] = useState(0);\nconst graphInstance = RGHooks.useGraphInstance();\n\nuseEffect(() => {\n    graphInstance.setCanvasCenter(canvasOffsetX, canvasOffsetY);\n}, [canvasOffsetX, canvasOffsetY]);\n```\n\n这段 JSX 表明，示例专用 UI 只有两个滑块，而图表表面本身接收了一个 canvas 图层覆盖层。\n\n```tsx\n\u003CDraggableWindow>\n    \u003Cdiv className=\"py-1 text-sm\">canvas center:\u003C/div>\n    \u003Cdiv className=\"c-option-name\">canvas X: {canvasOffsetX}\u003C/div>\n    \u003CSimpleUISlider min={-1000} max={1000} step={10} currentValue={canvasOffsetX} onChange={(newValue: number) => { setCanvasOffsetX(newValue); }} />\n    \u003Cdiv className=\"c-option-name\">canvas Y: {canvasOffsetY}\u003C/div>\n    \u003CSimpleUISlider min={-1000} max={1000} step={10} currentValue={canvasOffsetY} onChange={(newValue: number) => { setCanvasOffsetY(newValue); }} />\n\u003C/DraggableWindow>\n```\n\n这段经过 memoized 的代码块表明，坐标标签是在 SVG 渲染之前，基于固定图空间范围预先计算出来的。\n\n```tsx\nconst labels = useMemo(() => {\n  const xLabels = [];\n  const yLabels = [];\n\n  for (let x = minX; x \u003C= maxX; x += step) {\n    for (let y = minY; y \u003C maxY; y += step) {\n      xLabels.push({ x, y: y + step / 2, val: x });\n    }\n  }\n  // ... matching yLabels loop omitted\n  return { xLabels, yLabels };\n}, [minX, maxX, minY, maxY, step]);\n```\n\n这段 canvas 插槽接线方式，正是让网格与图空间而不是页面视口保持对齐的原因。\n\n```tsx\n\u003CRelationGraph options={graphOptions}>\n    \u003CRGSlotOnCanvas>\n        \u003Cdiv className=\"absolute left-[-1000px] top-[-1000px]\">\n            \u003CCoordinateGrid />\n        \u003C/div>\n    \u003C/RGSlotOnCanvas>\n\u003C/RelationGraph>\n```\n\n这段共享设置行展示了辅助面板如何在不重新加载场景的情况下，于运行时改变 relation-graph 的输入行为。\n\n```tsx\n\u003CSettingRow\n    label=\"Wheel Event:\"\n    options={[\n        { label: 'Scroll', value: 'scroll' },\n        { label: 'Zoom', value: 'zoom' },\n        { label: 'None', value: 'none' },\n    ]}\n    value={wheelMode}\n    onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}\n/>\n```\n\n这个导出辅助逻辑会先调用 relation-graph 的图片准备 API，然后再把 canvas DOM 交给 `modern-screenshot`。\n\n```tsx\nconst canvasDom = await graphInstance.prepareForImageGeneration();\nlet graphBackgroundColor = graphInstance.getOptions().backgroundColor;\nif (!graphBackgroundColor || graphBackgroundColor === 'transparent') {\n    graphBackgroundColor = '#ffffff';\n}\nconst imageBlob = await domToImageByModernScreenshot(canvasDom, {\n    backgroundColor: graphBackgroundColor\n});\nawait graphInstance.restoreAfterImageGeneration();\n```\n\n## 这个示例的独特之处\n\n对比数据表明，这个示例有一个非常明确的突出点：它把 `setCanvasCenter(...)` 变成了核心教学内容。外部滑块直接驱动 X 和 Y 方向的重新居中，因此这个演示把 canvas 移动本身单独抽离出来，而不是把它和数据加载或布局过渡混在一起。\n\n它也比附近的示例更偏向测量场景。与 `canvas-caliper` 相比，这个演示是在固定的图空间网格上移动 canvas 中心，而不是用标尺标注当前可见视口。与 `area-set` 相比，这里的 canvas 插槽用途是分析性的而不是语义性的：它存在的意义是揭示坐标和偏移，而不是划分一个已填充的场景。与 `zoom` 和 `gee-thumbnail-diagram` 相比，这里的外围控件改变的是图空间位置本身，而不是缩放或缩略图配置。\n\n最具辨识度的组合是：一个原本为空的固定布局图、一个跨越负坐标和正坐标并带有标签的 `RGSlotOnCanvas` 网格，以及由滑块驱动的 `setCanvasCenter(...)`。这使得该示例比那些恰好带有视口控件的数据密集型演示，更适合作为调试图空间对齐问题的起点。\n\n## 这种模式还适用于哪里\n\n这种模式很适合迁移到需要精确外部视角控制的工具中。典型的后续用途包括：跳转到选定工作区域的重新居中按钮、把 canvas 移动到指定坐标的同步检查器，以及按步骤浏览图空间地标的引导演示。\n\n它也适用于以测量为主的覆盖层。团队可以把演示中的网格替换为平面图坐标轴、仓库坐标、工程参考线、游戏地图分区或大图标注参考线，同时继续保留相同的 `RGSlotOnCanvas` 与 `setCanvasCenter(...)` 结构。\n\n最后，它还是一个很实用的排障模板。当项目需要验证插槽内容、图空间坐标、截图结果和交互模式是否保持对齐时，一个带有标签覆盖层的空场景，往往比一个填充了业务数据的图更容易分析。\n",false,500,1782615432772]