X6 坐标系完全指南:local、graph、client 与 page 四种坐标系的转换方法详解
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
坐标系转换是图编辑类应用中最高频、也最容易出错的环节——无论是把鼠标点击位置换算成节点坐标、把节点位置换算成拖拽辅助线坐标,还是实现"右键菜单跟随节点"这类交互,本质都是坐标系之间的映射。本文基于 X6 官方文档 坐标系 展开,结合仓库中 CoordManager 实现 与 坐标转换测试 的源码证据,系统讲解 X6 中local、graph、client、page四种坐标系的含义,以及pageToLocal、localToPage、clientToLocal、localToClient、localToGraph、graphToLocal、snapToGrid七个转换方法的签名、底层原理与实战用法。读完本文,你将能够准确判断任意场景下该用哪个转换方法,并理解每一步换算背后的矩阵运算。
一、X6 中的四种坐标系
在 X6 中,位置计算经常需要坐标转换。文档中明确了四种坐标系,理解它们是使用一切转换方法的前提:
local(画布本地坐标系):默认情况下与graph坐标系一致,但会随着画布的缩放(scale)和平移(translate)发生改变。画布中所有节点的位置(x、y)都以local坐标系为准,因此它是节点、边等模型数据实际存储所用的坐标系。graph(画布坐标系):即我们看到的画布视口所对应的坐标系,它不会随画布缩放和平移而改变。可以把它理解为"未被变换矩阵作用过的原始画布坐标系"。client(浏览器坐标系):鼠标事件中的e.clientX、e.clientY就是相对于浏览器坐标系(即相对浏览器视口左上角)。page(页面坐标系):与client相比,page额外考虑了页面在水平和垂直方向的滚动。鼠标事件中的e.pageX、e.pageY就是相对于页面坐标系(即相对文档左上角)。
从源码结构看,这四种坐标系并非凭空定义,而是由 src/graph/coord.ts 中的CoordManager类统一管理。该类通过两个基础数据完成所有换算:
getClientMatrix():调用this.view.stage.getScreenCTM()获取 SVG 舞台相对文档的屏幕 CTM(Current Transformation Matrix),这是 local 到 client 的核心桥梁;getClientOffset()/getPageOffset():分别通过svg.getBoundingClientRect()得到画布视口相对窗口的偏移,再叠加window.scrollX、window.scrollY得到页面偏移。
对应的单元测试见tests/graph/coord.spec.ts,其中通过getBoundingClientRect的 mock 返回值(如left: 10, top: 20)验证了偏移计算的正确性,并通过scrollX/scrollY的桩数据验证getPageOffset确实等于 client 偏移加滚动量。
二、坐标系之间的关系图
四个坐标系之间的换算关系可以用一句话概括:page与client之间差一个页面滚动偏移,graph与local之间差一个画布变换矩阵,local与client之间差一个屏幕 CTM(其本身已包含画布变换矩阵)。X6 提供的全部转换方法,本质上就是在这四个坐标系之间两两搭桥:
pageToLocal:page → local(先减页面偏移得到 graph,再用画布矩阵逆变换)localToPage:local → page(先用画布矩阵变换到 graph,再加页面偏移)clientToLocal:client → local(用屏幕 CTM 的逆矩阵变换)localToClient:local → client(用屏幕 CTM 变换)localToGraph:local → graph(用画布矩阵变换)graphToLocal:graph → local(用画布矩阵的逆变换)
其中clientToGraph虽然未在原文档方法列表中单独列出,但实现中同样存在(见 graph.ts),它通过graph.matrix().multiply(clientMatrix.inverse())组合两个矩阵完成换算,可视为clientToLocal与localToGraph的合成。
三、坐标转换方法详解
X6 的Graph实例上直接暴露了以下坐标转换方法(均在 graph.ts 的// #region coord区块中声明并委托给CoordManager实现)。所有方法都支持点和矩形两种输入形态:
- 传点:
(p: Point.PointLike)或(x: number, y: number),返回Point; - 传矩形:
(rect: Rectangle.RectangleLike)或(x, y, width, height)四个数值,返回Rectangle。
3.1 pageToLocal(...):页面坐标 → 画布本地坐标
pageToLocal(rect: Rectangle.RectangleLike): Rectangle pageToLocal(x: number, y: number, width: number, height: number): Rectangle pageToLocal(p: Point.PointLike): Point pageToLocal(x: number, y: number): Point将页面坐标转换为画布本地坐标,即把e.pageX、e.pageY这类坐标换算为节点可以直接使用的x、y。这是实现"点击画布空白处添加节点""右键菜单定位"等交互的标配方法。
底层实现(coord.ts 与 coord.ts):
pageToLocalPoint(x, y) { const pagePoint = Point.create(x, y) const graphPoint = pagePoint.diff(this.getPageOffset()) return this.graphToLocalPoint(graphPoint) }先减去页面偏移得到graph坐标,再调用graphToLocalPoint用画布矩阵的逆矩阵变换到local坐标。测试用例 coord.spec.ts 验证了该流程:pageToLocalPoint确实先 diff 页面偏移再做逆变换。
3.2 localToPage(...):画布本地坐标 → 页面坐标
localToPage(rect: Rectangle.RectangleLike): Rectangle localToPage(x: number, y: number, width: number, height: number): Rectangle localToPage(p: Point.PointLike): Point localToPage(x: number, y: number): Point将画布本地坐标转换为页面坐标,是pageToLocal的逆运算。典型场景:已知某节点的本地坐标,需要在页面层(如 DOM 定位的浮动面板)渲染一个跟随它的元素。
底层实现(coord.ts):
localToPagePoint(x, y) { const p = this.localToGraphPoint(x, y) return p.translate(this.getPageOffset()) }先用画布矩阵变换到graph坐标,再加上getPageOffset()(client 偏移 + 滚动量)。测试 coord.spec.ts 中,local 点(5,5)叠加页面偏移(2,3)得到(7,8),直接印证了这一加法运算。
3.3 clientToLocal(...):浏览器坐标 → 画布本地坐标
clientToLocal(rect: Rectangle.RectangleLike): Rectangle clientToLocal(x: number, y: number, width: number, height: number): Rectangle clientToLocal(p: Point.PointLike): Point clientToLocal(x: number, y: number): Point将浏览器坐标(e.clientX、e.clientY)转换为画布本地坐标。与pageToLocal的区别在于:这里不再关心页面滚动,直接利用屏幕 CTM 的逆矩阵完成换算。
底层实现(coord.ts):
clientToLocalPoint(x, y) { const clientPoint = Point.create(x, y) return Util.transformPoint(clientPoint, this.getClientMatrix().inverse()) }由于getClientMatrix()本身来自stage.getScreenCTM(),其逆矩阵可以同时抵消画布视口偏移与画布变换,一步到位得到 local 坐标。
3.4 localToClient(...):画布本地坐标 → 浏览器坐标
localToClient(rect: Rectangle.RectangleLike): Rectangle localToClient(x: number, y: number, width: number, height: number): Rectangle localToClient(p: Point.PointLike): Point localToClient(x: number, y: number): Point将画布本地坐标转换为浏览器坐标,是clientToLocal的逆运算。当需要把画布内部坐标映射为视口内的绝对定位(如自定义 Tooltip、ContextMenu 挂载到 body)时使用。
底层实现(coord.ts):直接用屏幕 CTM 变换本地点,即Util.transformPoint(localPoint, this.getClientMatrix())。
3.5 localToGraph(...):画布本地坐标 → 画布坐标
localToGraph(rect: Rectangle.RectangleLike): Rectangle localToGraph(x: number, y: number, width: number, height: number): Rectangle localToGraphPoint(p: Point.PointLike): Point localToGraphPoint(x: number, y: number): Point将画布本地坐标转换为画布坐标。graph坐标不受缩放和平移影响,因此在"判断两个节点在当前视口下是否重叠""把 local 坐标归一化到无变换坐标系中参与算法计算"等场景下非常有用。
底层实现(coord.ts):
localToGraphPoint(x, y) { const localPoint = Point.create(x, y) return Util.transformPoint(localPoint, this.graph.matrix()) }直接应用画布当前的变换矩阵this.graph.matrix()。该矩阵由 transform.ts 中的getMatrix()提供——它读取 viewport 元素的transform属性并通过viewport.getCTM()获取矩阵,同时做了缓存(viewportTransformString变化时才重新读取)。
注意:原文档中该方法内部点版本命名为
localToGraphPoint,与localToGraph等价,只是命名上更明确地表达"只处理点"。
3.6 graphToLocal(...):画布坐标 → 画布本地坐标
graphToLocal(rect: Rectangle.RectangleLike): Rectangle graphToLocal(x: number, y: number, width: number, height: number): Rectangle graphToLocal(p: Point.PointLike): Point graphToLocal(x: number, y: number): Point将画布坐标转换为画布本地坐标,是localToGraph的逆运算。
底层实现(coord.ts):
graphToLocalPoint(x, y) { const graphPoint = Point.create(x, y) return Util.transformPoint(graphPoint, this.graph.matrix().inverse()) }与localToGraphPoint完全对称,区别仅在于使用画布矩阵的逆矩阵inverse()。
3.7 snapToGrid(...):对齐到网格
snapToGrid(p: Point.PointLike): Point snapToGrid(x: number, y: number): Point将浏览器坐标转换为画布本地坐标,并对齐到画布网格。注意文档特别注明:输入是浏览器坐标(client 坐标),输出是吸附到网格后的 local 坐标。
底层实现(coord.ts):
snapToGrid(x, y) { const p = typeof x === 'number' ? this.clientToLocalPoint(x, y) : this.clientToLocalPoint(x.x, x.y) return p.snapToGrid(this.graph.getGridSize()) }内部先执行clientToLocalPoint完成 client → local 转换,再调用Point.snapToGrid(gridSize)按当前网格大小吸附。网格大小通过graph.getGridSize()获取(见 graph.ts)。测试 coord.spec.ts 验证:网格大小为 10 时,(12, 18)吸附后横纵坐标均能被 10 整除。
四、实战场景与示例
4.1 官方演示 playground
官方文档顶部内嵌的演示代码位于 site/src/api/coord/playground/index.tsx。该演示创建了一张包含两个节点和一条边的画布,并监听mousemove事件,在鼠标移动时实时显示四种坐标系下的坐标值:
const p1 = this.graph.pageToLocal(pageX, pageY) const p2 = this.graph.localToGraph(p1) this.c.innerHTML = ` <div>Page(pageX, pageY): ${pageX} x ${pageY}</div> <div>Client(clientX, clientY): ${clientX} x ${clientY}</div> <div>Local Point: ${p1.x} x ${p1.y}</div> <div>Graph Point: ${p2.x} x ${p2.y}</div> `配套的 settings.tsx 提供了scale(0.1~2.0)和translateX/translateY(-50~50)三个滑块,通过graph.scale()与graph.translate()实时改变画布变换。你会发现:当拖动滑块改变缩放或平移后,local与graph坐标开始分离,而client坐标始终保持不变——这正是四种坐标系性质最直观的体现。
4.2 实战:点击画布空白处添加节点
这是最典型的应用:通过鼠标事件拿到client或page坐标,转换后作为节点位置。
graph.on('blank:click', ({ e }) => { const node = graph.addNode({ x: 80, // 以 local 坐标系为准 y: 80, width: 80, height: 40, label: 'new node', }) // 把新节点的中心移动到鼠标点击处(画布已缩放平移时依然准确) const p = graph.clientToLocal(e.clientX, e.clientY) node.position(p.x - node.getSize().width / 2, p.y - node.getSize().height / 2) })如果页面存在滚动条,用graph.pageToLocal(e.pageX, e.pageY)也可以达到同样效果;两者的差异正是client与page坐标系的区别。
4.3 实战:在画布外渲染跟随节点的 DOM 元素
需要把节点坐标映射到页面/DOM 层时,使用localToPage:
const node = graph.getCellById('node-1')! const pos = node.getPosition() // local 坐标 const rect = graph.localToPage(pos.x, pos.y, node.getSize().width, node.getSize().height) tooltip.style.left = `${rect.x}px` tooltip.style.top = `${rect.y}px`五、方法速查与源码索引
| 方法 | 方向 | 核心底层运算 | 实现位置 |
|---|---|---|---|
pageToLocal | page → local | 减页面偏移 + 画布矩阵逆变换 | coord.ts、coord.ts |
localToPage | local → page | 画布矩阵变换 + 加页面偏移 | coord.ts、coord.ts |
clientToLocal | client → local | 屏幕 CTM 逆变换 | coord.ts、coord.ts |
localToClient | local → client | 屏幕 CTM 变换 | coord.ts、coord.ts |
localToGraph | local → graph | 画布矩阵变换 | coord.ts、coord.ts |
graphToLocal | graph → local | 画布矩阵逆变换 | coord.ts、coord.ts |
snapToGrid | client → local(吸附网格) | client 转 local + 网格吸附 | coord.ts |
各方法的 Graph 层入口统一委托实现,见 graph.ts;CoordManager作为Graph的公开只读成员graph.coord被实例化(graph.ts),画布销毁时随之一并dispose()。全部方法的单元测试覆盖于tests/graph/coord.spec.ts,是阅读底层行为的最佳参考。
六、小结
X6 的坐标转换体系可以归纳为三个关键等式:
local → graph:应用graph.matrix();graph → client:应用stage.getScreenCTM();client → page:加上window.scrollX / window.scrollY。
文档中提供的七个方法覆盖了这四个坐标系之间的主要转换路径。实际开发中,只需记住两条判断准则:处理鼠标事件坐标(e.clientX/e.clientY)优先用clientToLocal,处理页面滚动相关的定位(e.pageX/e.pageY)用pageToLocal;需要把节点位置映射回 DOM 层时反向使用localToClient/localToPage。再结合 官方演示源码 亲手拖动缩放滑块观察数值变化,就能彻底掌握这套坐标体系。
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考