在浏览器里查看 DWG,滚轮放大和拖动平移是最常用的两个操作。真正接入业务页面时,细节问题很快就会出现:鼠标指着一个节点,放大后节点为什么跑了?图纸明明没有修改,状态栏坐标为什么一直变化?换到高分屏后,点击位置为什么偏了?
这些问题都与同一件事有关:图纸坐标怎样变成屏幕上的位置,又怎样从鼠标位置换算回图纸坐标。
上一期讲了字体资源和文字显示。本期继续使用 UDE(UTUM Data Engine)公开 Web 示例,拆开缩放、平移和坐标换算。理解这套关系后,状态栏坐标、对象点选和后续业务标记就有了共同的定位依据。
本文以UDE DWG SDK v0.4.0 FREE 的有序查看入口为例。下面的worldPoint()、renderer.setView()等属于随包提供的 JavaScript 查看器代码;接入其他渲染器时,需要按对应的视图变换实现。
一、先分清三种坐标
鼠标在屏幕上移动时,网页事件提供的是位置,图纸中的实体也有自己的位置。两者需要换算。
| 坐标或尺寸 | 本文中的含义 | 用在哪里 |
|---|---|---|
图纸坐标(x, y) | 查看器显示数据使用的二维坐标 | 定位图形、查询对象 |
画布内位置(sx, sy) | 相对画布左上角的 CSS 像素位置 | 鼠标交互和视图变换 |
| 画布像素尺寸 | Canvas 实际分配的像素宽高 | WebGL 绘制,适配高分屏 |
浏览器的event.clientX、event.clientY相对当前浏览器视口。画布旁边如果有导航栏、图层面板或页面留白,就需要先减去画布自身的位置:
constrect=elements.drawing.getBoundingClientRect();constsx=event.clientX-rect.left;constsy=event.clientY-rect.top;这里得到的sx、sy仍然是CSS 像素。例如,鼠标在画布右侧 500 像素、下方 100 像素处,对应的就是(500, 100)。
图 1:坐标关系示意,用于解释公式;不是 DWG 运行截图。本文讨论的是示例的二维视图变换。
二、图纸坐标怎样映射到画布
这个查看器用三个值描述当前视图:
scale:每一个图纸单位对应多少个 CSS 像素。offsetX:图纸原点在画布上的水平位置。offsetY:图纸原点在画布上的垂直位置。
先记住这一组对应关系:
sx = x × scale + offsetX sy = -y × scale + offsetY这里的负号很关键。示例中的图纸 Y 轴向上,网页画布的 Y 轴向下,所以绘制时需要反转 Y 的方向。
反过来,从鼠标位置计算图纸坐标:
x = (sx - offsetX) / scale y = -(sy - offsetY) / scale公开示例中的worldPoint()正是这个过程:
functionworldPoint(event){constrect=elements.drawing.getBoundingClientRect();constx=event.clientX-rect.left;consty=event.clientY-rect.top;return{screenX:x,screenY:y,x:(x-renderer.offsetX)/renderer.scale,y:-(y-renderer.offsetY)/renderer.scale,};}这段函数同时返回画布位置和图纸坐标。前者用来确定缩放的中心,后者可以用于状态栏显示和对象查询。
用一组便于手算的数值检查:图纸点为(100, 50),scale = 2,偏移为(300, 200)。它在画布中的位置就是:
sx = 100 × 2 + 300 = 500 sy = -50 × 2 + 200 = 100将(500, 100)代回反向公式,得到的仍是(100, 50)。这组数值是教学示例,不代表某张 DWG 的实际坐标。
三、拖动平移:改变偏移量
鼠标向右拖动 60 个 CSS 像素,图形也应该向右移动 60 个 CSS 像素。最直接的做法是把鼠标位移加到视图偏移上。
下面摘自示例拖动处理的核心部分,drag保存上一次事件的位置:
renderer.setView(renderer.scale,renderer.offsetX+event.clientX-drag.x,renderer.offsetY+event.clientY-drag.y,);drag.x=event.clientX;drag.y=event.clientY;scale保持原值,offsetX、offsetY随拖动变化。图纸中的端点、圆心和文字插入点仍使用原来的坐标。
有序查看器支持鼠标左键或中键按下后拖动,并通过setPointerCapture()接收后续指针事件。松开时,如果从按下位置起的位移小于 3 个 CSS 像素,示例会进入对象点选处理。
因此,鼠标停在相同屏幕位置时,平移后状态栏可能显示另一个图纸坐标:这个位置下面已经换成了图纸的另一处。若要观察同一个节点,应让鼠标跟随那个节点移动。
四、滚轮缩放:让鼠标下面的点保持原位
仅修改scale,图形会围绕当前图纸原点的屏幕位置放大。原点如果在画布之外,鼠标指着的局部就可能迅速移出视野。
更符合查看习惯的行为是:缩放前后,鼠标下面的同一个图纸点保持在同一个画布位置。
要做到这一点,需要一起更新比例和偏移量。设新旧比例之比为factor:
factor = newScale / oldScale newOffsetX = sx - (sx - oldOffsetX) × factor newOffsetY = sy - (sy - oldOffsetY) × factor公开示例的滚轮事件采用这组公式:
elements.drawing.addEventListener('wheel',(event)=>{if(!renderer)return;event.preventDefault();constpoint=worldPoint(event);constnext=Math.max(0.000001,Math.min(1000000,renderer.scale*(event.deltaY>0?0.9:1.1)));constfactor=next/renderer.scale;autoFit=false;renderer.setView(next,point.screenX-(point.screenX-renderer.offsetX)*factor,point.screenY-(point.screenY-renderer.offsetY)*factor,);redrawHighlight();updateZoom();},{passive:false});worldPoint(event)在修改视图之前执行,以取得旧视图下的鼠标位置。passive: false让事件能够调用preventDefault(),避免操作画布时同时触发页面滚动。
继续用前面的图纸点(100, 50)。把scale从 2 改成 4,鼠标保持在(500, 100),就能算出:
factor = 4 / 2 = 2 newOffsetX = 500 - (500 - 300) × 2 = 100 newOffsetY = 100 - (100 - 200) × 2 = 300 缩放后再计算这个图纸点: sx = 100 × 4 + 100 = 500 sy = -50 × 4 + 300 = 100周围的图形放大了,但这个点的位置保持不变。
图 2:为了方便计算,示意图使用 2 倍放大。公开示例每次滚轮事件按方向乘以 1.1 或 0.9;这里的 2 倍不是它的默认滚轮步长。
五、适合窗口、窗口变化和高分屏
初次打开图纸时,查看器会根据图形包围盒和画布可用尺寸,计算可以容纳整张图纸的比例,再将图形居中:
scale = min( (画布宽度 - 左右留白) / 图形宽度, (画布高度 - 上下留白) / 图形高度 )对应的示例方法是renderer.fit(),默认每边预留 50 个 CSS 像素。这个计算针对有效、宽高均大于零的图形包围盒,运行时还会限制最小比例。
当图层或属性面板展开、浏览器窗口改变大小时,可用画布尺寸也会变化。示例通过ResizeObserver处理:自动适配状态下重新执行fit();用户已经拖动或缩放后,则在保持比例的同时,让原来的视图中心继续位于新画布中心。点击“适合窗口”可以重新回到自动适配状态。
高分屏还多了一层换算。假设画布的 CSS 尺寸为1000 × 600,devicePixelRatio = 2,实际绘图缓冲区就可能是2000 × 1200。
在本示例中,鼠标事件和视图参数统一使用 CSS 像素,渲染器在提交给 WebGL 时,再把比例和偏移乘以像素比。因此,前面的worldPoint()不需要再把鼠标位置乘以devicePixelRatio。重复乘一次,就会让点选与画面错位。
如果换用其他渲染器,应先确认它的相机、偏移和鼠标输入使用哪一种坐标约定,再接入换算。
六、坐标、单位和“100%”分别表示什么
缩放比例决定怎样看图,图纸单位决定怎样解释数据。两者需要分别处理。
例如,一段线的坐标差是1000,图纸约定以毫米绘制时可以解释为 1000 mm;图纸约定以米绘制时,它代表另一种实际长度。仅凭画面上的线段长度,或者状态栏的一个坐标值,无法确定工程单位。
这个有序查看器把renderer.scale × 100显示为缩放百分比。因此,100%表示每一个显示数据单位对应一个 CSS 像素,不能直接解释为工程图的 1:1 打印比例。
还有两个接入细节:
- 状态栏用
toFixed(2)显示坐标,表示保留两位小数供阅读;不要拿这段显示文本代替后续计算使用的数值。 - 前文的反向公式得到查看器显示数据中的二维坐标。如果业务需要转换到块的局部坐标、UCS 或其他业务坐标,还要继续处理对应的变换关系。
对象点选也依赖同一套坐标。第三期使用的候选对象查询中,5 / renderer.scale把约 5 个 CSS 像素的选取容差换算成图纸单位,使不同缩放下的点击范围更接近一致;这仍需要结合候选对象和命中逻辑判断最终选择。
七、在公开 Web 示例里操作一次
以下命令从 Windows PowerShell 执行,使用公开包中的 Python 后端:
cd./UDE-SDK./examples/web/run.ps1-Language python-Port 8082-OrderedPreview需要 64 位 Python 3.11+ 和 Microsoft Visual C++ x64 Runtime。打开http://127.0.0.1:8082/?ordered=1,上传自己有权使用的 DWG。免费版单个原始 DWG 上限为3,000,000 字节。
使用有序查看入口,是为了让页面行为与本文讲解的ordered_viewer.mjs保持一致。Python、Java、C# 后端生成显示结果后,本文这些缩放和平移操作由同一套前端处理。
可以按下面顺序观察:
- 把鼠标移到一个容易辨认的节点,查看底部 X、Y 坐标。
- 保持鼠标位置,滚轮放大,观察同一个节点是否保持在鼠标附近。
- 拖动平移,观察图形随指针移动,缩放比例保持原值。
- 展开图层或属性面板,观察可用画布变化后的视图位置。
- 点击“适合窗口”,重新显示整张图纸。
缩放和平移更新的是当前查看器的视图。已加载的图纸可以直接在浏览器重绘,无须为每次交互重新上传 DWG。
八、接入时常见的几个问题
| 现象 | 优先检查 |
|---|---|
| 放大后鼠标指着的节点移走了 | 是否在修改比例的同时,按鼠标位置更新了偏移 |
| 点选位置整体偏向一侧 | 是否减去了画布的rect.left、rect.top |
| Y 坐标或上下移动方向相反 | 正向和反向变换是否使用了相同的 Y 轴约定 |
| 高分屏上点击位置偏了 | 是否混用了 CSS 像素和实际画布像素,或重复乘了像素比 |
| 面板一展开,图纸就重新跳回全景 | 用户操作后是否仍在无条件执行fit() |
| “100%”时的尺寸和打印纸张不一致 | 是否把查看器比例误当成了物理尺寸或出图比例 |
示例源码与下一期
本文对应的公开源码:
examples/web/README.md:环境、启动参数和有序查看入口。ordered_viewer.mjs:鼠标坐标换算、拖动、滚轮、点选和窗口变化。ordered_compact_webgl.mjs:视图参数、适合窗口和高分屏绘制。
下一期继续沿着这套示例讲:从上传 DWG 到浏览器显示,后端和前端之间传递了什么?把render.bin、entities.bin和可选的render.ord放回完整的数据流程中。
本文由 UDE 团队结合公开示例整理。示例版本为 UDE DWG SDK v0.4.0 FREE、Windows x64。