dxfkit

    MoonBit ASCII DXF 读取与二维轮廓辅助检查库:七类核心轮廓、普通块展开、SVG预览与流式统计

    dxf
    cad
    laser-cutting
    cnc
    drawing
    parser
    moonbit
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    10 days ago
    Downloads
    53

    #dxfkit:接口与实现边界

    MoonBit ASCII DXF 读取与二维轮廓辅助检查库。模块版本 0.1.1,安装、场景、CLI 和网页入口见 README.md。

    在 MoonBit 工程中添加固定版本依赖:

    moon add mik1e80/dxfkit@0.1.1

    包导入名为 mik1e80/dxfkit;独立使用不依赖网页或本库源码工作区。注册表消费验证命令为 node scripts/verify-consumer.cjs --registry,与本地 moon.work 验证分别记录。

    本次范围为七类核心轮廓实体、受限二维 SOLID/TRACE 边界和普通 INSERT;逐字段输入、输出、限制与案例见交付范围,追加行为见补充几何审查。

    #为什么选择这个范围

    DXF 图纸交换与脚本处理是已有需求,Python ezdxf 是成熟参照。dxfkit 的交付目标是可复用的 MoonBit 读取与几何接口,附带可验证的检查示例。它不保证图纸可加工,也不以“生态唯一”或功能数量作为正确性的证据。

    #读取与数据模型

    let scan = @dxfkit.scan_tags(bytes)
    let drawing = @dxfkit.build_drawing(scan)
    let entities = drawing.entities()
    let report = drawing.check({ tol: 0.05, min_gap: 0.5 })
    println(report.to_text())
    let svg = drawing.to_svg(0.05)

    TagScan 返回 tag、诊断与编码;Drawing 保存段、记录、子类与警告。Entities 保存已解码实体及未解码记录。未知实体不自动丢弃,但本项目不提供写回或字节级无损往返承诺。

    支持不同时期 ASCII DXF 的组码和子类写法。47 个回归文件包含 AC1006、AC1009 到 AC1032 的示例,这不表示支持这些版本的全部实体与语义。

    数值异常保留文本并警告;整数使用整数解析,64位值不再经 Double 舍入。UTF-8 和 CP1252 可解码,其他声明代码页以 Latin-1 降级并警告;UTF-8 使用宽容解码。编码探测限于文件头 1024 字节。

    #几何与离散

    bounds() 是原始解码数据的几何包围估计,不代表通过 checked 合法性验证;非法 ELLIPSE 仍可能得到包围估计。可靠几何处理须先使用 flatten_checked(),本轮不新增 bounds_checked API。

    当前 main 要求显式 SPLINE 次数(71),缺失不再默认三次;明确拒绝 Closed/Periodic 标志,以及非空但不等于控制点数的权重数组;无权重才按全 1。ELLIPSE 轴比接收 (0,1]。多段线非零宽度/厚度保留中心线并报告不完整;缺 ENDBLK 按下一块或段末恢复且警告。完整修订见本轮记录。

    核心轮廓为 LINE、CIRCLE、ARC、ELLIPSE、LWPOLYLINE、普通二维 POLYLINE、受限 SPLINE。POINT 和文字用于位置或简化预览;SOLID/TRACE 在下述受限范围内支持二维边界;HATCH、DIMENSION、3DFACE 等保留已有字段,不能据此认为它们的轮廓已参与检查。

    ELLIPSE 的 11/21/31 是相对圆心长轴向量。普通 POLYLINE 不接受拟合、空间和网格语义;损坏顶点保留记录并拒绝整条轮廓,避免拼接出新形状。裸 Geometry::Face 不携带 SOLID/TRACE/3DFACE 身份,调用其 flatten() / flatten_checked() 返回 Empty。SOLID/TRACE 应使用 Entity::flatten_checked(tol),在默认 OCS、零厚度、所有角点同 Z 的范围内生成二维边界;3DFACE 仍报告未覆盖。

    • Geometry::flatten_checked(tol) 返回 Result[Flat, String],是曲线的 checked 入口;整图展开使用保留身份的 Entity::flatten_checked。
    • spline_points_checked(degree, knots, control_points, weights, tol) 支持连续、正权重 NURBS 的 XY 离散。
    • arc_from_bulge() 用逆时针形式描述点集;多段线连接时根据 bulge 符号选择遍历方向,并保留精确原始端点。
    • Drawing::expand(tol) 展开嵌套 INSERT,复合仿射变换并处理图层0继承;累计最大拉伸上界用于调整局部容差。
    • Drawing::declared_unit_label() 只显示单位声明,不转换坐标或阈值。

    样条通过 Boehm 节点插入分解为有理 Bézier 段,使用齐次 de Casteljau 细分。正权重下,曲线位于投影控制点的凸包中;用控制点到线段的距离判定细分,避免中点恰好落在弦上时漏掉曲线。椭圆由每段不超过90°的有理二次 Bézier 表示,复用同一离散过程。

    资源边界:样条次数不超过32,输入控制点不超过4096;递归深度16,输出最多65536点。圆弧最多4096段。checked入口达到上限且不能满足容差时返回错误,而不是宣称已满足目标。Double 运算仍有舍入误差;这些限制不构成任意数值尺度的严格精确算术保证。

    旧 flatten()、spline_points() 和 ellipse_points() 为兼容保留占位 / 空结果回退,调用方如需判断完整性应使用 checked 入口。

    #检查的实际语义

    Drawing::check(CheckOptions) 返回 Report,每条 Finding 有检查项、严重程度和文字说明。

    Report::has_incomplete_checks() 统一判断解析恢复、覆盖限制、文字未转轮廓或非法检查参数。报告结论明确标为“检查不完整”,即使同一文件中存在正常轮廓也保留这些原因;CLI --strict 因此失败。具体混合输入和自动断言见完整性约定。普通的开放轮廓几何告警不自动等于检查覆盖缺失。

    检查实际覆盖
    退化几何非正圆半径、空或单点轮廓、零长度线段
    闭合同图层开放实体的端点串链与缝隙;不等于完整拓扑证明
    自相交同一实体内非相邻折线段的真交叉
    重复路径同图层线段的共线重叠,排除同实体相邻段
    最小净距同图层不同实体的线段距离;跳过容差内的端点接触
    检查覆盖未支持或只提取元数据的实体、无轮廓、非默认OCS、未展开阵列、解析与离散诊断
    文字与图层未转换的文字、图层引用和关闭 / 冻结状态提示

    净距不是材料最窄厚度。它不覆盖单实体窄颈、跨图层关系,也不能代替刀具半径补偿或热 / 力学分析。曲线先离散后比较,阈值附近的结论需考虑两侧离散误差。

    关闭 / 冻结图层在部分几何检查中跳过,是本工具的筛选约定,不代表 CAM 一定按同样方式处理图层。

    参数要求 tol > 0、min_gap >= 0 且有限。min_gap = 0 不检查净距。单位沿用图纸坐标,HEADER声明仅供使用者确认。

    #流式入口

    TagStream::new() / feed(Bytes) / finish() 共用 ASCII 扫描状态机。跨块保留编码探测前缀、未完成行及组码状态。扫描警告会积累;不同调用方保留的输出也影响总内存。

    CLI stats 分块读文件、统计 tag、段、版本、编码,不保存全量 tag 或实体。scan_tags() 则会保留所有 tag;check / svg 使用全量图纸,不能作为低内存几何检查入口。

    #使用与验证

    Layer::is_enabled_for_checks() 只表示本工具默认筛选规则,旧 is_machinable() 文档弃用,保留为同义兼容接口,不判断能否加工;新代码使用前者,本版不添加会让旧调用在 --deny-warn 下失败的编译器弃用属性。Drawing::expand_with_limits(tol, ExpandLimits) 和 check_with_limits 支持图纸预算;默认展开上限为 10000 条放置记录、200000 点、100000 次访问,截断会标为不完整。Drawing::analyze(options) 使用同一 Expansion 生成 Report/SVG/单位;网页新增 analyze JSON 入口,旧 check/svg/svg_error 继续兼容。

    完整公开签名见自动生成的 pkg.generated.mbti;发布前运行 node scripts/verify-maintenance.cjs,核对格式及快照是否漂移。公开 API 审阅说明兼容接口和 checked/unchecked 边界。

    • 核心库仅依赖 MoonBit core;CLI / conformance 使用 moonbitlang/x 和 moonbitlang/async 的文件接口。
    • 库支持 wasm / wasm-gc / JS / native;文件工具支持 wasm / native。
    • CLI --strict 将警告也作为失败,便于批处理门禁;普通模式的0退出码不代表检查完全或可加工。
    • 网页使用同一库编译产物,部署前重新构建并执行回归。
    • SVG 数值使用 Double 往返表示,可含科学计数法;调用方不要依赖固定四位小数。
    • examples/library 是同模块调用包;examples/consumer 是独立模块工程,用公开 API 读取文件、提取实体、checked 离散和检查,已在仓库外以 Wasm、Native 验证。

    验证命令见 README.md。几何正确性以明确反例、解析几何和独立de Boor密集采样验证;第三方文件读取成功、测试数量或汇总点数不能单独证明正确性。

    逐实体坐标/尺寸/包围盒/误差和检查器正反例见验收矩阵。故障注入只作用于临时副本,验证列明的语义故障能被测试检出,不以“能够编译运行”替代判定正确。

    外部语料另按文件记录读取、实体字段解码和几何验证证据。现有 47 张文件均没有逐图纸独立几何真值;产生点列或能执行检查仍只记为已运行不变量回归。

    #SOLID / TRACE 的实体入口

    Entity::flatten_checked(tol) 保留实体身份,默认 OCS、零厚度、所有角点同 Z 时按 0→1→3→2 生成 SOLID/TRACE 闭合边界,标准三角形只输出一次重复的第三/第四点。原始 Face.corners 保持文件顺序,3DFACE 不统一重排。

    此前这些类型仅保留元数据的边界已调整,详见追加审查验收与迁移。裸 Geometry::Face 没有实体身份,其 flatten / checked 仍返回 Empty。整图展开、检查和 SVG 使用新的 Entity 入口。非默认 OCS、非零厚度、非平行 XY 面不产生轮廓;省略第四角点的恢复会报告不完整,部分第四坐标拒绝。

    #暂未实现

    二进制DXF、文件写回、通用编辑、完整引用审计、OCS变换、完整HATCH边界、关联标注重建、通用面实体及三维轮廓检查、全部INSERT阵列、字体转轮廓、刀路生成和参数化草图约束求解。

    #来源

    样条格式参考 Autodesk SPLINE 文档;能力与公式参考 ezdxf;标准样条算法为节点插入、de Casteljau及de Boor。第三方版权、完整许可及已记录的参考关系见 THIRD_PARTY_NOTICES.md。

    非默认 OCS 的 INSERT 在块展开入口报告并跳过整个引用;子实体不会作为正确的 XY 轮廓参与检查或预览。其他实体的非默认 OCS 仍按原有检查覆盖警告处理,未新增通用 OCS 转换。

    Aabb2

    pub(all) struct Aabb2 {
    min : Vec2
    max : Vec2
    }

    平面轴对齐包围盒。

    空盒用 min > max 表示——Aabb2::empty() 给的是 min = +∞, max = -∞, 这样第一个点一并进来就自然变成「只包含那一个点」的盒子, 不用额外记「有没有点」的状态。

    Aabb2::center

    fn Aabb2::center(self : Aabb2) -> Vec2

    中心。空盒返回原点。

    Aabb2::contains

    fn Aabb2::contains(self : Aabb2, p : Vec2, tol : Double) -> Bool

    按容差判断点是否落在盒子里。

    Aabb2::empty

    fn Aabb2::empty() -> Aabb2

    空盒:min 是正无穷、max 是负无穷。往里并第一个点就会得到正确的盒子。

    Aabb2::expand

    fn Aabb2::expand(self : Aabb2, p : Vec2) -> Aabb2

    并进一个点。

    Aabb2::intersects

    fn Aabb2::intersects(self : Aabb2, other : Aabb2, tol : Double) -> Bool

    两个盒子是否相交(按容差)。

    加工检查里用来做线段求交的粗筛:两个包围盒不相交,它们里面的 线段一定不相交,可以整块跳过。这是把 O(n²) 降到能跑的关键。

    Aabb2::is_empty

    fn Aabb2::is_empty(self : Aabb2) -> Bool

    是不是空盒(一个点都没并进来过)。

    Aabb2::size

    fn Aabb2::size(self : Aabb2) -> Vec2

    宽高。空盒返回零。

    Aabb2::union

    fn Aabb2::union(self : Aabb2, other : Aabb2) -> Aabb2

    并进另一个盒子。

    Aabb3

    pub(all) struct Aabb3 {
    min : Vec3
    max : Vec3
    }

    三维轴对齐包围盒。约定和 Aabb2 完全一样。

    Aabb3::empty

    fn Aabb3::empty() -> Aabb3

    Aabb3::expand

    fn Aabb3::expand(self : Aabb3, p : Vec3) -> Aabb3

    Aabb3::is_empty

    fn Aabb3::is_empty(self : Aabb3) -> Bool

    Aabb3::size

    fn Aabb3::size(self : Aabb3) -> Vec3

    三个方向的尺寸。空盒返回零向量。

    Block

    pub(all) struct Block {
    name : String
    base : Vec3
    entities : Array[Entity]
    }

    一个块定义。

    Blocks

    pub(all) struct Blocks {
    blocks : Array[Block]
    warnings : Array[String]
    }

    BLOCKS 段里解析出来的块定义。

    BulgeArc

    pub(all) struct BulgeArc {
    center : Vec2
    radius : Double
    start_angle : Double
    end_angle : Double
    }

    bulge 换算出来的圆弧参数。

    CheckOptions

    pub(all) struct CheckOptions {
    tol : Double
    min_gap : Double
    }

    检查参数。

    Drawing

    pub(all) struct Drawing {
    version : String
    sections : Array[Section]
    warnings : Array[String]
    }

    解析出来的图纸骨架:只有结构,没有语义。

    Drawing::analyze

    fn Drawing::analyze(self : Drawing, opt : CheckOptions) -> DrawingAnalysis

    Drawing::blocks

    fn Drawing::blocks(self : Drawing) -> Blocks

    图纸的 BLOCKS 段。没有 BLOCKS 段时是空表。

    Drawing::check

    fn Drawing::check(self : Drawing, opt : CheckOptions) -> Report

    加工前检查。

    tol 既是离散容差、也是判定尺度(重合、闭合都拿它比)。

    Drawing::check_with_limits

    fn Drawing::check_with_limits(self : Drawing, opt : CheckOptions, limits : ExpandLimits) -> Report

    按显式图纸预算检查;预算截断原因进入检查不完整报告。

    Drawing::declared_unit_label

    fn Drawing::declared_unit_label(self : Drawing) -> String

    仅显示HEADER中的单位声明,不改变坐标或自动换算检查阈值。

    Drawing::entities

    fn Drawing::entities(self : Drawing) -> Entities

    图纸的 ENTITIES 段解析成实体。没有 ENTITIES 段时是空表。

    Drawing::expand

    fn Drawing::expand(self : Drawing, tol : Double) -> Expansion

    图纸全部实体展开成世界坐标的几何。

    INSERT 递归展开,块定义来自 BLOCKS 段;普通实体先按容差离散、 再套上当前累积的变换。展开不了的东西(缺块、循环、太深) 跳过并警告,不猜位置。

    Drawing::expand_with_limits

    fn Drawing::expand_with_limits(self : Drawing, tol : Double, limits : ExpandLimits) -> Expansion

    达到全局预算即停止并报告不完整;不承诺恒定内存或实时性。

    Drawing::section

    fn Drawing::section(self : Drawing, name : String) -> Section?

    取一个段。

    Drawing::to_svg

    fn Drawing::to_svg(self : Drawing, tol : Double) -> String

    图纸 → SVG 字符串(一条 XML 文本)。

    tol 是离散容差,和检查用的一个口径。

    DrawingAnalysis

    pub(all) struct DrawingAnalysis {
    report : Report
    svg : String
    unit : String
    }

    同一 Drawing、同一展开结果生成报告和 SVG。

    Encoding

    pub(all) enum Encoding {
    Utf8
    Cp1252
    Other(Int)
    }

    文件用的编码。

    Encoding::is_fallback

    fn Encoding::is_fallback(self : Encoding) -> Bool

    这个编码是不是只能降级处理。

    Encoding::to_display

    fn Encoding::to_display(self : Encoding) -> String

    编码的中文名,出错信息里用。

    Entities

    pub(all) struct Entities {
    entities : Array[Entity]
    warnings : Array[String]
    }

    一个段里解析出来的实体。

    Entity

    pub(all) struct Entity {
    common : EntityCommon
    geom : Geometry
    }

    一条实体。

    Entity::bounds

    fn Entity::bounds(self : Entity) -> Aabb2?

    实体的二维包围盒。Unknown 返回 None。

    Entity::flatten_checked

    fn Entity::flatten_checked(self : Entity, tol : Double) -> Result[Flat, String]

    保留实体身份的离散入口。SOLID/TRACE 与 3DFACE 的存储顺序不同, 不改写原始 Face 角点,也不把仅有 Geometry 的 Face 统一重排。

    Entity::handle

    fn Entity::handle(self : Entity) -> String

    Entity::is_decoded

    fn Entity::is_decoded(self : Entity) -> Bool

    这条实体的几何认不认得出来。

    Unknown 是「解析器不认识」,不等于坏记录。检查环节要单独 报告有一批实体没被解码——图上那些东西也可能要切。

    Entity::kind

    fn Entity::kind(self : Entity) -> String

    Entity::layer

    fn Entity::layer(self : Entity) -> String

    EntityCommon

    pub(all) struct EntityCommon {
    kind : String
    layer : String
    handle : String
    record : Record
    }

    实体的公共部分:每条实体都有的东西。

    ExpandLimits

    pub(all) struct ExpandLimits {
    max_placed : Int
    max_points : Int
    max_visits : Int
    }

    图纸级展开预算。访问次数也计入空块和 INSERT,防止只有空引用的扇出。

    Expansion

    pub(all) struct Expansion {
    placed : Array[Placed]
    warnings : Array[String]
    }

    展开结果。

    Finding

    pub(all) struct Finding {
    check : String
    severity : Severity
    message : String
    }

    一条发现。

    Flat

    pub(all) enum Flat {
    Outline(points~ : Array[Vec2], closed~ : Bool)
    Dot(at~ : Vec2)
    Label(at~ : Vec2, height~ : Double, value~ : String)
    Empty
    }

    离散之后的形状。

    Geometry

    pub(all) enum Geometry {
    Line(start~ : Vec3, end~ : Vec3)
    Point(at~ : Vec3)
    Circle(center~ : Vec3, radius~ : Double)
    Arc(center~ : Vec3, radius~ : Double, start_angle~ : Double, end_angle~ : Double)
    Ellipse(center~ : Vec3, major~ : Vec3, ratio~ : Double, start_param~ : Double, end_param~ : Double)
    Polyline(vertices~ : Array[PolyVertex], closed~ : Bool)
    Text(at~ : Vec3, height~ : Double, value~ : String, rotation~ : Double)
    Insert(at~ : Vec3, block~ : String, scale~ : Vec3, rotation~ : Double)
    Face(corners~ : Array[Vec3])
    Dimension(at~ : Vec3, block~ : String, measurement~ : Double)
    Spline(degree~ : Int, knots~ : Array[Double], control_points~ : Array[Vec3], weights~ : Array[Double])
    Hatch(at~ : Vec3, solid~ : Bool, pattern~ : String, paths~ : Int)
    Unknown
    }

    实体的几何。只认本项目要处理的形状,其余落在 Unknown。

    Geometry::bounds

    fn Geometry::bounds(self : Geometry) -> Aabb2?

    几何的二维包围盒(丢掉 z——加工是二维的)。 Unknown 没有几何,返回 None。

    Geometry::flatten

    fn Geometry::flatten(self : Geometry, tol : Double) -> Flat

    把实体几何离散成折线。容差是弦高。

    Geometry::flatten_checked

    fn Geometry::flatten_checked(self : Geometry, tol : Double) -> Result[Flat, String]

    检查和展开使用此入口:达到上限或无法处理时返回错误,不把占位点当成正确曲线。
    pub(all) struct Header {
    vars : Array[HeaderVar]
    }

    HEADER 段。

    Header::extmax

    fn Header::extmax(self : Header) -> Vec3?

    图纸范围的右上角($EXTMAX)。

    Header::extmin

    fn Header::extmin(self : Header) -> Vec3?

    图纸范围的左下角($EXTMIN)。

    Header::get

    fn Header::get(self : Header, name : String) -> HeaderVar?

    取一个变量的原始值。

    Header::get_int

    fn Header::get_int(self : Header, name : String, default : Int) -> Int

    取一个变量的第一个值(数值)。没有就用默认值。

    注意 HEADER 变量的值有时是字符串有时是数字($ACADVER 是字符串, $INSUNITS 是整数),所以两个取值函数都要有。

    Header::get_point

    fn Header::get_point(self : Header, name : String) -> Vec3?

    取一个变量表示的三维点($EXTMIN / $EXTMAX 这类)。

    值不够三个时返回 None——宁可说「不知道」,也不要拿 x 当 z 用。

    Header::get_string

    fn Header::get_string(self : Header, name : String) -> String

    取一个变量的第一个值(字符串)。

    Header::insunits

    fn Header::insunits(self : Header) -> Int

    图纸单位。

    $INSUNITS 的值是个编号:0 = 没指定、1 = 英寸、4 = 毫米、6 = 米。 0 是最危险的值——没指定单位意味着图纸上的「1」到底是 1 毫米还是 1 英寸无从判断,差 25.4 倍。加工检查会把这一条报出来。

    HeaderVar

    pub(all) struct HeaderVar {
    name : String
    values : Array[Tag]
    }

    一个 HEADER 变量。

    Layer

    pub(all) struct Layer {
    name : String
    color : Int
    linetype : String
    frozen : Bool
    locked : Bool
    lineweight : Int
    }

    一个图层。

    Layer::color_index

    fn Layer::color_index(self : Layer) -> Int

    图层实际的颜色索引(取绝对值)。

    Layer::is_enabled_for_checks

    fn Layer::is_enabled_for_checks(self : Layer) -> Bool

    是否按本工具默认规则参与部分几何检查,不表示 CAM 或机床行为。

    Layer::is_machinable

    fn Layer::is_machinable(self : Layer) -> Bool

    兼容旧接口;仅表示本工具检查筛选规则,不表示能否加工。 文档弃用:新代码使用 Layer::is_enabled_for_checks();本版保留同义行为。 未添加编译器 deprecated 属性,以免兼容调用在 --deny-warn 下成为错误。

    Layer::is_off

    fn Layer::is_off(self : Layer) -> Bool

    图层是不是关着的。

    DXF 用颜色的符号表示开关,不是单独一个标志位—— 这一点不看规范很难猜到。

    Placed

    pub(all) struct Placed {
    entity : Entity
    layer : String
    flat : Flat
    }

    展开之后的一条几何:世界坐标。

    PolyVertex

    pub(all) struct PolyVertex {
    at : Vec3
    bulge : Double
    }

    多段线的一个顶点。

    Record

    pub(all) struct Record {
    kind : String
    common : Array[Tag]
    subclasses : Array[Subclass]
    }

    一条记录:组码 0 开头的一串 tag。

    Record::all_of

    fn Record::all_of(self : Record, code : Int) -> Array[Tag]

    某个组码出现的所有 tag,按顺序。

    Record::count_code

    fn Record::count_code(self : Record, code : Int) -> Int

    一个组码在记录里出现了几次。

    LWPOLYLINE 的顶点就是同一组码重复出现(每个顶点一对 10/20), 靠这个数出有几个顶点。

    Record::effective_classes

    fn Record::effective_classes(self : Record) -> Array[String]

    这条记录在语义上有哪些类。

    新版本总是从 AcDbEntity 起头(实体通用字段),后面跟着自己那个类; 老版本只合成出「自己那个类」,补上 AcDbEntity 让两边形状一致。

    Record::get

    fn Record::get(self : Record, code : Int) -> Tag?

    在记录里找一个组码(公共部分 + 所有子类,按顺序)。

    只看组码、不看子类——需要区分同名组码时用 get_in。 这个函数适合那些含义到处都一样的组码(图层 8、线型 6 之类)。

    Record::get_double

    fn Record::get_double(self : Record, code : Int, default : Double) -> Double

    取一个组码的浮点值。没有就用给的默认值。

    默认值由调用方给而不是固定 0.0:坐标缺省成 0 是合理的, 但半径缺省成 0 就变成了「一个退化的圆」,性质完全不同。

    Record::get_in

    fn Record::get_in(self : Record, subclass : String, code : Int) -> Tag?

    在指定的子类里找组码。

    这是「组码含义随子类变」的正面用法:明确说出我要的是哪个类的字段。

    Record::get_int

    fn Record::get_int(self : Record, code : Int, default : Int) -> Int

    取一个组码的整数值。

    Record::get_string

    fn Record::get_string(self : Record, code : Int) -> String

    取一个组码的字符串值。没有就是空串。

    Record::has_subclass

    fn Record::has_subclass(self : Record, name : String) -> Bool

    表里的组码 70 在 AcDbSymbolTable 子类里是「表里有多少项」, 在具体记录的子类里另有含义。取字段时要指定子类。

    Record::is_legacy

    fn Record::is_legacy(self : Record) -> Bool

    这条记录是不是老格式(一个 100 标记都没有)。

    老格式的字段全在公共部分里,取字段照样用 get,不影响。

    Record::point

    fn Record::point(self : Record, n : Int) -> Vec3?

    取一条记录里第 n 个点(10/20/30 是第 0 个,11/21/31 是第 1 个)。

    缺 z 按 0 处理——平面图里点就在 z=0 平面上,缺 z 是常态。 缺 x 或 y 返回 None——那说明记录坏了,交给调用方决定怎么办 (本文件的选择是跳过并警告,而不是拿原点补)。

    Record::subclass_names

    fn Record::subclass_names(self : Record) -> Array[String]

    记录的子类名列表。

    老记录没有 100,这里按记录类型合成一个——CIRCLE 记录的字段 在语义上就属于 AcDbCircle。合成之后,报告和检查逻辑看到的记录形状 在新旧版本之间是一致的。

    合成表只列我们要解码的实体。认不出的记录类型返回空数组, 上层按「老格式实体」处理即可——它们的字段全在公共部分,本来就取得出来。

    Report

    pub(all) struct Report {
    findings : Array[Finding]
    }

    检查报告。

    Report::count

    fn Report::count(self : Report, severity : Severity) -> Int

    统计某个严重程度的条数。

    Report::has_fatal

    fn Report::has_fatal(self : Report) -> Bool

    有没有致命问题。

    Report::has_incomplete_checks

    fn Report::has_incomplete_checks(self : Report) -> Bool

    是否存在输入恢复、覆盖限制、未转轮廓文字或无效检查参数。 只描述本次所选检查的完整性,不表示完整 DXF 兼容或可加工性。 保留 Finding 原来的分类,避免调用方依赖解析文字来判断完整性。

    Report::to_text

    fn Report::to_text(self : Report) -> String

    报告 → 纯文本:每条发现一行 [严重程度] [检查项] 说明, 末尾是统计和结论。CLI 和网页版都从这里取排版——两边永远不会打架。

    Report::verdict

    fn Report::verdict(self : Report) -> String

    一句话结论。检查项有限,没问题不等于万事大吉——措辞上 不替用户打包票。

    Section

    pub(all) struct Section {
    name : String
    records : Array[Record]
    }

    一个段。

    Severity

    pub(all) enum Severity {
    Info
    Warn
    Fatal
    }

    发现的严重程度。

    Subclass

    pub(all) struct Subclass {
    name : String
    tags : Array[Tag]
    }

    记录里的一段:由一个 100 子类标记开头的那些 tag。

    老版本(R10/R12)没有 100 标记,整条记录算作一个子类, 名字用记录类型名(见 subclass.mbt 的合成逻辑)。

    Tables

    pub(all) struct Tables {
    layers : Array[Layer]
    linetypes : Array[String]
    text_styles : Array[String]
    block_records : Array[String]
    }

    TABLES 段。

    Tables::layer

    fn Tables::layer(self : Tables, name : String) -> Layer?

    按名字找图层。

    Tag

    pub(all) struct Tag {
    code : Int
    value : TagValue
    }

    一个 tag:组码 + 值。

    Tag::is_record_start

    fn Tag::is_record_start(self : Tag) -> Bool

    组码是不是标志一条新记录(0)。

    组码 0 的值就是记录类型名——CIRCLE / LINE / SECTION / EOF。 整个文件的骨架就是靠它切出来的。

    Tag::is_subclass_marker

    fn Tag::is_subclass_marker(self : Tag) -> Bool

    组码是不是子类标记(100)。

    这是 DXF 最特别的地方:100 的值是一个类名(AcDbEntity / AcDbCircle…), 它一出现,后面组码的含义就换成那个类的定义了。 同一个组码 70 在 AcDbSymbolTable 里是「最大条目数」,在图层记录里是「标志位」。

    Tag::new

    fn Tag::new(code : Int, value : TagValue) -> Tag

    Tag::to_display

    fn Tag::to_display(self : Tag) -> String

    记录的显示用名字(组码 + 值),出错信息里用。

    TagScan

    pub(all) struct TagScan {
    tags : Array[Tag]
    warnings : Array[String]
    encoding : Encoding
    }

    一次扫描的结果。

    警告而不是异常:DXF 大量来自第三方导出器,坏文件比想象中多, 「读了 90% 然后告诉你哪里坏了」比「抛异常什么都没读出来」有用得多。

    TagStream

    pub(all) struct TagStream {
    // private fields
    }

    流式扫描器:feed 分块喂入,finish 收尾。

    tag 是「组码一行、值一行」的配对,配对可能横跨块边界——内部 缓冲没读完的行,feed 之间不用调用方操心。

    TagStream::encoding

    fn TagStream::encoding(self : TagStream) -> Encoding?

    文件编码;还没定(还没攒够 1024 字节)时是 None。

    TagStream::feed

    fn TagStream::feed(self : TagStream, chunk : Bytes) -> Array[Tag]

    分块喂入,返回这次新扫出来的 tag。

    头部攒够 1024 字节之前可能一个都不吐(在等编码判断)—— 这是设计,不是卡住了;finish 会把不足 1024 字节的文件收尾。

    TagStream::finish

    fn TagStream::finish(self : TagStream) -> Array[Tag]

    收尾:把缓冲里剩下的消费掉(最后一行没有换行符也算一整行), 报出「组码后面没有值」这类截断警告。之后 feed 再喂什么都返回空。

    TagStream::new

    fn TagStream::new() -> TagStream

    TagStream::warnings

    fn TagStream::warnings(self : TagStream) -> Array[String]

    扫描中攒下的警告(和 TagScan.warnings 同源)。

    TagType

    pub(all) enum TagType {
    Str
    I16
    I32
    I64
    F64
    Bool
    Bin
    }

    组码对应的数据类型。

    TagValue

    pub(all) enum TagValue {
    Str(String)
    I16(Int)
    I32(Int)
    I64(Int64)
    F64(Double)
    Bool(Bool)
    Bin(Bytes)
    }

    一个 tag 的值。

    有意做成强类型而不是「一律存字符串」:二进制格式里值本来就是分类型的, 而且取用时不用每次都重新解析文本(一张图几万个 tag,省下来的是实打实的)。

    TagValue::as_bool

    fn TagValue::as_bool(self : TagValue) -> Bool

    取布尔。整数非零为真。

    TagValue::as_double

    fn TagValue::as_double(self : TagValue) -> Double

    取浮点数。整数会转成浮点,其它类型返回 0。

    坐标组码理论上都是 F64,但现实里见过把整数写在坐标位上的文件, 所以整数也认。

    TagValue::as_int

    fn TagValue::as_int(self : TagValue) -> Int

    取整数。浮点会截断,其它类型返回 0。

    TagValue::as_string

    fn TagValue::as_string(self : TagValue) -> String

    取字符串。不是字符串时返回空串。

    Vec2

    pub(all) struct Vec2 {
    x : Double
    y : Double
    }

    平面上的一个点 / 向量。

    Vec2::add

    fn Vec2::add(self : Vec2, other : Vec2) -> Vec2

    Vec2::angle

    fn Vec2::angle(self : Vec2) -> Double

    极角,返回弧度,范围 (-π, π]。

    Vec2::approx_eq

    fn Vec2::approx_eq(self : Vec2, other : Vec2, tol : Double) -> Bool

    按容差判断是不是同一个点。

    Vec2::cross

    fn Vec2::cross(self : Vec2, other : Vec2) -> Double

    二维叉乘,返回一个标量(三维叉乘的结果是向量,二维退化成标量)。

    符号有几何含义:正表示 other 在 self 的逆时针一侧, 负表示顺时针一侧,零表示共线。判断点在直线的哪一侧、判断多边形绕向, 用的都是它。

    Vec2::distance

    fn Vec2::distance(self : Vec2, other : Vec2) -> Double

    两点距离。

    Vec2::dot

    fn Vec2::dot(self : Vec2, other : Vec2) -> Double

    点乘。

    Vec2::length

    fn Vec2::length(self : Vec2) -> Double

    模长。

    Vec2::new

    fn Vec2::new(x : Double, y : Double) -> Vec2

    Vec2::normalize

    fn Vec2::normalize(self : Vec2) -> Vec2

    单位化。零向量原样返回——退化几何(长度为零的线段)在真实图纸里存在, 不该让它把整个流程搞崩,交给检查环节去报。

    Vec2::perp

    fn Vec2::perp(self : Vec2) -> Vec2

    逆时针转 90 度的法向量。

    算「点在直线的哪一侧」和「线段往哪边偏移」时用。

    Vec2::rotate

    fn Vec2::rotate(self : Vec2, rad : Double) -> Vec2

    逆时针旋转 rad 弧度。

    Vec2::scale

    fn Vec2::scale(self : Vec2, k : Double) -> Vec2

    Vec2::sub

    fn Vec2::sub(self : Vec2, other : Vec2) -> Vec2

    self - other。

    Vec2::zero

    fn Vec2::zero() -> Vec2

    Vec3

    pub(all) struct Vec3 {
    x : Double
    y : Double
    z : Double
    }

    三维空间里的一个点 / 向量。

    DXF 的坐标组码都是三个一起出现(10/20/30 是 x/y/z), 所以解析出来的点一律是三维的,哪怕 z 恰好是 0。

    Vec3::cross

    fn Vec3::cross(self : Vec3, other : Vec3) -> Vec3

    Vec3::dot

    fn Vec3::dot(self : Vec3, other : Vec3) -> Double

    Vec3::is_zero

    fn Vec3::is_zero(self : Vec3, tol : Double) -> Bool

    按容差判断是不是零向量。

    Vec3::length

    fn Vec3::length(self : Vec3) -> Double

    Vec3::new

    fn Vec3::new(x : Double, y : Double, z : Double) -> Vec3

    Vec3::normalize

    fn Vec3::normalize(self : Vec3) -> Vec3

    单位化。零向量原样返回,理由和 Vec2::normalize 一样。

    Vec3::sub

    fn Vec3::sub(self : Vec3, other : Vec3) -> Vec3

    Vec3::xy

    fn Vec3::xy(self : Vec3) -> Vec2

    丢掉 z,投影到平面上。加工检查是二维的,用这个把三维点降下来。

    Vec3::zero

    fn Vec3::zero() -> Vec3

    angle_span

    fn angle_span(from : Double, to : Double) -> Double

    从 from 逆时针转到 to 跨过的角度,结果落在 [0, 2π)。

    arc_from_bulge

    fn arc_from_bulge(a : Vec2, b : Vec2, bulge : Double) -> BulgeArc

    bulge → 圆弧参数。

    返回值一律描述逆时针弧(和 ezdxf 的 bulge_to_arc 同款约定): bulge 为负时起止角已经对调,从 start_angle 逆时针走到 end_angle 覆盖的就是这段弧的点集。离散只关心点集,遍历方向在这里就消化掉。

    两个公式都是从源码核对的:圆心在弦中垂线上, r = 弦长·(1+b²)/(4b)(带符号——负 bulge 会自然镜像到另一侧), 方向角 = 弦方向 + π/2 − 2·atan(b)。

    arc_points

    fn arc_points(center : Vec2, radius : Double, start_angle : Double, end_angle : Double, tol : Double) -> Array[Vec2]

    圆弧 → 折线点列(含首尾两点)。

    build_drawing

    fn build_drawing(scan : TagScan) -> Drawing

    把 tag 流切成图纸骨架。

    绝不 panic:坏结构变成警告。「读了 90% 然后告诉你哪里坏了」 比「抛异常什么都没读出来」有用得多——真实世界的 DXF 大量来自第三方 导出器,损坏的文件比想象中多。

    circle_points

    fn circle_points(center : Vec2, radius : Double, tol : Double) -> Array[Vec2]

    整圆 → 折线点列,从 0°(正 x 方向)起步。

    不重复首点——闭合性用外层的 closed 标志表达, 和多段线顶点的表示一致。

    code_type

    fn code_type(code : Int) -> TagType

    查一个组码对应什么类型。

    区间照抄 ezdxf 的 lldxf/types.py(已核对)。没列到的组码一律当字符串—— 这不只是兜底:DXF 里「名字」类的字段散落在各个区间之外, 而字符串是唯一能安全容纳未知内容的类型。

    decide_encoding

    fn decide_encoding(data : Bytes) -> Encoding

    从文件开头判断该用什么编码。

    仅探测前1024字节;声明不在这个范围时会使用回退编码。

    decode_text

    fn decode_text(data : Bytes, enc : Encoding) -> String

    按指定编码把字节解成字符串。

    遇到不认识的代码页不静默乱码:降级成 Latin-1 并让调用方拿到一条警告。 乱码最坏的地方是它看起来「有内容」——用户会以为是文件本身有问题, 而不是工具不支持这个编码。

    ellipse_points

    fn ellipse_points(center : Vec3, major : Vec3, ratio : Double, start_param : Double, end_param : Double, tol : Double) -> Array[Vec2]

    椭圆(弧)→ 折线点列。

    点 = 圆心 + 长轴·cos p + 短轴·sin p——p 是参数,不是极角, 椭圆上等参数的点和等角度的点不是一回事。取到整椭圆时不重复首点。

    find_header_var

    fn find_header_var(sections : Array[Section], name : String) -> String?

    从 HEADER 段读一个变量。

    HEADER 变量没有 0 分隔,是成对挂在 SECTION 记录里的: 9 / $ACADVER 后面紧跟一个 tag 就是它的值。

    is_binary_code

    fn is_binary_code(code : Int) -> Bool

    是不是二进制块组码。

    is_handle_code

    fn is_handle_code(code : Int) -> Bool

    是不是句柄组码(5 / 105)。

    句柄是对象在文件内的唯一编号(十六进制字符串),330 之类的引用指向它。 105 是 DIMSTYLE 专用的句柄组码——同一个含义用了两个码,是历史包袱。

    is_point_code

    fn is_point_code(code : Int) -> Bool

    是不是坐标组码(10-39)。

    DXF 的坐标是成组出现的:10/20/30 是一个点的 x/y/z, 11/21/31 是第二个点,以此类推。规律是「个位数定轴、十位数定第几个点」。

    is_pointer_code

    fn is_pointer_code(code : Int) -> Bool

    是不是对象引用组码(软指针 / 硬指针)。

    值是另一个对象的句柄。330 是「所有者」,360 之类是硬引用。 靠这些把文件里散落的记录连成一张引用图。

    is_xdata_code

    fn is_xdata_code(code : Int) -> Bool

    是不是扩展数据(XDATA)组码(1000-1071)。

    第三方程序把自己的信息挂在实体上用的。解析时可以整段跳过, 但要原样保留——否则读一遍写回去就把别人的数据弄丢了。

    normalize_angle

    fn normalize_angle(rad : Double) -> Double

    把角度(弧度)归一到 [0, 2π)。

    DXF 里圆弧的起止角是度数、逆时针、而且可以大于 360 (370 度这种写法真实存在),所以算圆弧跨度之前必须归一到同一圈里, 否则 370 - 10 会算成 360 度而不是 0 度。

    parse_blocks

    fn parse_blocks(section : Section) -> Blocks

    解析 BLOCKS 段:每个 BLOCK 到 ENDBLK 之间的记录就是块的内容, 走和 ENTITIES 段同一套翻译(parse_records)。

    parse_entities

    fn parse_entities(section : Section) -> Entities

    解析一个段里的实体。

    入口是 ENTITIES 段;将来块定义(BLOCKS 段)里的实体也走这里。

    parse_header

    fn parse_header(section : Section) -> Header

    从 HEADER 段解析出变量表。

    走法:碰到 9 就开一个新变量,把后面所有不是 9 的 tag 全收进去, 直到下一个 9 或者段结束。不能按「两个两个一组」扫——坐标类的变量 有三个值,按对扫会把 y/z 丢掉,$EXTMIN 就只剩一个 x 了。

    parse_tables

    fn parse_tables(section : Section) -> Tables

    解析 TABLES 段。

    只认这四张表——别的一律跳过,但跳过是按结构跳的(认 TABLE/ENDTAB 的层次),不是按关键字。否则遇到没见过的表名会把它的内容当成外层记录。

    point_code_parts

    fn point_code_parts(code : Int) -> (Int, Int)?

    把坐标组码拆成 (第几个点, 哪个轴)。

    轴用 0/1/2 表示 x/y/z。不是坐标组码时返回 None。

    例:10 → (0, 0)、21 → (1, 1)、32 → (2, 2)。

    polyline_points

    fn polyline_points(vertices : Array[PolyVertex], closed : Bool, tol : Double) -> Array[Vec2]

    多段线顶点序列 → 折线点列(bulge 段换成圆弧点)。

    闭合的不重复首点;放开的一个不少,第一个点还是第一个顶点。

    scan_tags

    fn scan_tags(data : Bytes) -> TagScan

    把 ASCII DXF 的字节流切成 tag。

    碰到 EOF 就停——真实的 DXF 文件在 EOF 之后还可能有垃圾字节 (实测 ezdxf 的语料里就有 R12_with_trash_beyond_EOF.dxf), 那些不是数据,继续解会把噪声当成 tag。

    这是整份扫描的入口;分块扫描走 stream.mbt 的 TagStream。 两个入口共用同一套状态机(这里就是它的一次性封装), 行为不会有两套。

    severity_label

    fn severity_label(sev : Severity) -> String

    严重程度的中文标签。

    spline_points

    fn spline_points(degree : Int, knots : Array[Double], control_points : Array[Vec3], weights : Array[Double], tol : Double) -> Array[Vec2]

    样条 → 折线点列(xy 投影)。

    参数域是 [U[p], U[n+1]]——首尾各 p 个钳制节点的写法,端点正好是 首末控制点。在这个区间里自适应细分;数据不自洽的退回单点。

    spline_points_checked

    fn spline_points_checked(degree : Int, knots : Array[Double], control_points : Array[Vec3], weights : Array[Double], tol : Double) -> Result[Array[Vec2], String]

    将连续、正权重 NURBS 曲线投影到 XY 并离散。 无效节点、非正权重、非有限坐标、断裂曲线或资源上限返回错误。

    unit_name

    fn unit_name(code : Int) -> String

    单位编号的中文名。

    unit_to_mm

    fn unit_to_mm(code : Int) -> Double?

    一个单位编号对应多少毫米。没指定(0)或认不出时返回 None—— 不猜,猜错了尺寸差几百倍。