- 图形学
【免费下载链接】ezdxf
Python interface to DXF
本文以 ezdxf 官方教程 psp_viewports.rst 为骨架,结合仓库内示例脚本与源码实现,系统讲解在图纸空间(Paperspace)布局中创建和管理 VIEWPORT 视口的完整流程:从页面设置(page_setup)、视口添加与缩放比例计算,到视口方向调整、图层冻结与逐视口图层属性覆盖。读完本文,你将掌握用 ezdxf 为 R2000+ 版本 DXF 文件制作多视口图纸布局的完整实战方案,并能理解底层 DXF 数据的存储原理。
教程背景与核心示例脚本
本教程基于示例脚本 viewports_in_paperspace.py 展开。该脚本会为 DXF R12 与 R2000+ 版本各生成一个图纸文件,其模型空间内容包含:
- 三个位于 WCS(世界坐标系)XY 平面内的平面几何图形:一个红色矩形、一个绿色圆、一个青色三角形(均由 create_2d_modelspace_content 创建);
- 一个 20×20 网格、由正弦/余弦函数生成起伏曲面的 3D 网格(由 create_3d_modelspace_content 创建,高度为 3.0,范围 30×30)。
模型空间内容如下图所示(图片路径:gfx/vp-tut-msp-content.png):
重要:为什么建议使用 R2000+ 版本
原教程给出了一个重要警告:如果需要在图纸空间布局中使用页面尺寸,请使用 DXF R2000 或更高版本,因为 DXF R12 的页面尺寸导出无法正常工作!作者在 BricsCAD 中发现 R12 导出的图纸尺寸错误,在 Autodesk DWG Trueview 中发现边距错误,且未找到原因也未修复。这是图纸空间布局在 DXF 版本支持上的一个硬性边界,实战中务必注意。
页面设置(Page Setup):定义纸张本身
DXF 参考手册中关于图纸空间布局特性的文档非常匮乏,关于如何使用几乎没有任何实践说明,因此本教程中大部分信息是作者通过反复试错总结出来的假设。
Paperspace类的page_setup()方法定义了纸张本身的属性。模型空间和图纸空间的单位互不相关,甚至可以使用不同的单位制(英制、米制),但为了简单起见,官方建议两个空间使用相同的单位系统:
layout.page_setup(size=(24, 18), margins=(1, 1, 1, 1), units="inch")page_setup 完整参数签名
从源码 layout.py 可以看到page_setup()的完整签名与默认值:
def page_setup( self, size: tuple[float, float] = (297, 210), # 纸张尺寸 (宽, 高) margins: tuple[float, float, float, float] = (0, 0, 0, 0), # (上, 右, 下, 左),注意是顺时针顺序 units: str = "mm", # "mm" 或 "inch" offset: tuple[float, float] = (0, 0), # 绘图原点偏移,2D 点 rotation: int = 0, # 旋转,取值 0-3 scale: Union[int, tuple[float, float]] = 16, # 标准比例类型索引或 (分子, 分母) 元组 name: str = "ezdxf", # 纸张名称前缀 device: str = "DWG to PDF.pc3", # 设备 .pc3 配置文件或系统打印机名 ) -> None:各参数要点:
size:定义旋转模式 0 下纸张的整体尺寸。最佳实践是先在横向(landscape)模式下定义纸张范围,之后再通过
rotate参数旋转纸张。margins:四元组顺序为
(top, right, bottom, left)(顺时针)。内部会按unit_factor转换为毫米存储(英寸换算系数为 25.4)。units:仅支持
"mm"和"inch"(大小写不敏感,源码中units.startswith("inch")判定),其他值会抛出DXFValueError。rotation取值表(源码与教程一致):
取值 含义 0 不旋转 1 逆时针旋转 90 度 2 上下颠倒(180 度) 3 顺时针旋转 90 度 源码 layout.py 中会对
rotation做校验,非 0-3 的值抛出DXFValueError("valid rotation values: 0-3")。scale:反映图纸空间(paperspace)中纸张单位与绘图单位之间的关系。可以传整数(0-32 范围内的标准比例类型索引,映射关系在
const.STD_SCALES中),也可以传(分子, 分母)元组,例如(1, 50)表示 1:50。教程建议保持默认 1:1,并在图纸空间中用与纸张尺寸相同的单位绘制线条和文字。注意分子或分母为 0 会抛出DXFValueError。
page_setup 写入了哪些 DXF 数据
源码显示page_setup()会设置 PLOTSETTINGS 头部的多项属性,并把纸张名称规范化为"{name}_({width:.2f}_x_{height:.2f}_{units})"格式(如ezdxf_(24.00_x_18.00_Inches)),同时写入plot_configuration_file、四边*_margin、paper_width/paper_height(统一换算为毫米)、scale_numerator/scale_denominator、plot_paper_units(0 表示 Inches,1 表示 MM)、plot_rotation、绘图原点偏移等,最后调用reset_paper_limits()、reset_extents()、reset_viewports()重置纸张极限、范围和视口。
更简单的页面设置:Drawing.page_setup
原教程作者在写作过程中注意到:更换打印机/绘图仪和纸张尺寸会导致布局内容移位,因为所有纸张尺寸都是在没有边距的情况下定义的,也许最好把所有边距设为零。为此,作者在Drawing类上添加了辅助方法page_setup(),并提供了示例 simple_page_setup.py 演示用法。
Drawing.page_setup()位于 document.py,签名如下:
def page_setup( self, name: str = "Layout1", # 图纸空间布局名称 fmt: str = "ISO A3", # 纸张格式 landscape=True, # True 横向,False 纵向 ) -> Paperspace:该方法要求 DXF R2000 或更新版本。fmt支持的纸张格式(尺寸均为横向度量值):
| 名称 | 单位 | 宽 | 高 |
|---|---|---|---|
| ISO A0 | mm | 1189 | 841 |
| ISO A1 | mm | 841 | 594 |
| ISO A2 | mm | 594 | 420 |
| ISO A3 | mm | 420 | 297 |
| ISO A4 | mm | 297 | 210 |
| ANSI A | inch | 11 | 8.5 |
| ANSI B | inch | 17 | 11 |
| ANSI C | inch | 22 | 17 |
| ANSI D | inch | 34 | 22 |
| ANSI E | inch | 44 | 34 |
| ARCH C | inch | 24 | 18 |
| ARCH D | inch | 36 | 24 |
| ARCH E | inch | 48 | 36 |
| ARCH E1 | inch | 42 | 30 |
| Letter | inch | 11 | 8.5 |
| Legal | inch | 14 | 8.5 |
该布局使用纸张格式对应的单位作为绘图单位,无边距、无偏移、图纸空间比例为 1:1,因此更换输出设备非常容易(至少在 BricsCAD 中如此)。示例脚本中还提醒:无边距布局的绘图空间是整个纸张,但打印机/绘图仪不会打印到纸张边缘,真实文件中应始终在纸张边缘与内容之间保留安全空间,且该安全空间的大小因设备而异、无法预知,建议结合psp.dxf.paper_width和psp.dxf.paper_height自行设计。
在图纸空间中绘图
你可以像在任意布局空间一样向图纸空间添加 DXF 实体。坐标系原点(0, 0)位于画布(canvas)的左下角,画布范围等于纸张尺寸减去边距。你可以绘制超出该范围的内容,但 CAD 应用程序可能不会打印这些内容。
在 viewports_in_paperspace.py 中,作者按此规则做了如下计算并绘制边框:
layout: Paperspace = doc.layout("Layout1") # Arch C = 18 x 24 in width, height = 24, 18 m = 1 layout.page_setup(size=(width, height), margins=(m, m, m, m), units="inch") # 画布由纸张尺寸减去边距定义: canvas_width = width - 2 * m # 22 canvas_height = height - 2 * m # 16 # 画布左下角坐标为 0, 0关于 R12 的特殊处理
示例脚本同时支持 R12 导出,但 R12 走的是另一条路径page_setup_r12()(见 layout.py)。该方法会先删除所有已存在的视口,然后通过头部变量$PLIMMIN、$PLIMMAX、$PEXTMIN、$PEXTMAX设置打印范围,并缩放边距与纸张尺寸(源码中有 TODO 注释:不知道如何在 R12 中设置 inch/mm 模式)。同时注意DXF R12 只支持一个图纸空间(即一个布局),任何布局名称都返回同一个布局(见 viewports_in_paperspace.py 的注释)。
添加视口(Adding Viewports)
Viewport(DXF VIEWPORT)实体是一个指向模型空间的窗口,用于在图纸空间中按任意缩放比例和旋转角度显示模型空间的内容。VIEWPORT 实体通过工厂方法Paperspace.add_viewport()添加。
从源码 layout.py 可以看到其完整签名:
def add_viewport( self, center: UVec, # 视口在图纸空间中的中心点 size: tuple[float, float], # 视口在图纸空间中的 (宽, 高) view_center_point: UVec, # 要显示的模型空间区域中心点 view_height: float, # 模型空间中要显示的区域高度 status: int = 2, # 视口状态,默认 2 dxfattribs=None, # 附加 DXF 属性 ) -> Viewport:关键点:
center定义视口在图纸空间中的中心位置,size定义 VIEWPORT 在图纸空间中的宽度和高度;- 要显示的模型空间内容由
view_center_point和view_height定义; - 默认图层为 "VIEWPORTS"(源码中
attribs字典明确写了"layer": "VIEWPORTS"),用于后续控制视口边框显隐; status语义(源码注释):-1表示开启但在屏幕外,或因为$MAXACTVP计数超出而非激活;0表示关闭;任何大于 0 的值表示开启且激活,其中1是"活动视口",2是下一个,以此类推;dxf.id由get_next_viewport_id()自动分配,从 2 开始递增(源码 layout.py)。
添加视口后的图纸空间布局如下图所示(图片路径:gfx/vp-tut-psp-content.png):
缩放比例(Scaling Factor):由尺寸与视高的比值决定
VIEWPORT 的缩放因子不是一个显式的数值,而是由size参数中的 VIEWPORT 高度与view_height参数的比值关系决定的。
源码 viewport.py 中的get_scale()方法印证了这一计算方式:
def get_scale(self) -> float: """Returns the scaling factor from modelspace to viewport.""" msp_height = self.dxf.view_height if abs(msp_height) < 1e-12: return 0.0 vp_height = self.dxf.height return vp_height / msp_height # 视口高度 / 模型空间视高当两个值相等时,缩放比例为 1:1:
paperspace.add_viewport( center=(14.5, 2.5), size=(5, 5), view_center_point=(12.5, 7.5), view_height=5, )当view_height比 VIEWPORT 高度大 5 倍时,缩放比例为 1:5:
paperspace.add_viewport( center=(8.5, 2.5), size=(5, 5), view_center_point=(10, 5), view_height=25, )原示例 viewports_in_paperspace.py 中还演示了更多比例组合:view_height=10、视口高5得到 1:2(矩形视图);size=(0.3, 0.15)、view_height=7.5得到 1:50(缩放比例 = 7.5 / 0.15 = 50),并在每个视口下方用add_text()标注了比例文字,形成规范的"平面图 + 局部放大图"图纸布局。
另外可参考 viewport.py 的get_aspect_ratio()(返回width / height)与 viewport.py 的get_modelspace_limits()(返回模型空间中可见范围的(min_x, min_y, max_x, max_y)),这两个辅助方法可用于进一步验证视口覆盖的模型空间区域。
视口方向(View Direction):从俯视到任意视角
VIEWPORT 的默认视角是俯视(top down view,即沿 Z 轴向下看),但可以通过dxf命名空间中的view_target_point和view_direction_vector属性更改为任意视角:
vp = paperspace.add_viewport( center=(16, 10), size=(4, 4), view_center_point=(0, 0), view_height=30 ) vp.dxf.view_target_point = (40, 40, 0) vp.dxf.view_direction_vector = (-1, -1, 1)从源码 viewport.py 的 DXF 属性定义看:
view_direction_vector(组码 16,3D 点)默认值为Z_AXIS(即(0, 0, 1)),对应俯视;view_target_point(组码 17,3D 点)默认值为NULLVEC((0, 0, 0))。
get_view_direction()方法会返回归一化的视角向量,is_top_view属性判断是否仍为俯视(与 Z 轴接近即为俯视,见 viewport.py)。示例中设置view_target_point=(40, 40, 0)、view_direction_vector=(-1, -1, 1)后,视口将以斜向 45° 视角展示 3D 网格,即"Viewport to 3D Mesh"视图。注意:get_transformation_matrix()(模型空间到图纸空间变换矩阵)目前仅支持俯视视口(见 viewport.py 注释)。
视口边框(Viewport Frame):用图层控制显隐
VIEWPORT 的边框线(borderlines)默认显示在图纸空间中,但VIEWPORT 实体本身没有控制边框显隐的属性。边框的可见性由分配给 VIEWPORT 实体的图层控制——在 ezdxf 中默认图层是 "VIEWPORTS"(源码见 layout.py 的"layer": "VIEWPORTS"注释:"use separated layer to turn off for plotting")。
关闭该图层即可隐藏该图层上所有 VIEWPORT 实体的边框,但图层 "VIEWPORTS" 需要由库的使用者自行创建:
vp_layer = doc.layers.add("VIEWPORTS") vp_layer.off()示例 viewports_in_paperspace.py 中的写法更稳健:先检查图层是否存在,不存在才创建,然后统一关闭:
if "VIEWPORTS" not in doc.layers: vp_layer = doc.layers.add("VIEWPORTS") else: vp_layer = doc.layers.get("VIEWPORTS") vp_layer.off()源码注释还指出VIEWPORT 图层不是固定的:可以通过add_viewport(..., dxfattribs={"layer": "MyViewportLayer"})指定自定义视口图层。
冻结图层(Freeze Layers):每个视口独立控制可见性
每个 VIEWPORT 可以有独立的冻结图层,冻结的图层在该视口内不可见。要冻结视口中的图层,只需把冻结图层名称以列表(list-like 对象)形式赋值给 VIEWPORT 实体的frozen_layers属性:
vp.frozen_layers = ["Layer0", "Layer1"]从源码 viewport.py 看,frozen_layers是列表属性,并配套提供了更精细的操作方法:
freeze(layer_name):冻结指定图层;thaw(layer_name):解冻指定图层;is_frozen(layer_name):查询指定图层是否已冻结。
图层名比较通过validator.make_table_key()统一键化,因此大小写不敏感(与所有表名规则一致)。
重要:关于不存在的图层名
原教程特别提示:AutoCAD 和 BricsCAD 不会因图层名没有对应的图层表条目而崩溃,且图层名称与所有表名一样不区分大小写。源码 viewport.py 的post_load_hook印证了这一点:读取文件时冻结图层以句柄形式存储(组码 331),加载后通过实体数据库把句柄解析回图层名,遇到不存在的图层(KeyError)会被静默忽略(pass),不会报错。
覆盖图层属性(Override Layer Properties):逐视口的图层样式覆盖
每个 VIEWPORT 可以单独覆盖图层属性。这些覆盖存储在Layer实体中,并通过 VIEWPORT 的句柄(handle)引用。该流程略为复杂,示例文件 viewports_override_layer_attributes.py 有完整演示,核心四步:
- 获取
Layer对象; - 从图层获取
LayerOverrides对象; - 覆盖该 VIEWPORT 的属性;
- 提交更改。
layer = doc.layers.get("Layer0") override = layer.get_vp_overrides() override.set_linetype(vp.dxf.handle, "DASHED") override.commit()LayerOverrides 源码级解析
LayerOverrides类定义于 layer.py,通过Layer.get_vp_overrides()(见 layer.py)获取。支持的属性覆盖包括:
- ACI 颜色(
set_color(vp_handle, value),仅接受合法的图层颜色索引) - 真彩色(
set_rgb(vp_handle, value),RGB 三元组,传None可移除真彩色设置) - 透明度(
set_transparency(vp_handle, value),0.0 完全不透明,1.0 完全透明) - 线型(
set_linetype(vp_handle, value),要求该线型存在于 LTYPE 表中,否则抛出ValueError) - 线宽(
set_lineweight(vp_handle, value),要求是合法的图层线宽值)
对应的方法均有配套的get_*()读取方法。关键点是 layer.py 中commit()的注释:"Without a commit() all changes are lost!"(不调用 commit() 所有更改都会丢失!)——覆盖数据在内存中累积,只有commit()才会真正写回Layer实体的扩展数据。此外还有has_overrides()(查询某视口是否存在覆盖)和discard(vp_handle=None)(丢弃指定视口或全部视口的覆盖)。
示例脚本 viewports_override_layer_attributes.py 分别演示了override_aci(逐视口设置 ACI 色)、override_rgb(逐视口设置 RGB 真彩色)、override_ltype(逐视口设置线型,如 DASHED2/DOT2)和override_lw(逐视口设置线宽,如 13-140 等标准线宽值)四种覆盖方式,并把$LWDISPLAY头部变量置 1 以便在 DXF 查看器中显示线宽。仓库中还有对应的单元测试 test_141_layer_vp_override.py 可作参考。
完整运行示例与配套工具
把上述所有元素组合起来,运行 viewports_in_paperspace.py:
python examples/viewports_in_paperspace.py脚本的main()会依次为 R12、R2000、R2007 三个版本生成viewports_in_paperspace_{dxfversion}.dxf文件(默认输出到~/Desktop/Outbox,目录不存在时输出到当前目录)。R2000/R2007 文件中包含矩形(1:2)、圆(1:5)、三角形(1:1)、总览视图(1:1)、1:50 微缩视口以及 3D 网格斜视角视口等多个视口。
需要一提的是,示例最后调用了 document.py 的set_modelspace_vport(height, center)方法(如 viewports_override_layer_attributes.py 中的doc.set_modelspace_vport(60, (50, 30))),它用于设置 CAD 应用打开文件时模型空间的初始视图/缩放位置,替换当前的"*Active"视口配置并重置坐标系为 WCS——这是让用户打开文件后直接看到目标内容的实用技巧。
小结
本文完整覆盖了 ezdxf 图纸空间视口布局的六大技术要点:
- 页面设置:
page_setup()(及辅助的Drawing.page_setup())定义纸张尺寸、边距、单位、旋转与比例,注意margins为(上, 右, 下, 左)顺时针顺序; - 图纸空间绘图:原点在画布左下角,画布 = 纸张 − 边距,R12 请改用
page_setup_r12(); - 添加视口:
add_viewport(center, size, view_center_point, view_height, status),默认图层 "VIEWPORTS"; - 缩放比例:
视口高度 / view_height(源码get_scale()直接印证),相等即 1:1; - 方向与边框:默认俯视,通过
view_target_point/view_direction_vector改变视角,通过关闭 "VIEWPORTS" 图层隐藏边框; - 图层冻结与属性覆盖:
frozen_layers逐视口冻结图层,LayerOverrides逐视口覆盖 ACI 颜色、真彩色、透明度、线型与线宽,记得调用commit()。
若需在实战中进一步验证视口实体的序列化与缩放行为,可阅读测试文件 test_211_viewport.py(含 R12 与 R2000+ 的 VIEWPORT 实体读写测试),以及图层概念的官方文档 layers.rst。
- 图形学
【免费下载链接】ezdxf
Python interface to DXF
相关推荐
ezdxf Paperspace 布局实战:页面设置、视口(Viewport)与打印出图完整指南
ezdxf Paperspace 布局实战:页面设置、视口(Viewport)与打印出图完整指南 导读 Paperspace(图纸空间)是 ezdxf 中组织模
图形学ezdxf 图层表内部结构与视口图层属性覆盖机制深度解析
ezdxf 图层表内部结构与视口图层属性覆盖机制深度解析 本文基于 ezdxf 仓库的 图层表内部结构文档 https://link.gitcode.com/i
图形学ezdxf VIEWPORT 实体完全指南:布局空间视口的创建、属性体系与源码级原理解析
ezdxf VIEWPORT 实体完全指南:布局空间视口的创建、属性体系与源码级原理解析 导读 本文以 docs/source/dxfentities/view
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考