图纸内容通常按图层组织,每个图层带着一组控制显示与编辑的外观属性;与此同时,图纸坐标与屏幕坐标之间隔着若干层变换。解析器把实体的原始坐标还原到屏幕位置时,需要把这两条信息合起来看:图层决定「画不画、怎么画」,变换决定「画在哪」。
下面先说明图层属性的语义差别,再逐层展开坐标系的换算关系,接着讨论嵌套块与视口带来的变换叠加。
图层属性与状态
图层是实体的分类容器,一个实体通常只属于一个图层。图层上的属性可以分成几类:
| 属性 | 取值 | 影响范围 |
|---|---|---|
| 开关 | 开 / 关 | 是否绘制 |
| 冻结 | 冻结 / 解冻 | 是否参与重生成与相关计算 |
| 锁定 | 锁定 / 解锁 | 是否允许编辑 |
| 颜色、线型、线宽 | 具体值或随层、随块 | 未显式设置的实体所继承的外观 |
| 打印标志 | 打印 / 不打印 | 是否输出到打印结果 |
开关与冻结都会让图层不显示,但语义不同:关闭的图层不绘制,其上的实体仍可能参与图面范围统计等与可见性无关的处理;冻结的图层在重生成时被整体跳过,对处理开销的影响更明显。锁定只禁止编辑,不影响显示。外观属性方面,实体若显式设置了颜色、线型或线宽,则覆盖图层上的取值;未设置时按随层或随块的约定取值。这套结构决定了矢量路径与线宽映射中线宽与线型参数的来源。
坐标系的层次
图纸数据会在多个坐标系之间移动,常见的有:
- 世界坐标系:几何数据的存储基准,文件中的坐标多数以此表示
- 对象坐标系:部分二维实体的坐标在自身平面内给出,需结合拉伸方向换算到世界坐标系
- 用户坐标系:由用户定义的局部坐标系,通常只影响输入与显示,不改变文件中保存的坐标
- 模型空间与图纸空间:两套并存的布局,各自带独立的视图方向与比例
- 设备或屏幕坐标系:以像素为单位、原点在窗口角点的坐标系
存储到文件中的一般是世界坐标系或对象坐标系下的值,其余换算在运行时完成。把哪些换算留在存储层、哪些放在运行时,会影响文件与显示之间能否保持一致。
仿射变换与矩阵复合顺序
二维仿射变换可以用 3×3 齐次矩阵表示。在列向量约定下,平移量位于矩阵的第三列,点按 p' = M · p 变换:
// 3x3 齐次矩阵乘法,列向量约定
function mul(a, b) {
const r = new Array(9).fill(0);
for (let i = 0; i < 3; i++)
for (let j = 0; j < 3; j++)
for (let k = 0; k < 3; k++)
r[i * 3 + j] += a[i * 3 + k] * b[k * 3 + j];
return r;
}
// 先绕原点旋转 R,再按世界偏移 T 平移:M = T · R
const M = mul(T, R);
缩放、旋转与平移的复合顺序会影响结果,因为矩阵乘法不满足交换律。上例中 T · R 表示先施加旋转再平移;若写成 R · T,效果变成在旋转后的坐标系里平移,结果通常不同。块引用、视口与打印布局各自引入变换,因此实现中一般维护一个矩阵栈:进入一层就压入并与当前矩阵相乘,退出时弹出,绘制前用栈顶矩阵把几何送到设备空间。
嵌套块与变换叠加
块定义内部的实体坐标相对块基点保存。插入时给出的插入点、比例与旋转角构成一次变换,展开时块内实体的坐标按缩放、旋转、平移的顺序映射到宿主坐标系,再与插入点所在层级的变换相乘。若块内部又含有块引用,变换逐层叠加。
这里要留意基点。坐标是相对块基点还是相对原点,会改变缩放与旋转的作用中心。多数实现把块内坐标保存为相对基点的偏移,展开时先减去基点、施加缩放与旋转、再加上插入点。若直接对已平移的坐标施加缩放,整体位置会偏离预期。逐层叠加还会放大数值误差,因此嵌套层数较深时,变换需要在足够精度的数值上完成。
视口与图框的坐标映射
图纸空间里的视口是一个矩形区域,它引用模型空间中的一块视图,并带有自己的视图中心与缩放比例。视口的映射可分解为:取模型坐标、减去视图中心、乘以比例、映射到视口矩形。多个视口可以引用同一模型空间的不同区域,彼此独立。
图框通常是图纸空间坐标下的边框,并与打印比例相关联。把视口比例与打印比例分开记录,是因为同一视图可能需要在不同图幅下以不同大小输出。两者的组合决定了从模型坐标到输出纸张的换算链。
屏幕坐标与图纸坐标的往返换算
从屏幕方向往回换算,需要依次经过设备像素、视口矩形归一化、平移与缩放,到达图纸坐标。往返互换要求变换可逆,因此缩放系数不能为零。Y 轴朝向是常见的易错点:屏幕坐标原点在窗口角点且 Y 轴向下,而图纸坐标通常 Y 轴向上,换算时需要对 Y 分量取反。
// 屏幕坐标(CSS 像素)换算为图纸坐标
function screenToWorld(pt, view) {
const nx = (pt.x - view.rect.left) / view.rect.width; // 归一化到 0..1
const ny = (pt.y - view.rect.top) / view.rect.height;
return {
x: view.center.x + (nx - 0.5) * view.rect.width / view.scale,
y: view.center.y - (ny - 0.5) * view.rect.height / view.scale, // Y 轴取反
};
}
若视图带旋转角,上述近似不再成立,需要把屏幕偏移量先按负旋转角转回视图轴向,再按比例换算。涉及多视口时,还要先判断坐标落在哪个视口矩形内,再使用该视口自己的中心与比例。
小结
图层负责可见性、锁定与外观默认值,其语义差别集中在是否参与计算与是否允许编辑两处;坐标变换负责把存储坐标搬到屏幕,难点在矩阵复合顺序、Y 轴朝向与嵌套块的基点处理。两者交汇于渲染前的准备阶段:先按图层过滤实体、解析出各自的颜色与线型,再沿变换链把几何映射到设备空间。把过滤与变换分成两步,可以让图层状态变化只触发较轻的处理,而不必重建全部几何数据。