- 图形学
【免费下载链接】ezdxf
Python interface to DXF
本文基于 docs/source/tasks/select.rst 与 实体选择教程,并结合 src/ezdxf/select.py 源码与 tests/test_05_tools/test_543_select.py 测试用例整理而成。
一、概述:ezdxf.select模块是什么
ezdxf.select是 ezdxf 提供的实体选择工具模块,它允许用户基于各种二维形状——矩形窗口(Window)、圆形(Circle)、多边形(Polygon)、栅栏折线(Fence)以及单个点——从任意 DXF 实体集合中筛选实体。该模块自 ezdxf 1.4 起还提供了基于 R-Tree 的平面空间搜索索引PlanarSearchIndex,用于在大规模绘图中快速预筛候选实体。
与 CAD 应用中的"选中/框选/交叉选择"不同,本模块的选择判定不是基于实体的真实几何轮廓,而是基于实体的二维包围盒(bounding box)。因此:
bbox_inside与 CAD 的"内部选择(inside selection)"类似——要求实体的包围盒完全落在选择形状内部;bbox_outside与 CAD 的"外部选择(outside selection)"类似——要求实体的包围盒完全位于选择形状之外;bbox_overlap与 CAD 的"交叉选择(crossing selection)"类似,但有一个关键区别:只要实体的包围盒与选择形状发生重叠,即使实体的实际几何完全在选择形状外部(例如边框多段线横跨窗口边界),也会被选中。这一点与 CAD 交叉选择的行为不同,使用时应留意。
所有选择函数都接受任意可迭代的 DXF 实体集合作为输入(模型空间、图纸空间布局、块布局、EntityQuery查询结果、任何实现了__iter__的实体集合均可),并返回一个 ezdxf.query.EntityQuery 容器。该容器自身还提供基于实体类型和 DXF 属性的进一步筛选能力(详见下文"链式筛选"一节)。
适用场景
- 从模型空间中按窗口快速提取一批实体用于批量处理(如批量改图层、批量导出);
- 按圆形区域选取某点附近的实体;
- 按多边形(如地块、区域边界)选取落在其中的图元;
- 用栅栏折线选取与其路径相交的实体;
- 在大图纸中先用
PlanarSearchIndex缩小搜索范围,再做精确筛选。
二、为什么基于包围盒(Bounding Box)
包围盒方案的核心动机是性能优先于绝对精度。计算两个轴对齐矩形(AABB)之间是否相交/包含,仅需几次坐标比较,而精确的几何求交(曲线、样条、文本轮廓)代价高昂。因此select模块先用包围盒做粗筛,把复杂几何判定留给后续更精细的处理。
需要特别注意的是,文本类实体与包含曲线的实体的包围盒并不精确。ezdxf.bbox模块对样条、贝塞尔曲线等采用离散化/控制点近似,对文本实体使用字体度量近似,因此基于包围盒的选择结果可能与肉眼看到的几何轮廓存在偏差。关于精度限制的详细说明,请阅读 src/ezdxf/bbox.py 的文档。
这一点在设计上是有意为之。测试文件 tests/test_05_tools/test_543_select.py 的类注释明确写道:"The selection functions are testing only the bounding boxes of entities. This is a design choice: performance and simplicity over accuracy"(选择函数只测试实体的包围盒——这是性能与简单性优先于精度的设计选择)。例如TestBoundingBoxVsWindow.test_overlaps_all中,窗口(0,0)-(2,2)只与 CIRCLE 和 LWPOLYLINE 的包围盒重叠(并未触及几何本身),但bbox_overlap仍会选中全部 4 个实体。
实体来源
选择函数的entities参数可以是:
- 模型空间
doc.modelspace(); - 任意图纸空间布局;
- 块布局(block layout);
EntityQuery查询结果容器;- 任何可迭代的 DXF 实体集合。
源码 src/ezdxf/select.py#L219-L242 中的select_by_bbox()展示了统一处理逻辑:对每个实体调用bbox.extents((entity,), fast=True, cache=cache)计算包围盒,若包围盒无数据(has_data为假,例如空 HATCH)则跳过,随后将二维包围盒交给判定函数测试。
三、选择形状(Selection Shapes)
三种选择形状全部继承自抽象基类SelectionShape(src/ezdxf/select.py#L30-L43),该基类约定了三个抽象方法:
is_inside_bbox(entity_bbox):实体包围盒是否在选择形状内部;is_outside_bbox(entity_bbox):实体包围盒是否在选择形状外部;is_overlapping_bbox(entity_bbox):实体包围盒是否与选择形状重叠。
所有选择形状都将实体投影到xy 平面上进行判定(忽略 z 轴)。
3.1 Window(矩形窗口)
Window(p1, p2)以两个对角点定义一个轴对齐矩形窗口:
from ezdxf import select window = select.Window((0, 0), (10, 10))内部实现(src/ezdxf/select.py#L46-L68):
- 构造函数用两个角点构造
BoundingBox2d; is_inside_bbox:调用BoundingBox2d.contains()判断实体包围盒是否完全被窗口包含;is_outside_bbox:not self._bbox.has_overlap(entity_bbox),即两者完全不相交;is_overlapping_bbox:has_overlap()判断两者是否相交(包括边界相切)。
3.2 Circle(圆形)
Circle(center, radius)以圆心和半径定义一个圆形选择区域:
circle = select.Circle((150, 105), radius=60)内部实现(src/ezdxf/select.py#L71-L103):
- 构造时同时维护圆心
Vec2与圆的外接包围盒_bbox(用于快速排除); is_inside_bbox:要求实体包围盒的全部 4 个角点到圆心的距离 ≤ 半径;is_overlapping_bbox:先做包围盒相交粗筛,再检查任一实体包围盒角点是否在圆内;若 4 个角点都在圆外,则检查包围盒中心是否在圆内(处理圆完全"吞入"包围盒但角点在外的情况)。
对圆形而言,判定是基于真实圆形区域进行的("the selection tests are carried out on the real circular area"),但被测试对象仍是实体的包围盒——例如一条包围盒与圆相交但线段本身不与圆相交的 LINE,仍可能被bbox_overlap选中,测试用例TestBoundingBoxVsCircle.test_is_overlapping_line验证了这一行为。
3.3 Polygon(多边形)
Polygon(vertices)以一组顶点定义一个任意闭合多边形选择区域:
polygon = select.Polygon([(110, 168), (110, 107), (316, 107), (316, 243), (236, 243)])内部实现(src/ezdxf/select.py#L106-L165):
- 顶点会被转换为
Vec2列表;至少需要 3 个顶点,否则抛出ValueError("3 or more vertices required"); - 若首尾顶点重合(闭合多边形写法),会自动弹出最后一个重复顶点(
v.pop())将其视为开式顶点列表; is_inside_bbox:先做包围盒相交粗筛;再用is_point_in_polygon_2d逐一检查实体包围盒 4 个角点,任何角点在多边形外即判定不在内部;此外对凹多边形增加了补充测试——检查是否有多边形顶点严格落在实体包围盒内部,避免凹多边形漏判;is_overlapping_bbox:检查任一实体包围盒角点在多边形内(或边界上),或包围盒边与多边形边相交(通过CohenSutherlandLineClipping2d线段裁剪实现,见 src/ezdxf/math/clipping.py#L853)。
限制:复杂凹多边形可能无法 100% 正确工作(文档与源码均明确标注此警告)。测试用例test_circle_is_not_inside_concave_polygon验证了凹多边形(顶点[(-10,-10), (10,-10), (10,10), (0,0), (-10,10)])场景下,CIRCLE 的包围盒虽然整体在多边形内且各边角点均在多边形内,但多边形某些边穿过了实体包围盒,因此bbox_inside与bbox_outside均不成立——这正是"不保证 100% 正确"的典型例子。
四、六大选择函数详解
所有函数均返回EntityQuery容器,并接受可选的cache: bbox.Cache | None参数用于复用包围盒计算结果(详见下文"性能优化")。
4.1bbox_inside(shape, entities, *, cache=None)
选择包围盒完全位于选择形状内部的实体。对应shape.is_inside_bbox。
import ezdxf from ezdxf import select doc = ezdxf.readfile("your.dxf") msp = doc.modelspace() # 定义选择窗口(两个对角顶点) window = select.Window((0, 0), (10, 10)) # 选择模型空间中窗口内的实体 selected_entities = select.bbox_inside(window, msp) # 遍历选中实体 for entity in selected_entities: print(entity)基于 实体选择教程 的基准图(base.dxf),用窗口(150,105)-(280,240)执行bbox_inside的示例输出:
CIRCLE(#9D) LWPOLYLINE(#9E)即只有完整落入窗口内的 CIRCLE 与 LWPOLYLINE 被选中。
4.2bbox_outside(shape, entities, *, cache=None)
选择包围盒完全位于选择形状之外的实体。对应shape.is_outside_bbox。
window = select.Window((185, 105), (245, 240)) for entity in select.bbox_outside(window, msp): print(str(entity))示例输出(窗口收窄后):
TEXT(#9F) SPLINE(#A0) LINE(#A1)注意:bbox_outside的判定是"完全不重叠",而非"几何上在外"。若实体的包围盒与窗口相交(即使几何在窗口外),该实体不会被bbox_outside选中。
4.3bbox_overlap(shape, entities, *, cache=None)
选择包围盒与选择形状重叠的实体。对应shape.is_overlapping_bbox。这是最接近 CAD"交叉选择"的函数,但如本文开头所述,只要包围盒重叠即入选,与真实几何是否相交无关。
window = select.Window((150, 105), (280, 240)) for entity in select.bbox_overlap(window, msp): print(str(entity))示例输出(比bbox_inside多出 TEXT、SPLINE、LINE 和另一条 LWPOLYLINE):
CIRCLE(#9D) LWPOLYLINE(#9E) TEXT(#9F) SPLINE(#A0) LINE(#A1) LWPOLYLINE(#A2)4.4bbox_chained(start, entities, *, cache=None)
从指定的起始实体出发,选择所有与它直接或间接通过重叠包围盒相连的实体(链式蔓延选择)。例如选取一条 LINE 后,所有与它"沾边"的实体都会被连带选中:
line = msp.query("LINE").first for entity in select.bbox_chained(line, msp): print(str(entity))示例输出:
LINE(#A1) CIRCLE(#9D) LWPOLYLINE(#9E) SPLINE(#A0)源码实现(src/ezdxf/select.py#L308-L344)采用迭代蔓延算法:维护已选集合,反复扫描候选列表,一旦发现某个候选实体的包围盒与任一已选实体的包围盒重叠即加入并触发新一轮扫描,直到不再有新增为止。
警告:当前实现的时间复杂度为O(n²),对超大实体集合请谨慎使用。测试用例test_all_entities_chained_by_bounding_boxes用 10 条首尾重叠的 LINE 验证了链式传播(选中全部 10 条)。
4.5bbox_crosses_fence(vertices, entities, *, cache=None)
选择包围盒与一条开放折线(栅栏)相交的实体。栅栏与多边形选择不同——它是开放的折线路径,不构成闭合区域:
for entity in select.bbox_crosses_fence([(83, 101), (186, 193), (300, 107)], msp): print(str(entity))示例输出:
CIRCLE(#9D) LWPOLYLINE(#9E) SPLINE(#A0) LINE(#A1)实现细节(src/ezdxf/select.py#L245-L285):
- 至少需要 2 个顶点,否则抛出
ValueError("2 or more vertices required"); - 判定逻辑:先检查实体包围盒与栅栏整体包围盒是否重叠;再检查任一栅栏顶点是否落在实体包围盒内;若都不满足,则用
CohenSutherlandLineClipping2d判断栅栏线段是否与实体包围盒相交; - 单个点(POINT)按定义无法被栅栏选中——因为 POINT 的包围盒退化为一个点,而代码对"extmin 与 extmax 重合(即点包围盒)"的情况直接返回
False(if extmin.isclose(extmax): return False)。
测试用例还验证了:闭合的栅栏不等效于多边形选择——test_select_nothing_inside_a_closed_fence中,闭合栅栏只经过实体包围盒外部,未选中任何实体。另外test_select_by_touching_bbox_corner表明,栅栏只需触碰实体包围盒的一个角点即可选中该实体,即使并未穿过实体几何本身(文档注释明确说明 "The polyline does not cross the entity geometry itself!")。
4.6point_in_bbox(location, entities, *, cache=None)
选择指定点位于其包围盒内部的实体:
for entity in select.point_in_bbox((264, 140), msp): print(str(entity))示例输出:
LWPOLYLINE(#9E) SPLINE(#A0)实现非常直接(src/ezdxf/select.py#L288-L305):将点转换为Vec2后,对每个实体调用entity_bbox.inside(point)。注意判定对象是包围盒而非真实几何——点落在某实体包围盒内,不代表落在该实体几何上。
五、链式筛选:返回值 EntityQuery 的进一步使用
选择函数返回的 EntityQuery 不仅是一个可迭代容器(支持len()、索引、切片),还支持:
- 按实体类型与 DXF 属性二次筛选:例如
selection.query("LINE[layer=='walls']"); - 批量读写 DXF 属性:
selection.layer = "construction"会将所有选中实体的图层统一改为construction(__setitem__支持按属性名批量设置,见 query.py#L156-L161); - 丢弃/删除属性:
del selection["layer"]; - 清除已销毁实体:
selection.purge()。
典型组合用法:先用bbox_overlap按空间位置粗选,再用EntityQuery的查询字符串精筛类型和属性,实现"空间 + 属性"两级筛选:
selected = select.bbox_overlap(window, msp) lines_on_layer = selected.query("LINE[layer=='walls']")六、性能优化:复用包围盒缓存(bbox.Cache)
所有选择函数都接受cache关键字参数。当同一批实体需要执行多次选择(不同窗口/形状)时,复用缓存可避免反复计算包围盒:
from ezdxf import bbox, select cache = bbox.Cache() for corner1, corner2 in windows: window = select.Window(corner1, corner2) selected = select.bbox_inside(window, msp, cache=cache) # 复用缓存缓存机制定义在 src/ezdxf/bbox.py#L16-L89:以实体的 DXF handle 为键存储包围盒,并统计hits与misses命中次数;HATCH 实体因包含多个不可区分的边界路径图元而不参与缓存(返回None)。若实体被修改,可用cache.invalidate(entities)使缓存失效。
select_by_bbox内部使用fast=True计算包围盒——对贝塞尔曲线等基于控制点近似,可能得到略大的包围盒(见 bbox.py#L92-L124 的multi_recursive文档)。
七、平面空间搜索索引:PlanarSearchIndex(1.4 新增)
对于超大规模图纸,逐个计算包围盒并测试的选择方式可能成为瓶颈。PlanarSearchIndex(src/ezdxf/select.py#L347-L450)为 DXF 实体建立基于 R-Tree 的二维空间搜索索引(底层为 src/ezdxf/math/rtree.py#L139 的RTree),可用于预先缩小候选实体范围。
7.1 索引的工作原理
- 索引基于实体的包围盒,索引的是包围盒的角点(POINT 与 LINE 除外);
- POINT 索引其坐标点本身,LINE 索引其起点与终点;
- 索引在构造时一次性建立,构建后不可扩展;
- 只能找到至少有一个索引点位于搜索区域内的实体。实体包围盒与搜索区域重叠、但没有任何角点落入搜索区域时,将不会被找到(例如圆心在搜索区域内、但 4 个包围盒角点都在区域外的圆);
- 检测行为可通过重写
detection_points方法定制。
7.2 基本用法
import ezdxf from ezdxf import select doc = ezdxf.readfile("large.dxf") msp = doc.modelspace() # 建立索引(可选传 cache 复用包围盒缓存) index = select.PlanarSearchIndex(msp) # 查找检测点落在圆形区域内的实体 hits = index.detection_point_in_circle(center=(100, 100), radius=50) # 查找检测点落在矩形区域内的实体(含边界) hits = index.detection_point_in_rect((0, 0), (200, 200))7.3 三个核心方法
| 方法 | 功能 |
|---|---|
detection_points(entity) | 返回给定实体的检测点序列(xy 平面投影)。默认实现返回包围盒 4 个角点;POINT 返回其坐标,LINE 返回起点与终点。可重写此方法返回更精细的检测点,例如 LWPOLYLINE/POLYLINE 的顶点、块引用的等距栅格点 |
detection_point_in_circle(center, radius) | 返回检测点位于以center为圆心、radius为半径的圆内的所有实体 |
detection_point_in_rect(p1, p2) | 返回检测点位于两个角点定义的矩形内(含边界)的所有实体 |
7.4 与 bbox.Cache 的配合
索引构造时若未提供cache,会自行创建一个新的bbox.Cache,可通过公开属性index.cache访问。文档建议:由于该索引常与本模块其他选择工具搭配使用,应维护同一个包围盒缓存,避免频繁重算包围盒的开销。典型流程:
cache = bbox.Cache() index = select.PlanarSearchIndex(msp, cache=cache) # 索引构建使用缓存 candidates = index.detection_point_in_rect(p1, p2) # 粗筛 selected = select.bbox_overlap(window, candidates, cache=cache) # 精筛复用同一缓存测试用例TestPlanarSearchIndex验证了:半径为 1 的圆形搜索只能命中 POINT(CIRCLE 因无角点落入而未命中),半径为 8 时全部 4 个实体命中。
八、源码实现要点速览
| 关注点 | 位置 | 说明 |
|---|---|---|
| 选择函数入口 | src/ezdxf/select.py#L168-L216 | bbox_inside/bbox_outside/bbox_overlap均为select_by_bbox的薄封装 |
| 统一选择引擎 | src/ezdxf/select.py#L219-L242 | 逐实体算包围盒、跳过无数据实体、测试判定、收集结果 |
| 形状抽象基类 | src/ezdxf/select.py#L30-L43 | SelectionShape三个抽象判定方法 |
| 多边形点内判定 | src/ezdxf/math 的is_point_in_polygon_2d | 返回正值=内部、负值=外部、0=边界 |
| 线段-矩形相交 | src/ezdxf/math/clipping.py#L853 | CohenSutherlandLineClipping2d,用于栅栏与多边形边相交判定 |
| 空间索引 | src/ezdxf/math/rtree.py#L139 | RTree,提供points_in_sphere/points_in_bbox查询 |
| 包围盒计算 | src/ezdxf/bbox.py#L16-L124 | Cache类与extents/multi_recursive计算函数 |
| 测试验证 | tests/test_05_tools/test_543_select.py | 覆盖三种形状 × 三种判定、栅栏、点选、链式选择、空间索引的全部边界情形 |
九、注意事项与边界情况总结
- 精度边界:所有判定基于包围盒而非真实几何。文本与曲线类实体的包围盒不精确(详见 src/ezdxf/bbox.py 文档)。
- 凹多边形:
Polygon对复杂凹多边形的is_inside_bbox/is_outside_bbox判定可能不 100% 正确,源码注释明确说明 "This may not cover all concave polygons"。 - 栅栏与点:单个 POINT 实体按定义无法被栅栏选中;闭合栅栏不等效于多边形选择。
- 链式选择复杂度:
bbox_chained为 O(n²),大集合慎用。 - 索引局限性:
PlanarSearchIndex只能发现"至少一个检测点落入搜索区域"的实体;构建后不可扩展;max_node_size参数默认 5,源码注释提示"仅在明确知道后果时才修改"。 - 投影规则:所有形状均将实体投影到 xy 平面判定,三维实体的 z 坐标不影响选择结果。
- 空包围盒跳过:无法计算包围盒的实体(如无数据的 HATCH)会被静默跳过,不会导致选择失败。
掌握上述函数与形状后,你便可以在 ezdxf 中实现与 CAD 软件交互式选择工具相近的窗口选择、圆形选择、多边形选择、栅栏选择与点选能力,并可借助EntityQuery链式属性筛选与PlanarSearchIndex空间索引,将空间粗筛与属性精筛高效组合。
- 图形学
【免费下载链接】ezdxf
Python interface to DXF
相关推荐
5步终极指南:如何用OpenCore Legacy Patcher让旧Mac焕发新生
5步终极指南:如何用OpenCore Legacy Patcher让旧Mac焕发新生 想让老旧Mac设备突破官方限制,升级到最新macOS系统吗?OpenCor
操作系统固件驱动开发PX4 Geofence(地理围栏)完全指南:从基础圆柱围栏到多边形规划与源码级原理解析
PX4 Geofence(地理围栏)完全指南:从基础圆柱围栏到多边形规划与源码级原理解析 地理围栏(Geofence)是 PX4 提供的虚拟飞行边界机制,用于防
嵌入式物联网机器人自动驾驶智能硬件SlickGrid行选择模型:单选、多选与范围选择的完整实现
SlickGrid行选择模型:单选、多选与范围选择的完整实现 SlickGrid作为一款 闪电般快速的JavaScript网格组件 ,其强大的行选择模型功能让数
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考