JavaScript is required

画布中心偏移

这个示例构建的是一个 canvas 定位演示环境,而不是图数据展示案例。页面会渲染一个全高的 `RelationGraph` canvas,在 canvas 图层内挂载一个带有标签的大型 SVG 坐标网格,并在其上方放置一个浮动工具窗口。

使用挂载在插槽中的坐标网格重新居中 Canvas

这个示例构建了什么

这个示例构建的是一个 canvas 定位演示环境,而不是图数据展示案例。页面会渲染一个全高的 RelationGraph canvas,在 canvas 图层内挂载一个带有标签的大型 SVG 坐标网格,并在其上方放置一个浮动工具窗口。

用户可以拖动这个窗口、将其最小化、打开一个共享设置面板、导出图片,以及最重要的,通过两个滑块在 X 和 Y 轴上移动 canvas 中心。这个示例的重点在于把 canvas 重新居中这一行为单独抽离出来,因此无需节点数据、连线走线或自动布局变化的干扰,就能更容易观察效果。

数据是如何组织的

这个演示中没有图数据集。MyGraph.tsx 引入了 RGJsonData,但它从未调用 setJsonData,也没有向 RelationGraph 提供节点或连线,并且让 initializeGraph() 保持为空。运行时的状态只有两个数字:canvasOffsetXcanvasOffsetY,它们被用作目标 canvas 中心。

可视化参照来自 CoordinateGrid.tsx。这个组件在两个轴上都定义了从 -10001000 的固定图空间范围,使用 50 的步长,并在渲染 SVG 之前通过 memoized 方式预先计算每个网格分段的文本标签位置。在生产工具中,同样的结构可以表示车间坐标、仓库巷道、平面图参考线、CAD 标尺、座位图,或任何其他需要始终与图空间对齐的测量型覆盖层。

relation-graph 是如何使用的

index.tsxRGProvider 包裹整个示例,因此 relation-graph 的 hooks 可以解析当前活动图的上下文。在 MyGraph.tsx 中,RelationGraph 使用 layoutName: 'fixed' 运行,启用了位于右下角的横向工具栏,并设置了预定义的节点和连线默认值。由于没有加载任何图数据,这些节点和连线默认值更多只是潜在配置,而不是这个示例可见的核心内容。

关键的运行时集成点是 RGHooks.useGraphInstance()。一个 React effect 监听 canvasOffsetXcanvasOffsetY,然后在任一值变化时调用 graphInstance.setCanvasCenter(...)。这让一对普通的 React 控件变成了 relation-graph 的外部视角控制界面。

另一个重要的集成点是 RGSlotOnCanvas。示例没有通过 CSS 将网格画在图容器之外,而是把 SVG 直接挂载到 canvas 图层中,并将其包裹元素偏移到 left: -1000pxtop: -1000px。这种放置方式会让网格的 viewBox 与图空间坐标对齐,因此移动 canvas 中心时会产生可测量的可视化结果。

浮动辅助窗口来自一个共享的 DraggableWindow 组件。在这个示例里,它承载了 X/Y 滑块;其内置的设置覆盖层通过 RGHooks.useGraphStore() 以及 graphInstance.setOptions(...) 在运行时切换滚轮和拖拽行为。这个辅助组件还会使用 prepareForImageGeneration()getOptions()restoreAfterImageGeneration(),通过 modern-screenshot 导出当前 canvas。

样式刻意保持轻量。my-relation-graph.scss 为工具栏、节点和连线提供了选择器骨架,但没有加入具体覆盖样式,因此图表表面基本保持库默认外观,而坐标网格和浮动面板承担了主要的视觉含义。

关键交互

主要交互是由滑块驱动的重新居中。每个滑块都会以 10 为步进更新本地 React 状态,而 effect hook 会立即把新值传给 setCanvasCenter(...)。这些滑块不会移动节点,也不会改变布局;它们移动的是 canvas 中心本身。

浮动窗口本身也可交互。用户可以通过标题栏拖动它、将其最小化,并打开设置覆盖层。这个覆盖层并不是此示例独有的,但在这里它很重要,因为它允许用户在测试这个空白工作区时,把滚轮行为在滚动、缩放和禁用之间切换,并把拖拽行为在框选、移动和禁用之间切换。

最后一个有意义的交互是图片导出。辅助面板可以为截图准备当前图 canvas,将其渲染为 blob 并下载,因此在检查坐标对齐时,这个演示也能作为一个小型调试或文档辅助工具。

关键代码片段

这个 effect 是核心证据,说明该演示是通过图实例来控制 canvas 位置,而不是通过布局数据来控制。

const [canvasOffsetX, setCanvasOffsetX] = useState(0);
const [canvasOffsetY, setCanvasOffsetY] = useState(0);
const graphInstance = RGHooks.useGraphInstance();

useEffect(() => {
    graphInstance.setCanvasCenter(canvasOffsetX, canvasOffsetY);
}, [canvasOffsetX, canvasOffsetY]);

这段 JSX 表明,示例专用 UI 只有两个滑块,而图表表面本身接收了一个 canvas 图层覆盖层。

<DraggableWindow>
    <div className="py-1 text-sm">canvas center:</div>
    <div className="c-option-name">canvas X: {canvasOffsetX}</div>
    <SimpleUISlider min={-1000} max={1000} step={10} currentValue={canvasOffsetX} onChange={(newValue: number) => { setCanvasOffsetX(newValue); }} />
    <div className="c-option-name">canvas Y: {canvasOffsetY}</div>
    <SimpleUISlider min={-1000} max={1000} step={10} currentValue={canvasOffsetY} onChange={(newValue: number) => { setCanvasOffsetY(newValue); }} />
</DraggableWindow>

这段经过 memoized 的代码块表明,坐标标签是在 SVG 渲染之前,基于固定图空间范围预先计算出来的。

const labels = useMemo(() => {
  const xLabels = [];
  const yLabels = [];

  for (let x = minX; x <= maxX; x += step) {
    for (let y = minY; y < maxY; y += step) {
      xLabels.push({ x, y: y + step / 2, val: x });
    }
  }
  // ... matching yLabels loop omitted
  return { xLabels, yLabels };
}, [minX, maxX, minY, maxY, step]);

这段 canvas 插槽接线方式,正是让网格与图空间而不是页面视口保持对齐的原因。

<RelationGraph options={graphOptions}>
    <RGSlotOnCanvas>
        <div className="absolute left-[-1000px] top-[-1000px]">
            <CoordinateGrid />
        </div>
    </RGSlotOnCanvas>
</RelationGraph>

这段共享设置行展示了辅助面板如何在不重新加载场景的情况下,于运行时改变 relation-graph 的输入行为。

<SettingRow
    label="Wheel Event:"
    options={[
        { label: 'Scroll', value: 'scroll' },
        { label: 'Zoom', value: 'zoom' },
        { label: 'None', value: 'none' },
    ]}
    value={wheelMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>

这个导出辅助逻辑会先调用 relation-graph 的图片准备 API,然后再把 canvas DOM 交给 modern-screenshot

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();

这个示例的独特之处

对比数据表明,这个示例有一个非常明确的突出点:它把 setCanvasCenter(...) 变成了核心教学内容。外部滑块直接驱动 X 和 Y 方向的重新居中,因此这个演示把 canvas 移动本身单独抽离出来,而不是把它和数据加载或布局过渡混在一起。

它也比附近的示例更偏向测量场景。与 canvas-caliper 相比,这个演示是在固定的图空间网格上移动 canvas 中心,而不是用标尺标注当前可见视口。与 area-set 相比,这里的 canvas 插槽用途是分析性的而不是语义性的:它存在的意义是揭示坐标和偏移,而不是划分一个已填充的场景。与 zoomgee-thumbnail-diagram 相比,这里的外围控件改变的是图空间位置本身,而不是缩放或缩略图配置。

最具辨识度的组合是:一个原本为空的固定布局图、一个跨越负坐标和正坐标并带有标签的 RGSlotOnCanvas 网格,以及由滑块驱动的 setCanvasCenter(...)。这使得该示例比那些恰好带有视口控件的数据密集型演示,更适合作为调试图空间对齐问题的起点。

这种模式还适用于哪里

这种模式很适合迁移到需要精确外部视角控制的工具中。典型的后续用途包括:跳转到选定工作区域的重新居中按钮、把 canvas 移动到指定坐标的同步检查器,以及按步骤浏览图空间地标的引导演示。

它也适用于以测量为主的覆盖层。团队可以把演示中的网格替换为平面图坐标轴、仓库坐标、工程参考线、游戏地图分区或大图标注参考线,同时继续保留相同的 RGSlotOnCanvassetCanvasCenter(...) 结构。

最后,它还是一个很实用的排障模板。当项目需要验证插槽内容、图空间坐标、截图结果和交互模式是否保持对齐时,一个带有标签覆盖层的空场景,往往比一个填充了业务数据的图更容易分析。