// 实体层第一步:把 ENTITIES 段里的记录翻译成几何。
//
// 结构层把记录切成公共部分 + 子类之后,这里做「翻译」:认得出的类型
// (LINE / POINT…)变成带坐标的几何,认不出的原样留在 `Unknown` 里——
// **不是错误,也不丢**。DXF 上百种实体,加工检查必须先知道
// 「有多少实体我没看懂」,才谈得上给结论。
//
// 为什么是「公共壳 + 几何」而不是「一条实体一个结构体」:图层、句柄、
// 原记录每条实体都有,类型相关的只有几何那部分。分开之后,
// 统计、按图层筛选这些操作用公共壳就够,不用挨个 match。
//
// 坐标分组的约定(`tag.mbt` 里 `point_code_parts` 管这个):
// `10/20/30` 是第 0 个点、`11/21/31` 是第 1 个(LINE 的起点和终点)。
//
// 角度:圆弧的起止角在文件里写的是**度数**,解析时换算成弧度——
// 内部一律用弧度(`vec.mbt` 的角度工具都是弧度)。
// ── 数据模型 ────────────────────────────────────────────────────────────
///|
/// 实体的公共部分:每条实体都有的东西。
pub(all) struct EntityCommon {
/// 记录类型名(`LINE` / `CIRCLE` / …)
kind : String
/// 图层(组码 8)。没写时是空串
layer : String
/// 句柄(组码 5,DIMSTYLE 用 105)。没写时是空串
handle : String
/// 原始记录。已解码的字段从这取,**没解码的字段也在这**——
/// 供调用方核对未解码字段;不承诺文件写回或字节级无损往返
record : Record
}
///|
/// 实体的几何。只认本项目要处理的形状,其余落在 `Unknown`。
pub(all) enum Geometry {
/// 线段:起点、终点
Line(start~ : Vec3, end~ : Vec3)
/// 单点
Point(at~ : Vec3)
/// 圆:圆心、半径。
///
/// **半径可能是 0 或负数**——坏图纸里真实存在,部分机床遇到直接报错。
/// 保留原值(不替换、不跳过),让加工检查去报这一条。
Circle(center~ : Vec3, radius~ : Double)
/// 圆弧:圆心、半径、起止角(弧度)。
///
/// 起止角可能大于一圈(370 度绕回 10 度的写法真实存在),
/// 判跨度用 `arc_sweep` 归一。
Arc(
center~ : Vec3,
radius~ : Double,
start_angle~ : Double,
end_angle~ : Double
)
/// 椭圆(弧):圆心、长轴向量(圆心指向长轴端点)、短长轴比、起止参数(弧度)。
///
/// 参数 p 对应的点是 `圆心 + 长轴·cos p + 短轴·sin p`,整椭圆是 0 到 2π。
Ellipse(
center~ : Vec3,
major~ : Vec3,
ratio~ : Double,
start_param~ : Double,
end_param~ : Double
)
/// 多段线:一串顶点,每段可以是直线(bulge = 0)或圆弧(bulge ≠ 0)。
///
/// 轻量的 LWPOLYLINE 和带 VERTEX 记录的 POLYLINE 都归到这里——
/// 顶点从哪来不一样,形状表示是一样的。
Polyline(vertices~ : Array[PolyVertex], closed~ : Bool)
/// 文字(TEXT / MTEXT / ATTRIB / ATTDEF 共用):插入点、字高、内容、旋转角。
///
/// 内容**原样保留**:MTEXT 的格式化代码(`\P` 换行、`\f` 字体…)不解释,
/// 那是渲染和离散层的事。具体哪种看 `kind`——加工检查里它们都是
/// 「文字没转成曲线」这一条。
Text(at~ : Vec3, height~ : Double, value~ : String, rotation~ : Double)
/// 块引用(INSERT):插入点、块名、缩放、旋转角(弧度)。
///
/// 真正的形状要展开块内容再做嵌套变换——那是几何层的事,
/// 这里只记录引用本身。
Insert(at~ : Vec3, block~ : String, scale~ : Vec3, rotation~ : Double)
/// 填充面(SOLID / TRACE / 3DFACE 共用):角点列表,三个或四个。
///
/// 原样保留文件的 10、11、12、13 顺序,包括三角形重复的第四角点。
/// SOLID/TRACE 的边界连接为 10、11、13、12;3DFACE 不能统一重排。
/// 需要实体身份的边界入口是 Entity::flatten_checked;裸 Face 不推断轮廓。
Face(corners~ : Array[Vec3])
/// 标注(DIMENSION):定义点、匿名块名、测量值。
///
/// 箭头、界线、文字都画在匿名块(`*D1` 之类)里。**标注放错图层
/// 是加工图纸的经典事故**(轮廓图层上睡着一条标注),所以它必须
/// 是解码出来的实体,而不是 `Unknown`。老版本(R12)没有匿名块,
/// `block` 是空串。
Dimension(at~ : Vec3, block~ : String, measurement~ : Double)
/// 样条曲线(SPLINE):次数、节点向量、控制点、权重。
///
/// 曲线的真实形状要按 NURBS 求值——那是离散层的事,这里把定义
/// 数据原样收好(节点数和控制点数是否自洽留给校验层)。
/// 权重(组码 41)只有有理样条才写;缺省补 1——权重全 1 就是
/// 普通的 B 样条。
Spline(
degree~ : Int,
knots~ : Array[Double],
control_points~ : Array[Vec3],
weights~ : Array[Double]
)
/// 区域填充(HATCH):标高点、是否实体填充、图案名、边界路径数。
///
/// 边界能由线 / 弧 / 椭圆 / 样条四种边组合,还有老式的多段线写法——
/// **边界几何这版不展开**(加工切的是轮廓实体,填充多是区域标记),
/// 先记录身份,让统计和检查环节看得见它。
Hatch(at~ : Vec3, solid~ : Bool, pattern~ : String, paths~ : Int)
/// 认不出的实体类型。**不等于坏记录**——只是解析器还没解码它
Unknown
}
///|
/// 一条实体。
pub(all) struct Entity {
common : EntityCommon
geom : Geometry
}
///|
/// 一个块定义。
pub(all) struct Block {
/// 块名。`INSERT` 靠它引用(`*Model_Space` 之类带星号的是系统块)
name : String
/// 基点——块内容坐标相对它,引用时先减基点
base : Vec3
/// 块的内容(和 ENTITIES 段一样的实体流,到 `ENDBLK` 为止)
entities : Array[Entity]
}
///|
/// BLOCKS 段里解析出来的块定义。
pub(all) struct Blocks {
blocks : Array[Block]
warnings : Array[String]
}
///|
/// 多段线的一个顶点。
pub(all) struct PolyVertex {
at : Vec3
/// 到下一个顶点的凸度(bulge)——圆弧段的「弯度」,
/// `tan(圆心角 / 4)`:0 = 直线段,正 = 逆时针鼓,负 = 顺时针鼓。
/// 最后一个顶点的 bulge 只在闭合时有意义(它管连回第一点的那条边)。
///
/// 可变是因为解析顺序:`42` 在它所属顶点的 `20` **之后**才出现,
/// 顶点先落地、凸度后补上。
mut bulge : Double
}
///|
/// 一个段里解析出来的实体。
pub(all) struct Entities {
entities : Array[Entity]
/// 解析中发现的毛病(缺坐标之类被跳过的记录)
warnings : Array[String]
}
// ── 取字段 ──────────────────────────────────────────────────────────────
///|
/// 取一条记录里第 n 个点(`10/20/30` 是第 0 个,`11/21/31` 是第 1 个)。
///
/// **缺 z 按 0 处理**——平面图里点就在 z=0 平面上,缺 z 是常态。
/// **缺 x 或 y 返回 `None`**——那说明记录坏了,交给调用方决定怎么办
/// (本文件的选择是跳过并警告,而不是拿原点补)。
pub fn Record::point(self : Record, n : Int) -> Vec3? {
let x = match self.get(10 + n) {
Some(t) => t.value.as_double()
None => return None
}
let y = match self.get(20 + n) {
Some(t) => t.value.as_double()
None => return None
}
let z = self.get_double(30 + n, 0.0)
Some(Vec3::new(x, y, z))
}
///|
/// 取句柄(`5`,DIMSTYLE 里是 `105`——同一个含义用了两个码的历史包袱)。
fn handle_of(record : Record) -> String {
let h = record.get_string(5)
if h != "" {
h
} else {
record.get_string(105)
}
}
// ── 访问 ────────────────────────────────────────────────────────────────
///|
pub fn Entity::kind(self : Entity) -> String {
self.common.kind
}
///|
pub fn Entity::layer(self : Entity) -> String {
self.common.layer
}
///|
pub fn Entity::handle(self : Entity) -> String {
self.common.handle
}
///|
/// 这条实体的几何认不认得出来。
///
/// `Unknown` 是「解析器不认识」,**不等于坏记录**。检查环节要单独
/// 报告有一批实体没被解码——图上那些东西也可能要切。
pub fn Entity::is_decoded(self : Entity) -> Bool {
match self.geom {
Unknown => false
_ => true
}
}
///|
/// 几何的二维包围盒(丢掉 z——加工是二维的)。
/// `Unknown` 没有几何,返回 `None`。
pub fn Geometry::bounds(self : Geometry) -> Aabb2? {
match self {
Line(start~, end~) =>
Some(Aabb2::empty().expand(start.xy()).expand(end.xy()))
Point(at~) => Some(Aabb2::empty().expand(at.xy()))
Circle(center~, radius~) => Some(circle_bounds(center.xy(), radius))
Arc(center~, radius~, start_angle~, end_angle~) =>
Some(arc_bounds(center.xy(), radius, start_angle, end_angle))
// 弧段用整椭圆的盒——永远包含,只是偏大;精确的弧段盒留给离散层
Ellipse(center~, major~, ratio~, ..) =>
Some(ellipse_bounds(center, major, ratio))
Polyline(vertices~, closed~) => Some(polyline_bounds(vertices, closed))
// 文字按插入点记——真实占用范围要字体度量。加工检查里文字本身是
// 「未转曲线」的警告项,不是切割几何
Text(at~, ..) => Some(Aabb2::empty().expand(at.xy()))
// 块引用的范围要展开块内容才知道;这里先记插入点,
// 展开(嵌套变换)是几何层的任务
Insert(at~, ..) => Some(Aabb2::empty().expand(at.xy()))
Face(corners~) => {
let mut box = Aabb2::empty()
for p in corners {
box = box.expand(p.xy())
}
Some(box)
}
// 标注的真实范围在匿名块里;定义点先记下
Dimension(at~, ..) => Some(Aabb2::empty().expand(at.xy()))
// 控制点盒是保守的——曲线整体在控制点的凸包里,盒装得下
Spline(control_points~, ..) =>
if control_points.length() == 0 {
None
} else {
let mut box = Aabb2::empty()
for p in control_points {
box = box.expand(p.xy())
}
Some(box)
}
// 填充的真实范围要展开边界;标高点先记下
Hatch(at~, ..) => Some(Aabb2::empty().expand(at.xy()))
Unknown => None
}
}
///|
/// 实体的二维包围盒。`Unknown` 返回 `None`。
pub fn Entity::bounds(self : Entity) -> Aabb2? {
self.geom.bounds()
}
// ── 角度与几何辅助 ──────────────────────────────────────────────────────
///|
/// 一整圈(弧度)。`vec.mbt` 的 `normalize_angle` 里是同一个值,
/// 这里要用在「起止角相等按整圈」的判断上。
let two_pi : Double = 6.283185307179586
///|
/// 度 → 弧度。DXF 文本里的角度一律是度数(圆弧起止角、文字旋转角),
/// 本项目的内部表示一律是弧度(`normalize_angle`、`angle_span` 都是弧度)。
fn to_radians(deg : Double) -> Double {
deg * 0.017453292519943295
}
///|
/// 圆弧从起角到止角跨过的弧度。
///
/// 起止角相等(模一整圈)按**整圈**处理——`0 到 360 度`这种写法
/// 指的是整圆,不是零度弧(`370 度绕回 10 度`是它的亲戚,同一个问题)。
fn arc_sweep(start : Double, end : Double) -> Double {
let span = angle_span(start, end)
if span < 1.0e-12 {
two_pi
} else {
span
}
}
///|
/// 圆上的点(角度是弧度)。
fn point_on_circle(c : Vec2, r : Double, angle : Double) -> Vec2 {
Vec2::new(c.x + r * @math.cos(angle), c.y + r * @math.sin(angle))
}
///|
/// 整圆的二维包围盒。半径取绝对值——负半径的圆是坏数据,
/// 但盒子的形状还是它,报不报让检查环节决定。
fn circle_bounds(center : Vec2, radius : Double) -> Aabb2 {
let r = radius.abs()
Aabb2::empty()
.expand(Vec2::new(center.x - r, center.y - r))
.expand(Vec2::new(center.x + r, center.y + r))
}
///|
/// DXF 圆弧(起止角语义)的二维包围盒。
/// 「起止角相等按整圈」是 `arc_sweep` 的语义,严格对应 DXF 的 ARC。
fn arc_bounds(
center : Vec2,
radius : Double,
start_angle : Double,
end_angle : Double,
) -> Aabb2 {
arc_bounds_with_sweep(
center,
radius,
start_angle,
arc_sweep(start_angle, end_angle),
)
}
///|
/// 圆弧的二维包围盒:两个端点,加上跨过的象限点。
///
/// 光取端点是错的——0° 到 270° 的弧,最右边其实在 90° 的象限点上。
///
/// 跨度由调用方给:DXF 的 ARC 走 `arc_sweep`(起止相等 = 整圈);
/// 多段线的 bulge 段走 `angle_span`——bulge 的弧不适用「起止相等
/// 按整圈」,近直线的微小 bulge 会被误判成巨大圆弧。
fn arc_bounds_with_sweep(
center : Vec2,
radius : Double,
start : Double,
sweep : Double,
) -> Aabb2 {
if sweep >= two_pi - 1.0e-12 {
// 整圈,直接给整圆的盒
return circle_bounds(center, radius)
}
let r = radius.abs()
let mut box = Aabb2::empty()
box = box.expand(point_on_circle(center, r, start))
box = box.expand(point_on_circle(center, r, start + sweep))
// 跨过的象限点(0° / 90° / 180° / 270°)
let mut k = 0
while k < 4 {
let q = k.to_double() * 1.5707963267948966
if angle_span(start, q) <= sweep {
box = box.expand(point_on_circle(center, r, q))
}
k = k + 1
}
box
}
///|
/// 椭圆的二维包围盒。
///
/// 长轴向量 M 和短轴向量 N(M 逆时针转 90 度、乘短长轴比):
/// 椭圆上的点写成 `center + M·cos p + N·sin p`,两个方向的最大偏移
/// 分别是 `√(Mx² + Nx²)` 和 `√(My² + Ny²)`——对整椭圆是精确的。
fn ellipse_bounds(center : Vec3, major : Vec3, ratio : Double) -> Aabb2 {
let m = major.xy()
let n = Vec2::new(-m.y * ratio, m.x * ratio)
let half_x = (m.x * m.x + n.x * n.x).sqrt()
let half_y = (m.y * m.y + n.y * n.y).sqrt()
let c = center.xy()
Aabb2::empty()
.expand(Vec2::new(c.x - half_x, c.y - half_y))
.expand(Vec2::new(c.x + half_x, c.y + half_y))
}
///|
/// 多段线的二维包围盒:顶点盒,加上圆弧段的精确范围。
///
/// bulge 段换算成圆弧(`arc_from_bulge`,离散层)后按端点 + 象限点
/// 取盒——之前「弦 + 矢高撑开」那版保守估计已经不需要了。
fn polyline_bounds(vertices : Array[PolyVertex], closed : Bool) -> Aabb2 {
let mut box = Aabb2::empty()
for v in vertices {
box = box.expand(v.at.xy())
}
let n = vertices.length()
if n < 2 {
return box
}
let segments = if closed { n } else { n - 1 }
let mut i = 0
while i < segments {
let a = vertices[i]
let b = vertices[(i + 1) % n]
if a.bulge != 0.0 {
let ax = a.at.xy()
let bx = b.at.xy()
if ax.distance(bx) > 0.0 {
let arc = arc_from_bulge(ax, bx, a.bulge)
box = box.union(
arc_bounds_with_sweep(
arc.center,
arc.radius,
arc.start_angle,
angle_span(arc.start_angle, arc.end_angle),
),
)
}
}
i = i + 1
}
box
}
// ── 解析 ────────────────────────────────────────────────────────────────
///|
/// 解析一个段里的实体。
///
/// 入口是 ENTITIES 段;将来块定义(BLOCKS 段)里的实体也走这里。
pub fn parse_entities(section : Section) -> Entities {
let warnings : Array[String] = []
let entities = parse_records(section.records[:], warnings)
{ entities, warnings, }
}
///|
/// 扫一段记录流,翻译成实体(ENTITIES 段和 BLOCKS 段里块的内容共用)。
///
/// 「多记录实体」在这里消化:POLYLINE 吃掉后面的 VERTEX 直到 `SEQEND`;
/// INSERT 后面跟着的 ATTRIB(块引用的属性值)**不吞**——它们作为独立
/// 实体留在列表里(属性文字也可能没转曲线),只跳过收尾的 `SEQEND`。
fn parse_records(
records : ArrayView[Record],
warnings : Array[String],
) -> Array[Entity] {
let entities : Array[Entity] = []
let mut i = 0
while i < records.length() {
let record = records[i]
if record.kind == "POLYLINE" {
// 顶点不在这一条记录里——跟在后面的 VERTEX 记录中,`SEQEND` 收尾
let mut j = i + 1
while j < records.length() && records[j].kind == "VERTEX" {
j = j + 1
}
match parse_polyline_entity(record, records[i + 1:j], warnings) {
Some(e) => entities.push(e)
None => ()
}
if j < records.length() && records[j].kind == "SEQEND" {
j = j + 1
} else {
warnings.push(
"POLYLINE 缺少 SEQEND;已保留可读取记录,请复核输入完整性",
)
}
i = j
} else if record.kind == "VERTEX" || record.kind == "SEQEND" {
// 没有主人的结构性记录——不是实体,跳过
i = i + 1
} else {
match parse_entity(record, warnings) {
Some(e) => entities.push(e)
None => ()
}
i = i + 1
}
}
entities
}
///|
/// 取一个子类里的 tag;没有这个子类(老格式)时退回公共部分。
///
/// **顺序敏感的场景要用它**:多段线的顶点是同一组码重复出现,
/// 成组顺序本身携带信息,`get` / `all_of` 都给不了。
fn subclass_tags(record : Record, name : String) -> Array[Tag] {
for sc in record.subclasses {
if sc.name == name {
return sc.tags
}
}
record.common
}
///|
/// 解析 LWPOLYLINE(轻量多段线,R13 起)。
///
/// 顶点在 `AcDbPolyline` 子类里**成组重复出现**:每个顶点一对 `10/20`,
/// 可选的 `42`(凸度)跟在它的 `20` 后面。所以按顺序扫,不能用 `get`
/// 取——`get` 只会拿到第一对。组码 90 与实际顶点数不符时报告诊断。
/// 损坏坐标保留原始记录,但不把剩余顶点拼成另一条轮廓。
fn parse_lwpolyline(record : Record, warnings : Array[String]) -> Geometry? {
warn_polyline_extent(record, [43, 40, 41, 39], warnings)
let elevation = record.get_double(38, 0.0)
let vertices : Array[PolyVertex] = []
let mut pending_x : Double? = None
let mut damaged = false
for t in subclass_tags(record, "AcDbPolyline") {
match t.code {
10 => {
if pending_x is Some(_) {
warnings.push(
"LWPOLYLINE 顶点缺少 Y(20),未生成该实体的轮廓",
)
damaged = true
}
pending_x = Some(t.value.as_double())
}
20 =>
match pending_x {
Some(x) => {
vertices.push({
at: Vec3::new(x, t.value.as_double(), elevation),
bulge: 0.0,
})
pending_x = None
}
None => {
warnings.push(
"LWPOLYLINE 顶点缺少 X(10),未生成该实体的轮廓",
)
damaged = true
}
}
42 =>
if pending_x is Some(_) || vertices.length() == 0 {
warnings.push(
"LWPOLYLINE 顶点 bulge(42)没有完整坐标,未生成该实体的轮廓",
)
damaged = true
} else {
vertices[vertices.length() - 1].bulge = t.value.as_double()
}
_ => ()
}
}
if pending_x is Some(_) {
warnings.push(
"LWPOLYLINE 最后一个顶点缺少 Y(20),未生成该实体的轮廓",
)
damaged = true
}
if vertices.length() == 0 {
warnings.push("LWPOLYLINE 解析不出顶点,已跳过")
return None
}
if record.get(90) is Some(_) && record.get_int(90, 0) != vertices.length() {
warnings.push(
"LWPOLYLINE 声明顶点数 \{record.get_int(90, 0)} 与完整坐标数 \{vertices.length()} 不一致;请复核输入",
)
}
if damaged {
return Some(Unknown)
}
let closed = (record.get_int(70, 0) & 1) != 0
Some(Polyline(vertices~, closed~))
}
///|
/// MTEXT 的内容:正文超过 250 字符会被切成多个 `3`,最后一段在 `1` 里。
/// **按出现顺序拼起来**;格式化代码(`\P` 换行、`\f` 字体…)原样保留——
/// 解释它们是渲染 / 离散层的事。
fn mtext_value(record : Record) -> String {
let sb = StringBuilder()
for t in subclass_tags(record, "AcDbMText") {
if t.code == 3 || t.code == 1 {
sb.write_string(t.value.as_string())
}
}
sb.to_string()
}
///|
/// 解析 SPLINE 的定义数据。
///
/// 节点(`40`)、权重(`41`)和控制点(`10/20/30`)都是**同一组码
/// 重复出现**的,控制点还是三码一组的坐标——按顺序扫,缺 `30` 的
/// 按 z=0 处理(和 `Record::point` 一个约定)。
fn parse_spline(record : Record, warnings : Array[String]) -> Geometry {
let degree = match record.get(71) {
Some(tag) => tag.value.as_int()
None => {
warnings.push(
"SPLINE 缺少次数(71);无法确定曲线定义,保留记录,轮廓未检查",
)
return Unknown
}
}
let flags = record.get_int(70, 0)
if (flags & 3) != 0 {
warnings.push(
"SPLINE 使用闭合或周期语义,本版本尚未重建该语义;保留记录,轮廓未检查",
)
return Unknown
}
let knots : Array[Double] = []
let weights : Array[Double] = []
let control : Array[Vec3] = []
let mut px : Double? = None
let mut py : Double? = None
let mut damaged = false
for t in subclass_tags(record, "AcDbSpline") {
match t.code {
40 => knots.push(t.value.as_double())
41 => weights.push(t.value.as_double())
10 => {
// 上一个点没等到 30 就被新点顶掉:按 z = 0 收掉
match (px, py) {
(Some(x), Some(y)) => control.push(Vec3::new(x, y, 0.0))
(None, None) => ()
_ => {
warnings.push(
"SPLINE 控制点缺少 X/Y 坐标;保留记录,未生成该实体的轮廓",
)
damaged = true
}
}
px = Some(t.value.as_double())
py = None
}
20 => {
if px is None || py is Some(_) {
warnings.push(
"SPLINE 控制点缺少 X 或重复 Y;保留记录,未生成该实体的轮廓",
)
damaged = true
}
py = Some(t.value.as_double())
}
30 =>
match (px, py) {
(Some(x), Some(y)) => {
control.push(Vec3::new(x, y, t.value.as_double()))
px = None
py = None
}
_ => {
warnings.push(
"SPLINE 控制点的 Z 前没有完整 X/Y;未生成该实体的轮廓",
)
damaged = true
px = None
py = None
}
}
_ => ()
}
}
match (px, py) {
(Some(x), Some(y)) => control.push(Vec3::new(x, y, 0.0))
(None, None) => ()
_ => {
warnings.push(
"SPLINE 最后一个控制点缺少 X/Y;保留记录,未生成该实体的轮廓",
)
damaged = true
}
}
if record.get(73) is Some(_) && record.get_int(73, 0) != control.length() {
warnings.push(
"SPLINE 声明控制点数与完整坐标数不一致;请复核输入",
)
}
if record.get(72) is Some(_) && record.get_int(72, 0) != knots.length() {
warnings.push(
"SPLINE 声明节点数与实际节点数不一致;请复核输入",
)
}
if damaged {
return Unknown
}
// 只有完全省略权重时使用全 1;部分或多余权重不能静默改写曲线。
if weights.length() == 0 {
for _ in control {
weights.push(1.0)
}
} else if weights.length() != control.length() {
warnings.push(
"SPLINE 权重数量与控制点数量不一致;保留记录,未生成该实体轮廓",
)
return Unknown
}
Spline(degree~, knots~, control_points=control, weights~)
}
///|
/// 实体的公共外壳。单记录实体和 POLYLINE 这种多记录实体共用。
fn common_of(record : Record) -> EntityCommon {
{
kind: record.kind,
layer: record.get_string(8),
handle: handle_of(record),
record,
}
}
///|
/// 翻译一条 POLYLINE(连同它后面的 VERTEX 记录)。
///
/// 老的 POLYLINE 顶点在**独立记录**里——DXF 里少数「一条实体横跨
/// 多条记录」的地方。顶点上的 42 和 LWPOLYLINE 是一个意思。
fn parse_polyline_entity(
record : Record,
vertex_records : ArrayView[Record],
warnings : Array[String],
) -> Entity? {
warn_polyline_extent(record, [40, 41, 39], warnings)
let mut vertex_width = false
for vr in vertex_records {
for code in [40, 41] {
for tag in vr.all_of(code) {
if tag.value.as_double() != 0.0 {
vertex_width = true
}
}
}
}
if vertex_width {
warnings.push(
"POLYLINE 顶点含非零宽度;本工具仅检查中心线,区域/厚度未验证",
)
}
let common = common_of(record)
let flags = record.get_int(70, 0)
// 仅接收普通二维折线。拟合、空间折线和网格不能按普通顶点连线。
// bit 1(闭合)和 bit 128(线型生成)不改变本项目的中心线表示。
if (flags & 126) != 0 {
warnings.push(
"POLYLINE 标志 \{flags} 超出普通二维折线范围;保留记录,轮廓未检查",
)
return Some({ common, geom: Unknown, })
}
let vertices : Array[PolyVertex] = []
let mut damaged = false
for index, vr in vertex_records {
if vr.get_int(70, 0) != 0 {
warnings.push(
"POLYLINE 顶点 \{index + 1} 带拟合或空间标志;保留记录,轮廓未检查",
)
damaged = true
}
match vr.point(0) {
Some(at) => vertices.push({ at, bulge: vr.get_double(42, 0.0), })
None => {
warnings.push(
"POLYLINE 顶点 \{index + 1} 缺少 X/Y 坐标;未生成该实体的轮廓",
)
damaged = true
}
}
}
if vertices.length() == 0 {
warnings.push("POLYLINE 解析不出顶点,已跳过")
return None
}
if damaged {
return Some({ common, geom: Unknown, })
}
Some({ common, geom: Polyline(vertices~, closed=(flags & 1) != 0), })
}
///|
/// 取一个组码的数值。**没写返回 `None`**——和 `get_double` 的默认值
/// 是两回事:这里要区分「写了 0」和「根本没写」,缺必填字段是要报警的。
fn number_of(record : Record, code : Int) -> Double? {
match record.get(code) {
Some(t) => Some(t.value.as_double())
None => None
}
}
///|
/// 翻译一条记录。返回 `None` 表示这条被跳过了(毛病已记进 `warnings`)。
///
/// **缺必填坐标的记录跳过,不拿原点补**:补出来的假线段会混进
/// 最小间隙检查里,报一条警告、少一条实体,比一个错的位置诚实。
fn parse_entity(record : Record, warnings : Array[String]) -> Entity? {
let kind = record.kind
let common = common_of(record)
match kind {
"LINE" =>
match (record.point(0), record.point(1)) {
(Some(start), Some(end)) => Some({ common, geom: Line(start~, end~), })
_ => {
warnings.push(
"\{kind} 缺端点坐标(10/20 起,11/21 止),已跳过",
)
None
}
}
"POINT" =>
match record.point(0) {
Some(at) => Some({ common, geom: Point(at~), })
None => {
warnings.push("\{kind} 缺坐标(10/20),已跳过")
None
}
}
"CIRCLE" =>
match (record.point(0), number_of(record, 40)) {
(Some(center), Some(radius)) =>
Some({ common, geom: Circle(center~, radius~), })
(None, _) => {
warnings.push("\{kind} 缺圆心坐标(10/20),已跳过")
None
}
(_, None) => {
warnings.push("\{kind} 缺半径(40),已跳过")
None
}
}
"ARC" =>
match
(
record.point(0),
number_of(record, 40),
number_of(record, 50),
number_of(record, 51),
) {
(Some(center), Some(radius), Some(a0), Some(a1)) =>
Some({
common,
geom: Arc(
center~,
radius~,
start_angle=to_radians(a0),
end_angle=to_radians(a1),
),
})
(None, _, _, _) => {
warnings.push("\{kind} 缺圆心坐标(10/20),已跳过")
None
}
(_, None, _, _) => {
warnings.push("\{kind} 缺半径(40),已跳过")
None
}
(_, _, _, _) => {
warnings.push("\{kind} 缺起止角(50/51),已跳过")
None
}
}
"ELLIPSE" =>
match (record.point(0), record.point(1)) {
(Some(center), Some(major)) =>
match number_of(record, 40) {
Some(ratio) => {
// 41/42 省略时是整椭圆——语义明确,不报警
let start_param = match number_of(record, 41) {
Some(v) => v
None => 0.0
}
let end_param = match number_of(record, 42) {
Some(v) => v
None => two_pi
}
Some({
common,
geom: Ellipse(
center~,
// 11/21/31 已经是相对于圆心的向量,不能再次减去圆心。
major~,
ratio~,
start_param~,
end_param~,
),
})
}
None => {
warnings.push(
"\{kind} 缺短长轴比(40),推不出形状,已跳过",
)
None
}
}
_ => {
warnings.push(
"\{kind} 缺圆心或长轴向量(10/20、11/21),已跳过",
)
None
}
}
"TEXT" | "MTEXT" | "ATTRIB" | "ATTDEF" =>
match record.point(0) {
Some(at) =>
match number_of(record, 40) {
Some(height) => {
let value = if kind == "MTEXT" {
mtext_value(record)
} else {
record.get_string(1)
}
Some({
common,
geom: Text(
at~,
height~,
value~,
rotation=to_radians(record.get_double(50, 0.0)),
),
})
}
None => {
warnings.push("\{kind} 缺字高(40),已跳过")
None
}
}
None => {
warnings.push("\{kind} 缺插入点坐标(10/20),已跳过")
None
}
}
"LWPOLYLINE" =>
match parse_lwpolyline(record, warnings) {
Some(geom) => Some({ common, geom, })
None => None
}
"INSERT" =>
match record.point(0) {
Some(at) => {
let block = record.get_string(2)
if block == "" {
warnings.push("INSERT 缺块名(2),已跳过")
None
} else {
Some({
common,
geom: Insert(
at~,
block~,
scale=Vec3::new(
record.get_double(41, 1.0),
record.get_double(42, 1.0),
record.get_double(43, 1.0),
),
rotation=to_radians(record.get_double(50, 0.0)),
),
})
}
}
None => {
warnings.push("INSERT 缺插入点坐标(10/20),已跳过")
None
}
}
"SOLID" | "TRACE" | "3DFACE" =>
match (record.point(0), record.point(1), record.point(2)) {
(Some(a), Some(b), Some(c)) => {
let corners : Array[Vec3] = [a, b, c]
match record.point(3) {
Some(d) => corners.push(d)
None => {
if record.count_code(13) +
record.count_code(23) +
record.count_code(33) >
0 {
warnings.push(
"\{kind} 第四角点坐标不完整,已跳过;不能当作三角形",
)
return None
}
if kind == "SOLID" || kind == "TRACE" {
warnings.push(
"\{kind} 缺第四角点,按三个已有角点恢复;检查不完整",
)
}
}
}
Some({ common, geom: Face(corners~), })
}
_ => {
warnings.push("\{kind} 缺角点坐标(10/11/12),已跳过")
None
}
}
"SPLINE" => Some({ common, geom: parse_spline(record, warnings), })
"HATCH" =>
match record.point(0) {
Some(at) =>
Some({
common,
geom: Hatch(
at~,
solid=record.get_int(70, 0) != 0,
pattern=record.get_string(2),
paths=record.get_int(91, 0),
),
})
None => {
warnings.push("\{kind} 缺标高点坐标(10/20),已跳过")
None
}
}
// ARC_DIMENSION / LARGE_RADIAL_DIMENSION 是 R2007 起给弧长标注和
// 大半径标注起的独立实体名,记录形状和 DIMENSION 一样(同一套
// AcDbDimension 子类)——借用同一条路(语料里实测到了这两种)
"DIMENSION" | "ARC_DIMENSION" | "LARGE_RADIAL_DIMENSION" =>
match record.point(0) {
Some(at) =>
Some({
common,
geom: Dimension(
at~,
// R12 的标注没有匿名块,缺块名不算坏记录
block=record.get_string(2),
measurement=record.get_double(42, 0.0),
),
})
None => {
warnings.push("\{kind} 缺定义点坐标(10/20),已跳过")
None
}
}
_ => Some({ common, geom: Unknown, })
}
}
///|
/// 图纸的 ENTITIES 段解析成实体。没有 ENTITIES 段时是空表。
pub fn Drawing::entities(self : Drawing) -> Entities {
match self.section("ENTITIES") {
Some(s) => parse_entities(s)
None => {
let entities : Array[Entity] = []
let warnings : Array[String] = []
{ entities, warnings, }
}
}
}
///|
/// 解析 BLOCKS 段:每个 `BLOCK` 到 `ENDBLK` 之间的记录就是块的内容,
/// 走和 ENTITIES 段同一套翻译(`parse_records`)。
pub fn parse_blocks(section : Section) -> Blocks {
let blocks : Array[Block] = []
let warnings : Array[String] = []
let records = section.records
let mut i = 0
while i < records.length() {
if records[i].kind != "BLOCK" {
i = i + 1
continue
}
let head = records[i]
let mut j = i + 1
while j < records.length() &&
records[j].kind != "ENDBLK" &&
records[j].kind != "BLOCK" {
j = j + 1
}
let name = head.get_string(2)
if j >= records.length() || records[j].kind != "ENDBLK" {
warnings.push(
"块「\{name}」缺少 ENDBLK;按下一 BLOCK 或 BLOCKS 段末尾恢复,检查不完整",
)
}
if name == "" {
warnings.push("BLOCK 没有名字,已跳过")
} else {
let base = match head.point(0) {
Some(p) => p
// 基点缺失按原点处理并记一条——块的内容至少要看得见
None => {
warnings.push(
"块「\{name}」缺基点(10/20),按原点处理",
)
Vec3::zero()
}
}
let entities = parse_records(records[i + 1:j], warnings)
blocks.push({ name, base, entities, })
}
if j < records.length() && records[j].kind == "ENDBLK" {
j = j + 1
}
i = j
}
{ blocks, warnings, }
}
///|
fn warn_polyline_extent(
record : Record,
codes : Array[Int],
warnings : Array[String],
) -> Unit {
for code in codes {
for tag in record.all_of(code) {
if tag.value.as_double() != 0.0 {
warnings.push(
"\{record.kind} 含非零宽度或厚度;本工具仅检查中心线,区域/厚度未验证",
)
return
}
}
}
}
///|
/// 图纸的 BLOCKS 段。没有 BLOCKS 段时是空表。
pub fn Drawing::blocks(self : Drawing) -> Blocks {
match self.section("BLOCKS") {
Some(s) => parse_blocks(s)
None => {
let blocks : Array[Block] = []
let warnings : Array[String] = []
{ blocks, warnings, }
}
}
}