npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

text2cad-mcp

v0.16.4

Published

Natural-language driven CAD MCP server (2D/3D): generate part/assembly drawings and bridge to AutoCAD & CATIA. Auto-installs its Python backend into a managed virtualenv.

Readme

Text2CAD-MCP

契约驱动的通用 CAD 生成 MCP 服务。LLM 把设计结论写成一份 DesignSpec 契约(零件构成 / 装配约束 / 运动原理) → 输出 2D 工程图 + 3D 模型 + 可交互 Web 查看器,并桥接到 AutoCAD(DXF/DWG) 与 CATIA(CATPart/CATProduct)。

内核零品类硬编码。 加一个全新产品的唯一动作是再写一份 JSON 契约,src/ 一行不改。 判别标准:*换一个完全不同的产品,这段代码要不要改?*要改,就是把设计能力放错地方了。

职责边界:LLM 管设计,MCP 管制造 ⭐

LLM(设计方)                                    MCP(制造方)
零件分解、装配关系、运动原理       ──▶   造型 → 装配求解 → 2D 出图 → 3D 建模 → Web 交互
写成一个 DesignSpec JSON 契约            (产品差异 100% 来自契约内容)
                       ▲                                    │
                       └────── diagnostics 诊断回灌 ◀────────┘
                       (八闸给出的可操作修改建议)

只靠两个工具就能走通全流程:

| 工具 | 作用 | |---|---| | design_spec_schema() | 契约自描述:词表(图元/剖面/算子/组合子/约束/关节/出图)+ 规则 + 八闸说明 + 当前环境的 capability_gaps(哪些算子会被跳过/不可用/只能网格近似)+ 可运行示例 | | build_from_spec(spec_json) | 唯一通用生成入口:契约 → DXF/SVG(2D)+ STEP/STL(3D)+ 可交互 Web 查看器 + BOM/质量/干涉报告 + diagnostics |

契约规格见 docs/DESIGN_SPEC.md。

四份互不相关的契约(自行车 / 6 轴机器人臂 / 步枪机构 / 桌面台钳,共 45 个零件)由同一份内核、零 per-product 分支产出, 作为回归夹具放在 tests/fixtures/:

| 契约 | 覆盖的难点 | |---|---| | spec_bicycle.json | 极坐标阵列(辐条/齿)、sweep 管件、mirror 后三角、source 传动链、插接配合、多刚体(车架组 + 前叉组各自锚定) | | spec_robot_arm.json | point_to_point 相对约束链(除底座外零手算坐标)、parent 串链、prismatic 相位反向、cut 减重孔 | | spec_ak47.json | 18 零件完整总装、sweep 弧形弹匣、translate+rotate 组合搭枪托/握把、parent + mode="oscillate" 自动机循环 | | spec_bench_vise.json | 螺旋传动(丝杠 revolute + 活动钳口 prismatic)、linear 阵列导杆、丝杠与手柄同轴同步回转 |

能力边界(两层)

1. 契约通道(⭐ 任意产品,主推) —— LLM 写 DesignSpec JSON → build_from_spec:

  • 零件几何是声明式 op 树({"op": …} 递归求值):8 种图元 + 7 种剖面算子(extrude 含 taper/twist、revolve、sweep、loft、shell、fillet、chamfer)+ 7 种组合子(union/cut/intersect/ translate/rotate/mirror/pattern)+ 通用修饰键(at/rot/mirror/pattern)。
  • 装配用相对约束表达:fixed / coaxial / coincident / distance / angle / point_to_point / pattern, 配合零件自声明 anchors,可以一个绝对坐标都不写。
  • 运动用关节表达:revolute(含往复摆动 mode="oscillate")/ prismatic / orbit / slider_crank / screw。source 表达传动比链,parent 表达结构串链(下游自动跟随上游)。
  • 出图:drawing.views(front/back/top/bottom/left/right 或自定义 eye/right)+ 尺寸 + 标题栏。

2. 构件级通用层 —— 只需单个零件、不想写整份契约时更直接:

  • generate_part —— sketch 基元(rect / circle / line / polyline / arc)任意组合 → DXF。
  • generate_3d_part —— extrude(含孔)/ revolve / tube(两点管段)→ STEP/STL。
  • validate / solve_assembly_layout / kb_search / list_standard_parts —— 老 PartSpec/AssemblySpec 链路的校验与布局工具,仍保留可用。

几何后端(可插拔 + 能力协商)

同一份契约可以跑在不同几何内核上;换后端不换结果,只换速度与保真度。

| 后端 | 依赖 | 能力 | 定位 | |---|---|---|---| | manifold | manifold3d(默认核心依赖,原生约 2 MB,只依赖 numpy) | 精确布尔、拉伸/旋转/扫掠/螺旋、输出保证流形、流形/亏格自检 | 全新安装即可 3D 建模,速度比 cadquery 快 7~25 倍 | | cadquery | cadquery(可选,重) | B-rep 实体、STEP/IGES、圆角/倒角/抽壳/放样 | 需要可编辑实体或精确特征时安装 |

调度方式:TEXT2CAD_BACKEND=manifold|cadquery 显式指定;不设则按能力权重自动挑最强的。 能力协商:后端缺某个算子时降级而不失败 —— 软算子(fillet/chamfer/shell)静默跳过并在 result.degraded 记账;硬算子(loft)明确报错。design_spec_schema() 的 capability_gaps 会把当前环境的缺口提前告诉 LLM。

pip install manifold3d          # 默认内核(已随包安装,通常无需手动执行)
pip install "text2cad-mcp[three-d]"   # 或 pip install cadquery —— 解锁 STEP / 圆角 / 抽壳

元素选择:select 谓词 + anchors 特征拾取(P3)

不用写死坐标,让 MCP 从真实几何里算出要的那一组元素:

// ① 倒角/抽壳要"哪几条边/哪几个面" → 几何谓词(cadquery 精确;网格后端跳过并记账)
{"op":"chamfer","of":{…},"distance":1,
 "select":{"kind":"edges","type":"circle","r_near":8}}   // 只倒 Φ8 孔的口

// ② 装配锚点不想算坐标 → 特征拾取(planar_face / hole_axis / cylinder_axis)
{"anchors":{"seat":{"pick":"hole_axis","dir":"z","r":14}}}
  • 谓词支持 kind / type / parallel / normal / at_max / at_min / x_ge … / within_box / r_near, 多键之间是与的关系。
  • 特征拾取 B-rep 后端为精确解(实测 0 误差),网格后端是网格近似(返回值带 approx=true)。
  • 诚实性纪律:信息不足(不给 r 而存在多根轴)或压根没有该特征时返回 None, 绝不编造一个"看起来合理"的点 —— 静默取错点比报错危险得多。

为什么不用"面的稳定编号":那会引入参数化 CAD 著名的拓扑命名问题(TNP)。 本仓库的 op 树每次全量重算,本来就没有 TNP,所以借的是"按几何性质选元素"这个 表达力,而不是 TNP 的缓解机制。

确定性八闸(L0 → L3,外加闸 7 公差、闸 8 可制造性)+ 诊断回灌

每次生成都会过八道闸,结论在返回值的 gates 里逐条列出:

| 闸 | 层级 | 检查内容 | |---|---|---| | 1_schema | — | 契约词表 / 引用 / 锚点 / id 合法性。有 error 则不生成任何几何。 | | 2_kinematics | — | 约束是否全部解出;每个子装配必须有 fixed 锚定件(否则绝对位置无定义);未参与约束而停在原点的零件。并入约束图分析:约束回代残差(过约束矛盾)、环秩(冗余约束)、无基准子装配。 | | 3_geometry | — | 逐零件几何自检(空实体 / 非流形 / 无效体)+ 装配干涉(包围盒预筛 + 精确求交体积)。 | | 4_design_calc | L1 选型 | 契约参数 vs KB 经验公式(轴径 / 扭矩 / 传动比自洽)。需求(L0 requirements)缺失时静默跳过,不编造。并入计算深度扩展:契约显式声明的 calculations 由 KB 公式库(kb/calc/*.yaml)求值——轴承寿命 / 螺栓预紧 / 弹簧 / 带传动 / 焊缝 / 动平衡 / 电机损耗 / 过盈传扭,加一种计算只加 YAML;缺输入记 missing_input 并点名缺哪个符号。 | | 5_motion_sweep | L3 全行程扫掠 | 运动链按驱动器相位采样(默认 24 点、覆盖含传动比展开的完整周期),逐相位复算世界位姿后对候选对求交——静态检查照不到的"运动中咬死"(平衡重扫过活塞裙、连杆摆到极限撞缸体……)。 | | 6_part_standards | L1b 零件标准 | 按 part.kind 从 KB part_standards/ 拉适用设计标准(强度 / 公差 / 重量 / 工艺 / 成本五维),逐项把契约值 / 几何反推值与公式要求比对。error 折入 ok(强度类),warning 不折(优化建议)。 | | 7_tolerance | L1c 公差配合 | 孔轴配合(GB/T 1800.1 / ISO 286)极限间隙与过盈、过盈接触压力(厚壁筒 Lamé)、尺寸链闭环(极值法 / RSS)、GD&T 基准完整性、IT 精度工艺可达性。KB 未收录的代号/尺寸段记 skipped 不插值。 | | 8_dfm | L1d 可制造性 | 特征规则(最小孔径 / 深径比 / 壁厚 / 孔壁 / 圆角 / 薄壁长厚比,门槛按工艺族取)+ 射线量测(真实几何量壁厚、孔壁,超预算如实跳过)+ 装配可达性(沿装入方向扫掠;声明 dfm.assembly_direction 才判错)。外购件不套自制工艺门槛。 |

ok=True 要求八闸全绿 —— 避免"看起来成功其实装错了"。

闸门覆盖率:ok=True 不等于"检过了"

闸 4/6/7/8 依赖契约显式声明(requirements / calculations / tolerances / dfm.process),契约没写就会大面积跳过。诚实性要求"没输入就不编造结论", 但只做到一半 —— 还得告诉调用方我什么都没查,否则 LLM 拿到全绿会以为设计被全面检视过。

返回值因此带 gate_coverage,逐闸给出:

"gate_coverage": {
  "gates": {
    "8_dfm": {"checked": 42, "skipped": 8, "rate": 0.84,
              "fixable_skipped": 4,      // 补契约/补 KB 就能激活
              "structural_skipped": 6,   // 外购件,本就不该套自制门槛
              "reference_skipped": 0,    // KB-only 设计参考(不计入分母)
              "status": "active", "top_reasons": ["三角面…超过预算…(1 条)"]}
  },
  "active_gates": ["6_part_standards", "7_tolerance", "8_dfm"],
  "idle_gates": ["4_design_calc"]
}
  • 闸 1-3 是基础闸(有没有零件都跑,不会空转),这里只统计 4/6/7/8。
  • 空转的闸会在 diagnostics 里补一条 info 级提示(不折 ok,输入缺失不是设计缺陷), 点名该补哪段契约。
  • KB-only 参考条目不计入分母 —— 否则往 KB 里多写参考标准会让覆盖率不升反降,指标就没意义了。
  • 只看 checks 会低估 DFM:它只在不合格时写 checks,通过的留痕在 evaluated。

实测(BLDC64 14 件 / 陀飞轮 20 件):闸 7 公差 100% / 81%,闸 8 DFM 84% / 71%, 闸 6 零件标准 4% / 14%(受零件类别覆盖度限制,见下)。

缓存诚实性:产物快照的指纹覆盖导出器 + 各闸分析模块 + 编排器自身源码 + 整个 KB 的内容哈希。 改一行 YAML 门槛、改一版判定逻辑,旧快照立刻失效 —— 否则会出现"升级后还在返回上一版体检报告"。

扫掠闸的两条诚实性约定:

  • 包围盒预筛 + 预算上限(默认 20000 次精确求交);超预算如实报告 complete=false, 不假装查全。V6 发动机(31 件 / 17 运动件)全量实测约 1.2 万对,可完整查全。
  • 采样用的关节变换与 Web 查看器逐分支一致(闭式解同源);判定用交集体积 (重叠 > 0.5 mm³ 记命中),共面 / 数值噪声不误报。

失败时返回 diagnostics:[{gate, level, where, what, hint}, …],每条 error 都带可操作的修改建议, 外加一句 next_step。LLM 直接照着改契约重跑即可闭环,不需要猜。

零件设计标准库(kb/part_standards/)

零件级检视(闸 6)的标准是数据不是代码:kb/part_standards/<类别>.yaml 按零件类别组织, 每条标准登记 dimension(strength / tolerance / weight / process / cost)、check(求值器注册名)、 inputs(数据来源映射)、params(KB 系数)、formula、source、hint。新增零件类别 = 加一个 YAML。 kinds 支持多值(一份标准覆盖语义相近的多个类别,如 panel 同时管 plate/cover/frame/bracket, bolt 同时管 bolt/screw),不必为每个类别复制一份文件。 当前已登记 7 类 22 条(shaft / gear / bolt / bearing / housing / panel / …),材料强度表在 kb/materials.yaml。

设计期 LLM 可用 kb_search 按需检索(返回整条标准,含公式与来源),例如 kb_search("轴 扭转 强度")、kb_search("轴承 配合 公差");KB-only 标准(契约无对应可反推输入, 如公差配合推荐)在闸 6 里如实记 skipped 并引导检索——没有输入就不假装检视。

诊断 → 优化:不合格时给反解建议

光说"不合格"还不够 —— 设计者要的是"改成多少"。求值器在判定时会对同一个公式做代数反解 (不另立经验系数),把建议值一并写进结论:

{"check": "shaft.torsion_strength", "level": "error",
 "msg": "lead_screw 的 shaft.torsion_strength:契约值 195.3MPa,公式值 1856MPa(低于 -89.5%)",
 "fix": {"param": "diameter_mm", "value": 59.31, "current": 28.0, "note": "轴径"},
 "hint": "… 按同一公式反解:轴径 建议取 59.31 mm(当前 28)"}

τ = 16T/(πd³) ≤ [τ] 反解出 d ≥ (16T/(π[τ]))^(1/3) —— 已随轴扭转强度与板件跨厚比落地, 其余标准按同一模式补 fix 即可(是求值器内部的代数变形,不是新增一套数)。

一键安装(npm,推荐 ✅)

发布为 npm 包 text2cad-mcp,会自动检测 Python ≥ 3.10、在 ~/.text2cad/venv 建虚拟环境、 pip install 后端依赖(含默认几何内核 manifold3d),然后以 stdio 启动 MCP 服务。 客户端 Agent 无需手动配置任何 Python 环境。

{
  "mcpServers": {
    "text2cad": {
      "command": "npx",
      "args": ["-y", "text2cad-mcp"]
    }
  }
}

也可全局安装后用命令名直接启动:npm install -g text2cad-mcp,配置里写 "command": "text2cad-mcp"。

可选环境变量(一般无需改):

| 变量 | 作用 | 默认 | |---|---|---| | TEXT2CAD_HOME | venv 缓存根(不影响产物位置) | ~/.text2cad | | TEXT2CAD_OUTPUT_DIR | 产物输出目录 | 桌面/outputs/ | | TEXT2CAD_KB_DIR | 领域知识库目录(默认用包内自带;指向外部目录可追加自有知识) | 包内 src/text2cad/kb | | TEXT2CAD_TEMPLATE_DIR | 国标标注模板目录 | 包内 src/text2cad/templates/gb | | TEXT2CAD_DXF_VERSION | 默认 DXF 版本 | R2007 | | TEXT2CAD_BACKEND | 强制几何后端(manifold / cadquery) | 自动挑最强 |

输出组织(同名产品固定一个文件夹)

每次生成都会在输出根目录下确定一个 产品文件夹,该产品的全部产出都归入其中:

桌面/outputs/
├─ 6轴串联工业机器人手臂_20261011_211500/
│  ├─ <产品名>.dxf            # 2D 多视图工程图(AutoCAD 2007+ 可开)
│  ├─ <产品名>_views.svg      # 2D 预览
│  ├─ <产品名>.stl            # 3D 网格
│  ├─ <产品名>_3d.html        # 可交互 Web 查看器(含按运动原理联动)
│  ├─ <产品名>.step           # 3D B-rep 实体(需 cadquery)
│  ├─ <产品名>_spec.json      # 回写的契约(可追溯本次生成的确切输入)
│  ├─ parts/                  # 逐零件 STEP(每件旁带 .fp 几何指纹)
│  └─ bom.json                # 物料清单(含质量估算)
└─ …

目录命名有两种模式,由 TEXT2CAD_DIR_MODE 控制:

| 模式 | 行为 | 适用 | |---|---|---| | stable(默认) | 同名产品始终复用同一目录并覆盖写 | 反复调参迭代——桌面上只有一份产物 | | timestamp | 每次新建 名称_时间戳 目录 | 需要并排保留多个版本做对比 |

stable 模式下,上一轮有、这一轮没有的零件文件会被挪进 parts/.stale/(只挪不删), 不会被误当成本轮产物。

早期默认是 timestamp,导致一次调参就在桌面堆出七八个几乎一样的文件夹。 配合下节的零件级缓存,stable 才让"改一处、只重算一处"真正成立。

桌面路径会被自动探测(含 OneDrive 重定向与中文「桌面」),探测不到时回退到用户主目录。

增量生成:零件级几何缓存

契约是纯函数 DesignSpec → 几何,没有持久化模型状态。这是刻意的取舍——单份 JSON 就是完整真相(可 diff / 可回滚 / 可并行 / 免疫拓扑命名漂移),代价是每次都要全量重算。 缓存补的正是这一刀:契约仍然无状态,但几何多了一层中间结果缓存。

三道闸门,输入越像上次就越省:

| 闸门 | 判据 | 命中时省掉 | |---|---|---| | 整单复用 | 契约逐字符相同(fast_key) | 一切,直接返回上次结果(毫秒级) | | 产物复用 | 零件几何 + 位姿 + 图纸配置相同(art_key) | 全部建模 / 投影 / 导出 | | 零件复用 | 单个零件的 op 树 + 后端相同(几何指纹) | 该零件的求值、三角化、STEP 导出 |

指纹是内容寻址的:SHA256(规范化 op 树 + 后端名),改参数指纹就变,旧条目自动失效。 三角化结果额外按网格容差建键;干涉结论按"零件指纹 + 世界位姿 + 允许接触集"建键, 且只缓存查全了的结论(提前早停的不缓存,免得把"没查完"固化成"没问题")。

整单/产物两级指纹还额外掺入一份导出器代码指纹(_exporter_fingerprint():把 assembly_viewer / project2d / glb_writer / shading / appearance 等产物写出模块的 源码内容哈希进去)。原因很实际:渲染管线一升级,同一份契约仍会命中旧快照、把上一版外观 原样返回——命中率好看,交出去的却是旧样子。这类变化不体现在零件几何里,只能靠代码指纹兜住。

存储分两级:进程内存(MCP 常驻,同进程重复请求零成本)+ 磁盘 (%LOCALAPPDATA%/text2cad/cache,跨进程有效,LRU 上限 TEXT2CAD_CACHE_MAX_MB,默认 4 GB)。 序列化由各后端自己负责——B-rep 走 OCCT 原生 BREP(实测往返体积位级一致), 网格后端走 npz 顶点/索引。读回后一律比对体积,往返不等价就当作没命中重新求值。

任何缓存异常都静默降级为未命中,绝不让缓存把生成搞失败。排查时可整体关闭:

TEXT2CAD_CACHE=0          # 关闭缓存
TEXT2CAD_DIR_MODE=timestamp   # 回到每次新建目录
TEXT2CAD_ICE_MAX_PAIRS=800    # 放宽干涉精确求交的预算

实测(2.0T V6 / 31 零件 / 201k 三角面 / cadquery 后端):

| 场景 | 改造前 | 改造后 | |---|---|---| | 契约没变,再跑一次(同进程) | ~66 s | 0.02 s | | 只改一个零件(皮带轮轮毂) | ~66 s | 35–38 s(30/31 件命中,STEP 全部沿用) | | 冷启动(空缓存) | ~66 s | 58–66 s(多出的部分是写缓存) | | 桌面 V6 文件夹 | 7 个 / 177 MB | 1 个(覆盖写) |

改一件仍要 35 秒,是因为总装图纸必须重投影:2D 消隐要拿全部 201k 三角面重新算 遮挡关系,这一步与"改了哪一件"无关。已把 5 个视图各自的顶点去重合并成一次 (24.0 s → 17.8 s,逐段位级一致);再往下就要动消隐算法本身了。

产物级增量:连图纸都不重画

几何缓存只省掉"建模",出图仍然是逐零件重算的——而一张零件图只依赖它自己的网格、 元数据、公差/表面要求与图框。所以这一层缓存的是文件字节,键是这些输入的内容指纹:

零件图(DXF/SVG):mesh 指纹 + 材料/图号/数量/工艺/注释 + 公差 + 表面 + 形位公差
                  + 图框 + 质量档 + 绘图代码指纹
装配视图:视图配置 + 各零件 mesh 指纹 + 质量档 + 剖切 + 绘图代码指纹

命中就直接写回上次的字节,跳过投影与 ezdxf 序列化;还原时逐文件校验 sha256, 校验不过即当作未命中(绝不用可疑字节冒充结果)。任何异常都静默降级为"没命中"。

实测(SS250 / 34 件 / manifold 后端,改一个曲柄臂半径):

| 场景 | 关掉增量复用 | 开启后 | |---|---|---| | 改一个零件参数 | 38.3 s | 22.2 s(1.73×) | | 只改公差表数值 | 13.1 s | 13.2 s,其中 2D 投影 5.4 s → 0.04 s | | 冷启动(空缓存) | — | 39.3 s |

关掉这一层做 A/B 或排查"是缓存还是算错了":

TEXT2CAD_INCREMENTAL=0    # 只关产物/视图缓存,几何缓存仍然生效

为什么不做"按零件增量投影总装图":消隐是全局的——某条边看不看得见取决于 前面所有零件,拆不开。能增量的是单零件图纸(已经做了),不是总装投影。 总装视图只能靠"输入完全一致"整段复用,以及后续对消隐本身做向量化。

外部路径边界(沙箱)

render_preview / export_dwg / export_catpart / build_catproduct / export_freecad_step / cae_export / plm_export / open_design_studio 都吃调用方 给的路径。单机自用无妨,一旦作为服务挂给别人用,等于把整台机器的读写权限交出去。

TEXT2CAD_ROOT="/data/cad;/mnt/share"   # 多个根用系统路径分隔符
  • 未设置时完全不限制(保持原有行为,不给出图添堵);
  • 设置后只允许访问这些根目录,.. 穿越一并拦住,越界返回明确的诊断而不是裸报错;
  • 自己生成的产物目录(<桌面>/outputs 或 TEXT2CAD_OUTPUT_DIR)始终允许读回。

依赖可复现(lock)

requirements.txt 里的 >= 只是下限,上游一次小版本升级就可能换掉行为。 requirements.lock 钉住实机验证过的完整依赖闭包(含传递依赖、带平台标记):

python scripts/gen_lock.py          # 重新生成
python scripts/gen_lock.py --check  # 校验 lock 与当前环境一致(CI 用)

启动器首次安装时先按 lock 装依赖,再 pip install --no-deps 装本包—— 避免每次安装都重新解析一遍 >=。lock 里只锁版本不锁 hash:跨平台 wheel 哈希不同, 一份带 --hash 的 lock 在别的平台会直接装不上(需要强校验请按平台分别生成)。

代码保护:哪些公开、哪些编译

发布包会把 src/ 原样带出去。python scripts/protect.py report 给出暴露面账本:

合计 67 个 .py,28129 行
  其中受保护 26 个 / 13833 行 —— 编译后应从包里消失
  明文保留 10 个 / 4528 行 —— 接口层
  其余 31 个 / 9768 行 —— 导出桥/后端适配/studio 等

分层原则是契约是接口、实现才是秘密:工具签名与 DesignSpec 保持明文(生态入口), 八闸规则、消隐算法、公差推理、质量特性积分等编译成 .pyd/.so。

pip install cython                      # 构建机需要 C 编译器
python scripts/protect.py build --marker "acme-2026-11"
npm pack build/protect                  # 发布受保护产物

完整策略(含许可证选择、KB 数据处理、薄启动器方案、水印溯源)见 PROTECTION.md(仓库根目录,不随 npm 包发布)。

从 0.16.0 起本项目的许可证已由 MIT 改为专有(All Rights Reserved), 详见 LICENSE。0.16.0 之前的版本曾以 MIT 发布,其已分发副本仍受 MIT 约束——这一点无法追溯撤销,只能靠撤回旧版本 + 新版本专有化来止血。

长任务与可观测性

长任务:为什么不能同步等

build_from_spec 是同步工具。整机量级实测十几分钟,MCP 的请求-响应模型撑不住 ——客户端会超时、会话会卡死。所以另有一条不阻塞的通道:

| 工具 | 作用 | |---|---| | submit_build(spec_json, ...) | 提交生成,立即返回 job_id | | job_status(job_id) | 查 state / progress(0~1) / stage / elapsed_s;留空则列最近任务 | | job_result(job_id, wait_seconds=0) | 取完整结果(与 build_from_spec 同构);wait_seconds>0 可阻塞等一会 | | job_cancel(job_id) | 请求取消 |

submit_build(spec)        → {"job_id": "a1b2c3d4", "state": "queued"}
job_status("a1b2c3d4")    → {"state": "running", "progress": 0.52, "stage": "3d"}
job_result("a1b2c3d4")    → 完整结果(ok / dxf_path / step_path / gates / diagnostics …)

取消是协作式的,这一点必须说清楚:Python 无法从外部强杀线程,job_cancel 只置 标志,任务在下一个阶段边界(contract → geometry → mates → mesh → 3d → 2d → web → checks)才停。所以返回 cancelling 只代表"受理了",要再看 job_status 确认变成 cancelled。已在跑的单个阶段不会被打断。

build_from_spec 本身也升级了:它会按阶段发 notifications/progress(客户端需带 progressToken)。不支持进度通知的客户端,就用上面的轮询通道。

日志:默认安静,需要时看得见时间线

全部走 stderr(stdout 是 MCP 协议通道,写一个字节就破帧)。每次工具调用一个 run_id,结束时落一条汇总,含各阶段耗时、缓存命中、降级原因:

INFO [443f62c0] done tool=build_from_spec ok=True elapsed_ms=31690.7
    steps: contract=1.5ms  geometry=321.6ms  mates=23.8ms  mesh=1896.1ms  3d=2609.8ms  2d=6822.7ms  web=2611.4ms  checks=6901.1ms
    counters: parts=18  instances=18  parts_built=0  parts_cached=18
    degraded: fillet(后端不支持,已跳过)

| 环境变量 | 默认 | 说明 | |---|---|---| | TEXT2CAD_LOG_LEVEL | INFO | DEBUG / INFO / WARNING / ERROR / OFF;DEBUG 会逐阶段打 | | TEXT2CAD_LOG_FORMAT | text | json 输出一行一条,便于 grep / jq / 采集 | | TEXT2CAD_LOG_FILE | 空 | 额外落盘路径 | | TEXT2CAD_JOB_WORKERS | 1 | 并发执行的后台任务数(几何受 GIL 限制,默认 1 更可预测) | | TEXT2CAD_JOB_KEEP | 50 | 保留的已结束任务数(内存上限) |

上面那组 steps 就是排查"到底慢在哪"的入口。实测热缓存下几何只要 0.3 秒, 真正的大头是 2D 投影和八闸校验(各占三成左右),这与"零件太多"无关。

快速开始(从源码开发)

以下仅面向本机开发/调试。终端用户请用上面的 npm 一键安装方式,无需接触源码。

python -m venv .venv && .venv\Scripts\activate      # Windows
pip install -e .                                    # 核心:mcp + ezdxf + pyyaml + manifold3d

python -m text2cad.server                           # 启动 MCP 服务
python tests/run_all.py                             # 零依赖跑全套测试(equiv/gates/e2e/smoke)
python -m pytest tests -q                           # 或走 pytest

tests/test_mcp_smoke.py 用运行它的解释器启动 server,也可用环境变量 TEXT2CAD_PYTHON 指定。

MCP 客户端配置见 config.example.json(源码 / Python 方式)或 config.example.npm.json(npm 一键方式,推荐)。

Studio 可视化编辑页

对已有设计目录(含 params.json 与参数化生成器)起一个本地 Web 编辑页:

python run_studio.py designs/engine250        # 默认 8765 端口并自动开浏览器
python run_studio.py designs/ss250 --port 8766 --quality high
  • 参数手柄:拖动滑块 / 改数值 → 三视图与 3D 实时重算(内存试算,不写盘)
  • 试算 / 保存分离:改动先落 dirty 标记,点「保存到设计文件」才写回 params.json;「确认生成」提交前会隐式保存,保证产物与参数一致
  • 八闸复检:每次生成走完整八闸流水线,绿灯即交付
  • 细节见 docs/STUDIO.md(含 /api/patch、/api/save、/api/commit 接口语义)

任意契约直接可编辑(propose_spec → open_spec_studio)

上面的编辑页要求先有 params.json 和手写生成器。这对成熟设计是对的——生成器里才装着 "缸径 → 活塞直径"这类派生关系;但对"刚出契约、想先让人确认参数"太重:还没确认就要求先写 一个生成器,等于把确认推到了成本之后。

所以推荐的工作流是先体检、再开页:

# 第 1 步:体检——不跑几何、不启服务,返回可确认性盘点 + 顺手备好编辑目录
propose_spec(spec_file="outputs/xxx/spec_xxx.json")
# → readiness(几何/运动/演示层/交互层/参数/公差/表面各块做没做)
# → ask(哪些是推断出来的、哪些没做需要人拍板)
# → deliverables(可选产物清单)

# 第 2 步:确认参数——把可调页面推到用户面前
open_spec_studio(spec_file="outputs/xxx/spec_xxx.json")
# 或本地:python run_studio.py outputs/xxx/spec_xxx.json

参数表按契约的 parameters[].set(JSON 指针)自动生成;契约没写 parameters 时, 引擎会挖一层"共享尺寸"候选:同一个字段同值出现在 ≥2 个零件里(如六扇门共有的 h=2100), 多半就是一个可调尺寸。候选进 自动推断(共享尺寸) 分组,与设计方声明的参数分得开, 且只保证数值一致、不保证设计协调——门洞变宽时门扇不会自动跟着变。 三条硬边界:

  • 只挖尺寸类字段(w/d/h/r/厚/径),不碰 translate.at 坐标——改坐标能让零件错位
  • 一次出现不算证据:只在单个零件出现的尺寸不会被挖
  • 名字诚实:挖不出语义就写"共享高度 2100(6 处)",不编造"门扇高"

编辑页本身(_edit_<契约名>/,只读底稿 + params.json + 生成物,只听 127.0.0.1):

  • 改一个尺寸秒级看到 2D + 3D:只重算受影响的零件(实测:电梯改"轿门高度"0.6 s 重建 11 件; 无刷电机改机壳外径 3.1 s)
  • affects 自动派生:从指针路径直接说出"改这个会重建哪几件",不用手写影响范围
  • 回写是纯函数:改的是契约副本,底稿 spec_src.json 永远不动,"重置"回得去
  • 只改已经存在的数:指针取不到值就记 issue 跳过,不新建层级
  • 产物勾选:确认生成时只出勾选的那几种(总装图/零件图/STEP/GLB/网页/BOM/CAE/PLM)

没写 parameters 且没有可推断的共享尺寸时,只剩运动量(关节行程 + 驱动转速)可调。 "能打开编辑页"不等于"有东西可调"——要让用户确认尺寸,就在契约里写 parameters, 写法见 docs/DESIGN_SPEC.md 第 18 节。

目录

仅供开发参考。终端用户通过 npm 安装后无需关心内部结构。

text2cad_mcp/
├─ bin/text2cad-mcp.js     # npm 启动器(建 venv、装依赖、起服务)
├─ src/text2cad/
│  ├─ spec.py              #   DesignSpec 契约层(词汇表唯一真源 + 校验 + 自描述)
│  ├─ studio/              #   可视化编辑页(参数手柄 → 试算/保存分离 → 确认生成)
│  ├─ spec_pipeline.py     #   build_from_spec:契约 → 零件 → 装配 → 2D/3D/Web + 八闸 + 诊断
│  ├─ design_calc.py       #   闸4 L1 选型计算(KB 经验公式对照,宁缺勿编)
│  ├─ calc_engine.py       #   闸4 计算深度:KB 公式库求值器(安全 AST,零 eval)+ 单位换算
│  ├─ family.py            #   参数化族:{{占位}} 展开 + 变型批量生成 + 系列对比表
│  ├─ plm_export.py        #   PLM/ERP:物料主数据 + 多级 BOM(项号/版本)+ ECN
│  ├─ cae.py               #   质量特性(散度定理积分)+ Abaqus/Nastran 输入卡导出
│  ├─ part_review.py       #   闸6 L1b 零件级设计标准检视(按 kind 分派 KB 标准)
│  ├─ tolerance.py         #   闸7 公差与配合:孔轴配合/过盈接触压力/尺寸链闭环/形位基准
│  ├─ dfm.py               #   闸8 可制造性:特征规则 + 射线量测壁厚/孔壁 + 装配可达性扫掠
│  ├─ constraint_graph.py  #   闸2 补充:约束回代残差/环秩/自由度掩码/无基准子装配
│  ├─ sourcing.py          #   可采购 BOM:物料编码 + 工艺路线 + 成本估算 + 装配树展开
│  ├─ agent_loop.py        #   design() 唯一入口(只编排,零品类知识)
│  ├─ geometry/
│  │  ├─ backend/          #   可插拔后端:base(协议+注册表)/ selector / common / manifold / cadquery
│  │  ├─ opgraph.py        #   纯调度器:契约 op 树 → 后端实体,零内核细节
│  │  ├─ mates.py          #   装配约束求解(相对位姿传播)
│  │  ├─ motion.py         #   运动编译(source 传动链 / parent 串链)
│  │  ├─ sweep.py          #   闸5 L3 全行程扫掠干涉
│  │  ├─ standard_parts.py #   L2 标准件几何算子
│  │  ├─ cache.py          #   内容寻址几何缓存(三级指纹 + 磁盘 LRU)
│  │  ├─ glb_writer.py     #   零依赖 glTF 2.0 写出器(GLB)
│  │  ├─ shading.py        #   折边角顶点劈分(硬边看起来是硬的)
│  │  └─ appearance.py     #   按 part.kind 的 PBR 材质分档(GLB 与查看器同一份)
│  ├─ drawing/             #   project2d(投影+消隐)/ assembly_viewer(Web 查看器)/ freecad_export(FCStd+TechDraw)/ autocad_export + catia_export(COM 桥接)
│  ├─ knowledge.py         #   领域知识库(目录自动装载 + 通用树检索)
│  ├─ kb/                  #   知识库 YAML(标准件 + 经验公式 + part_standards/ + tolerance/ 公差表 + dfm/ 工艺门槛 + calc/ 计算公式库 + manufacturing/ 单价费率 + plm/ 编码规则 + constraint/ + materials.yaml)——打包进 wheel
│  ├─ templates/gb/        #   国标标注模板(GB/T 131/1182/324)——打包进 wheel
│  ├─ models.py            #   构件级模型(PartSpec / AssemblySpec)——只服务单零件场景
│  ├─ deps.py              #   依赖自检 + 就绪等级
│  └─ server.py            #   MCP 工具入口(27 个工具)
├─ tests/                  # 回归测试(fixtures/ 契约 + 24 个套件 + run_all.py)
├─ scripts/                # 开发辅助:gen_v6_engine.py(V6 参数化契约)/ run_v6_check.py(八闸复跑)
├─ run_studio.py           # Studio 编辑页启动器(python run_studio.py <设计目录>)
└─ docs/DESIGN_SPEC.md     # 契约规格(LLM 写契约的权威参考);docs/STUDIO.md 为编辑页说明

依赖说明与自动自检

原则:有的直接用,缺的才提示一次。

| 依赖 | 用途 | 必需 | |---|---|---| | mcp / fastmcp | MCP 服务(stdio) | 核心 | | ezdxf | 完整工程图 DXF(多视图/尺寸标注/中心线/标题栏) | 核心 | | pyyaml | 知识库 YAML 读取 | 核心 | | manifold3d | 默认几何内核(布尔/拉伸/旋转/扫掠/螺旋 + 流形自检) | 核心 | | cadquery | 可选 B-rep 内核:STEP/IGES、圆角/倒角/抽壳/放样 | 可选 | | pywin32 | COM 自动化:AutoCAD 桥接(DWG)+ CATIA 桥接(CATPart/CATProduct) | 可选 | | pycatia | CATIA 高层 Python 封装(有 pywin32 就非必须) | 可选 | | matplotlib | DXF → SVG/PNG 预览渲染 | 可选 | | FreeCAD 1.0+(可执行文件,非 pip) | .FCStd(可继续编辑)+ TechDraw 工程图 | 可选 |

FreeCAD 不是 Python 包,pip install 装不到 —— 它是带自己 Qt/OCCT 的独立程序,只能当子进程调用。 所以它不会出现在 pip 安装命令里,缺失时给出的是一条手动安装说明 + TEXT2CAD_FREECAD_CMD 环境变量指引。 没装的影响见下面「FreeCAD 未安装时缺什么」。

调用 check_environment() 即可拿到各能力可用性、一次性安装命令与就绪等级: FULL(B-rep + CATIA / AutoCAD 原生)/ B_REP / MESH_3D / 2D_ONLY / MINIMAL。 FULL 也会如实标注 FreeCAD 是否在位——等级高不等于每一条链路都在。

FreeCAD 未安装时缺什么

生成主流程完全不受影响。 这条链路在 deps.py 里是 core: False,不参与八闸, spec_pipeline 一次都不 import 它。缺了就只有两样东西拿不到:

| 拿不到 | 会拿到什么替代 | |---|---| | .FCStd —— 能在 FreeCAD 里继续改特征、接着建模的活文档 | .STEP(B-rep 实体,可再导入任何 CAD,但没有特征历史) | | TechDraw 工程图(fmt=dxf/svg) | 自带 2D 出图链路(ezdxf 多视图 DXF + SVG 预览),零件图不缺 |

调用 export_freecad_step() 会返回 {"ok": false, "note": <安装指引>}, 不会静默产出一个空文件或假成功;check_freecad() 可提前问,避免生成完才发现。

3D 产出优先级

CATProduct(CATIA 装配,可编辑·最终目标)
   ↑ 自动由 CATPart 组装
CATPart(CATIA 零件,可编辑原生格式)
   ↑ 自动由 STEP 转换(CATIA COM)
STEP(B-rep 实体,CATIA/AutoCAD 通用首选交换格式)
   ↑ 由 cadquery 生成(pip install cadquery)
FCStd(FreeCAD 原生文档,**可继续编辑**,跨平台·免授权)
   ↑ 由 FreeCADCmd 无头子进程转换(装 FreeCAD 后可用)
GLB(glTF 2.0 二进制,**生态通用**:Blender/three.js/Windows 3D 查看器/Unity/网页)
   ↑ 零依赖自研导出器(默认可用)
STL(三角网格,网页预览 / MeshLab / 3D 打印)
   ↑ 由 manifold3d 生成(默认可用)

依赖可用就一路产出到 CATProduct;缺依赖则结果里一次性给出 pip 命令,不阻塞已有产出。

GLB:对外的主交付格式(P1)

build_from_spec 每次都会在 _3d.html 旁再写一份 .glb(result.glb_path):

  • 零依赖自研 glTF 2.0 写出器(geometry/glb_writer.py),不装任何东西就有;
  • 同一份装配:GLB 约 1.9 MB vs STL 约 3.9 MB(顶点焊接 + uint32 索引 + 单 BIN 块), 且保留零件分组、名称、颜色与透明度 —— STL 一个颜色都没有;
  • 根节点带 Z-up → Y-up 四元数,写进 Blender / three.js / Windows 3D 查看器不会躺倒;
  • 自研紧凑格式仍保留在 _3d.html 内部(那是给自家查看器用的,不是给外部生态的)。

三维页面:从"能转"到"能讲"(演示层 / 交互层)

_3d.html 不只是个能转的模型查看器。同样的外壳对任何产品都成立——它不认识"电梯", 只认识契约里的两段可选数据:

| 段 | 管什么 | 不写会怎样 | |---|---|---| | presentation | 标题/主题/机位/布景/引线标注/信息卡/读数/控件 | 按包络自动出四向机位、材料信息卡、通用开关——缺省即合格 | | interaction | 状态量、动作、时序、关节绑定、点击行为 | 退回一条正弦驱动(老契约行为不变) |

三件事值得单独说:

  1. 机位用 fit 而不是坐标。 {"fit":["motor","sheave"]} 表示"装下这几个零件", 距离由包围盒算,换产品不用改数字;pad 调松紧、fov 调透视强度。 要"站在轿厢里往外看"这种第一人称机位,用 anchor:{part, pos:[0..1]} 在零件盒里按比例取点。 手写 pos/target 只在 fit 确实表达不了时才用。
  2. 关节绑到状态量,不是绑到全局相位。 vars / actions / sequences / bindings 才表达得了"先关门 → 走车 → 到站开门",以及"只有当前层的厅门开"(when 条件)。 move 走梯形速度曲线,加减速与实时速度都能直接读数。
  3. 页面是单文件离线的。 three.js 随包内联成 data: 模块,_3d.html 双击即可打开, 不联网、不依赖同目录任何文件;断网环境下 Playwright 实测 ~2.6s 就绪。 开发时可设 TEXT2CAD_THREE=cdn 切回 CDN 省体积(此时不再离线)。

调页面不必反复重编译:页面上任何状态都能从控制台驱动—— __t2c.vars、__t2c.setView(id)、__t2c.fire(action)、__t2c.setToggle('ghost', true)。 URL 深链同样支持:?view=shaft&ghost=1&notes=1&go=to2。

观感:为什么棱是硬的(折边角劈分)

早期版本把"焊接顶点 + 邻接面法向求和"当成优化,结果机加工件的 90° 棱被抹成柔和渐变带, 零件看着像注塑外壳。V6 实测:气缸体 5265 条共享边里有 33.6% 是大于 30° 的真硬边, 61.7% 的顶点跨在 90° 棱上——全部被平均成了 45° 折中法向。

现在统一走 geometry/shading.py 的 split_by_crease():先焊接,再按折边角把顶点裂开。

| | 圆柱面内部(64 段,相邻 5.6°) | 90° 棱 | |---|---|---| | 处理 | 同一簇 → 保持光滑 | 裂成两个顶点,各带各的法向 | | 材质 | 法向连续过渡 | 高光断开,出现明确的明暗分界 |

代价:V6 全装配 202k 三角面,顶点数约 ×2.0,GLB 4.6 → 5.9 MB,HTML 2.3 → 2.8 MB。 crease_deg 在 15°~45° 区间结果几乎不变(需要劈的就是那批 90° 棱),不需要按模型调参。

同一套劈分同时用于 .glb 与 _3d.html:GLB 直接写劈分后的法向;HTML 把裂开的顶点 喂给 three.js,computeVertexNormals() 按拓扑平均后自然还原硬边——前端无需任何配合, 两处观感也不会不一致。

材质:按零件类型分档,而不是全局一套

全装配统一 metalness 0.55 / roughness 0.38 是"抛光件"的参数,套在铸件上就把砂型表面 的粗糙感抹平了。现在 assembly_viewer.KIND_MATERIAL 按 part.kind 给参数(单一真相在 Python 侧,算好后随 payload 传给前端):

| 分档 | 典型零件 | metalness / roughness | |---|---|---| | 铸件 | 缸体、缸盖、油底壳、进气/排气歧管 | 0.20–0.32 / 0.70–0.82(漫反射为主) | | 磨光钢 | 主轴颈、曲柄销、凸轮轴、气门 | 0.86–0.88 / 0.18–0.20(高光锐利) | | 铝加工件 | 活塞、带轮、法兰 | 0.66–0.76 / 0.32–0.44 | | 铜合金 | 轴瓦、十字滑环 | 0.70–0.80 / 0.36–0.44 | | 塑料罩盖 | 气门室罩、正时罩 | 0.06 / 0.82(几乎全漫反射) | | 未登记 | — | 0.40 / 0.50 |

kind 未标注时落 default,行为可预期。光照也相应调整:环境贴图强度压到 0.32(均匀 环境光会把每个面照得一样亮、显得"平"),主方向光提到 1.55 且刻意偏 +X 侧(跟相机同轴 打光会被模型自己挡住,阴影全看不见),启用 ACES 胶片曲线让高光滚降不过曝,并打开自阴影 ——零件之间与凹槽内部的暗部是"有体积感"的关键。

这张表放在 geometry/appearance.py,GLB 写出口用的是同一份(写进 pbrMetallicRoughness.metallicFactor / roughnessFactor)。否则同一台装配在 _3d.html 里是铸件质感、在 Blender 里是抛光塑料,等于两份产物。

FreeCAD 桥接(P4):把"终态文件"变成"可续的起点"

STEP 是交换格式,.FCStd 才是能被人接着改的活文档。装了 FreeCAD 后:

export_freecad_step(step_path="out/bench_vise.step", fmt="all")
# → out/bench_vise.FCStd                  用 FreeCAD 打开可继续加特征
# → out/bench_vise_techdraw.dxf           整页工程图(带图框 + 多视图排布)
# → out/bench_vise_techdraw_{front,top,right,iso}.svg   逐视图矢量图
check_freecad()      # 先看本机有没有 FreeCADCmd(含版本、路径与格式能力)

实现要点:走 FreeCADCmd 无头子进程(FreeCAD 自带内嵌 Python 与 Qt/OCCT, 在 MCP 进程里 import FreeCAD 几乎必炸);没装 FreeCAD 时只抛 FreeCADNotAvailable + 一次性安装说明,不影响 STEP/STL/GLB/DXF/HTML 任何链路。

headless 的真实能力边界(FreeCAD 1.1.4 实测,不是推测)

| 格式 | headless 可用 | 通道 | |---|---|---| | DXF | ✅ 最完整 | TechDraw.writeDXFPage(page, path) —— 整页、带图框、多视图排布 | | SVG | ✅ 逐视图 | TechDraw.viewPartAsSvg(view) —— 返回字符串(不写文件),按视图落盘 | | PDF | ❌ 不支持 | 只有 GUI 会话的 TechDrawGui.exportPageAsPdf;控制台会报 Cannot load Gui module |

要整页带图框的产出,用 DXF;PDF 需要桌面会话里跑 FreeCAD GUI。工具不会把不可用的格式伪装成成功, 每格式的失败明细都在返回值的 drawing_errors 里。

安装

Windows 下本模块会自动扫描 C:\Program Files\FreeCAD*\bin\FreeCADCmd.exe(按版本倒序),

# Windows:官方安装包落到 C:\Program Files\FreeCAD\
#   本模块会自动扫描 C:\Program Files\FreeCAD*\bin\FreeCADCmd.exe(版本倒序)
# 装在别处 / 便携版:设环境变量
set TEXT2CAD_FREECAD_CMD=C:\path\to\FreeCAD\bin\FreeCADCmd.exe

三个只有真机才暴露、桩测试永远发现不了的问题(已在代码里修掉并留了注释):

  1. 脚本路径含中文 → FreeCADCmd 直接崩。中文用户名的机器上 Temp 天然是 C:\Users\<中文>\AppData\Local\Temp,脚本落在那儿会以 Application unexpectedly terminated(exit=1、零输出)退出。 实测 cwd 含中文是安全的,只有 argv 里的脚本路径不行;本机 NTFS 也关了 8.3 短名(GetShortPathNameW 原样返回)绕不过去 —— 所以脚本改落到 C:\Users\Public\AppData\Local\Temp\text2cad。
  2. 子进程 stdout 是 GBK。FreeCAD 内嵌 Python 的输出跟着 Windows locale 走, 父进程按 UTF-8 解码会把返回的路径读成乱码,于是"图纸报错没生成"——可文件明明在磁盘上。 对策:json.dumps(ensure_ascii=True),让这条回传对编码彻底免疫。
  3. 模板文件名全猜错。TechDraw 页面不挂模板会报 Template not set for Page; 而 1.1.4 实际给的是 Default_Template_A4_Landscape.svg + ISO/、ASME/ 两个子目录共 126 个模板,写死候选名单一个都命中不了 —— 改成递归扫描 + 偏好打分(A4 > A3 > A2、横向优先)。

耗时预期:小件约 4 秒;真实台钳(8 MB STEP、十余个零件)走完全套实测 264 秒, 所以默认超时设的是 300 秒,复杂装配请用 timeout= 继续放大。

⚠️ CATIA 打开 3D 的正确方式(重要)

STL 是网格(mesh),CATIA 不能用"文件 → 打开"直接吃它 —— 直接双击/打开会"毫无反应"或只进树不显示。

  1. 首选:用 STEP。pip install cadquery 后重跑,得到 .step,在 CATIA 里 文件 → 打开, 会作为实体零件直接进入"零件设计",可编辑、可出工程图。之后可用 export_catpart(step_path) (需 pywin32 + 已装 CATIA,pycatia 可选)另存为 .CATPart,或用 build_catproduct(...) 组装成 .CATProduct。
  2. 用 STL(网格):在 CATIA 中 文件 → 新建 → 零件,进入 数字化外形编辑器(DSE) 或 快速曲面重建 工作台,再 插入 → 导入 → STL,把网格导入为 Mesh 特征。

一句话:给 CATIA 用 STEP,不要直接"打开"STL。STL 更适合 FreeCAD / MeshLab / 3D 打印 / 网页预览。

DXF 版本(AutoCAD 兼容性)

默认导出 R2007(AC1021),可被 AutoCAD 2007 及以后打开(避免旧版 CAD 报 "图形版本中遇到未知值 AC1024")。

  • 更老的 CAD / 通用查看器:R2000(AC1015)或 R12(AC1009,兼容性最强)。
  • 更新特性:R2010(AC1024,需 AutoCAD 2010+)。

设置方式:环境变量 TEXT2CAD_DXF_VERSION=R2007,或工具参数 dxf_version="R2007"。

零依赖降级后端(工程图层面)

若未安装 ezdxf,服务会自动使用零依赖后端产出可打开的简化 DXF(圆/矩形/线/文字 + 标题栏 + BOM); 装上 ezdxf 后同一链路自动切换为完整工程图(多视图 + 尺寸标注 + 中心线)。

构件级通用层用法

任意产品(多零件 + 装配 + 运动)请走契约通道;这层适合只需输出单个零件轮廓/实体的场合。

1) 2D:generate_part(sketch / plate / tube)

from text2cad.server import generate_part
from text2cad.models import PartSpec
import json

plate = PartSpec(name="带孔板", part_type="sketch", params={
    "profile": {
        "outer": {"shape": "rect", "w": 120, "h": 80},
        "holes": [{"cx": 30, "cy": 40, "r": 10}, {"cx": 90, "cy": 40, "r": 10}],
    }})
print(generate_part(json.dumps(plate.to_dict())))   # → {"ok": True, "dxf_path": "…", …}

part_type="sketch" 的 profile 由以下基元任意拼成:

| 基元 | 字段 | 说明 | |---|---|---| | rect | w,h,x,y | 矩形(x,y 为左下角,默认 0,0) | | circle | cx,cy,r | 圆 | | line | points:[[x,y],[x,y]…] | 直线段(侧视图骨架、车架管等) | | polyline | points:[[x,y]…] | 闭合/开放折线 | | arc | cx,cy,r,a0,a1 | 圆弧 |

plate(带孔板)与 tube(圆环)是 sketch 之上的便捷封装,内部同样走 sketch 基元。

2) 3D:generate_3d_part(extrude / revolve / tube → STEP/STL)

未装 cadquery 时走网格后端出 STL;装了则同时给出 STEP。

from text2cad.server import generate_3d_part

# 带中心孔的法兰
print(generate_3d_part(json.dumps({
    "name": "法兰", "part_type": "sketch",
    "params": {"operation": "extrude",
               "profile": {"outer": {"shape": "circle", "r": 80},
                           "holes": [{"cx": 0, "cy": 0, "r": 20}]},
               "depth": 15}})))            # → {"ok": True, "path": "法兰.step", …}

# 旋转成体(外径 40、内径 20、厚 10 的环)
print(generate_3d_part(json.dumps({
    "name": "环", "part_type": "sketch",
    "params": {"operation": "revolve",
               "section": {"shape": "rect", "w": 10, "h": 10, "x": 25, "y": 0},
               "angle": 360}})))

车架/管线用 operation:"tube":{"a":[x,y,z],"b":[x,y,z],"r":半径}。

测试

tests/
├─ fixtures/                # 4 份 DesignSpec 契约(回归基线:台钳 / 自行车 / AK-47 / 机械臂)
├─ test_backend_equiv.py    # 15 个几何不变量 × 每个后端 + 双后端逐点等价
├─ test_gates.py            # 八闸反向用例:坏契约必须被拦下且诊断带 hint
├─ test_e2e_specs.py        # 4 份契约跑 build_from_spec(断言 ok + 八闸 + 产物落盘)
├─ test_mcp_smoke.py        # 真实 stdio 握手 + tools/list + 断言品类工具不在注册表
├─ test_glb_writer.py       # P1:GLB 容器自洽 / Z-up 不被偷转 / 空件处理
├─ test_pick_feature.py     # P3:特征拾取精度 + 诚实性,谓词按半径区分孔
├─ test_freecad_bridge.py   # P4:桩跑通协议 + **装了 FreeCAD 就自动跑真机**(FCStd/DXF/SVG 校验)
├─ test_catia_bridge.py     # P5:CATIA 桥双轨——桩守协议 + 装了 CATIA 就跑真机(CATPart/CATProduct)
├─ test_autocad_bridge.py   # P5:AutoCAD 桥双轨——桩守协议 + 装了 AutoCAD 就跑真机(DXF→DWG)
├─ test_cache.py            # 增量生成:三级指纹 / 磁盘缓存跨进程 / "契约没变"秒回
├─ test_deps_env.py         # 环境自描述与降级记账
├─ test_design_calc.py      # L1 选型计算:KB 公式对照 + 宁缺勿编(缺需求即跳过)
├─ test_part_review.py      # L1b 零件标准检视:kind 分派 + KB-only 记 skipped
├─ test_standard_part_op.py # L2 标准件算子(轴承/螺栓/卡簧…)
├─ test_shading.py          # 材质分档与棱边观感
├─ test_sweep.py            # L3 扫掠:缓存失效 / 相位语义 / 让位集合 / 进程相关几何
├─ test_tolerance.py        # 闸7 公差:数值表自洽(带宽=IT)/ ISO 286 手册对照 / 未收录记 skipped
├─ test_sourcing.py         # 可采购 BOM:编码规则 / 自制外购判定 / 成本汇总 / 装配树
├─ test_dfm.py              # 闸8 可制造性:工艺族门槛 / 射线量测 / 可达性(BLDC 三条真实问题)
├─ test_cae.py              # 质量特性解析解 + Abaqus/Nastran 卡结构
├─ test_calc.py             # KB 计算引擎:整库自洽扫描 / 求值沙箱 / 8 族数值 / 诚实边界
├─ test_family.py           # 参数化族:占位替换 / 变型隔离 / 对比表不美化
├─ test_plm.py              # PLM 导出:一物一码 / 多级 BOM 项号 / ECN 差异
├─ test_constraint.py       # 约束图:过约束矛盾(回代残差)/ 环秩 / 无基准子装配 / 自由度
└─ run_all.py               # 零依赖总入口(20 套件)
python tests/run_all.py          # 全部
python tests/run_all.py gates    # 只跑八闸反向用例
python tests/run_all.py pick     # 只跑特征拾取(含"宁缺勿编"反向用例)
python tests/run_all.py freecad  # P4 桥接(没装 FreeCAD 时真机那条自动跳过)
python tests/run_all.py catia    # P5 CATIA 桥(没装 CATIA 时真机那条自动跳过)
python tests/run_all.py autocad  # P5 AutoCAD 桥(没装 AutoCAD 时真机那条自动跳过)
python tests/run_all.py tolerance  # 只跑闸7 公差配合
python tests/run_all.py dfm        # 只跑闸8 可制造性
python tests/run_all.py sourcing   # 只跑可采购 BOM
python tests/run_all.py constraint # 只跑约束图分析

freecad / catia / autocad 三个桥接套件的策略都是双轨:桩用例守协议、 本机装了对应 CAD 就自动追加真机用例(真实 STEP → FCStd + DXF/SVG 校验、 STEP → CATPart/CATProduct、DXF → DWG 重开验证)。 这样既不会因为"环境没装"而漏掉回归,也不会因为"只有桩"而对真机行为产生虚假信心。

测试可零依赖运行(不要求 pytest);装了 pytest 也可 python -m pytest tests -q。

双后端逐点等价只在同一解释器同时装了 manifold3d 与 cadquery 时才会执行, 否则该节自动跳过(套件会打印"只装了一个后端")。要复现完整对照: pip install manifold3d cadquery 后重跑 python tests/run_all.py。

状态

  • 契约通道 ✅ 契约词表 + 环境自描述 → build_from_spec 产出 2D/3D/Web + BOM/质量/干涉报告,内核零 per-product 分支。
  • 可插拔几何后端 ✅ manifold(默认)/ cadquery,双后端逐点等价有测试守住。
  • 能力协商 ✅ 软算子降级并记账、硬算子明确报错,capability_gaps 随环境自描述。
  • 确定性八闸 + 诊断回灌 ✅ gates 逐条结论 + diagnostics[{gate,level,where,what,hint}]。
  • 闸 7 公差与配合 ✅ 契约新增 tolerances 段(fits 孔轴配合 / chains 尺寸链 / gdnt 形位公差),把原本只能写进 note 文本的 H7/m6、气隙 ±0.05、同轴度 Ø0.03 变成可计算字段:极限间隙过盈、过盈接触压力(厚壁筒解)、尺寸链极值法/RSS 闭环、 基准完整性、工艺经济精度可达性。数值取自 kb/tolerance/limits.yaml (GB/T 1800.1 节选),未收录的公差带/尺寸段记 skipped,不插值不外推; 全表有"带宽 == IT 等级标准公差"的自洽性测试(164 个带)防抄错。
  • L0 → L3 工程深度 ✅ L1 选型计算(KB 经验公式对照,缺需求静默跳过)、 L1b 零件级设计标准检视(5 类 16 条,KB-only 标准记 skipped 并引导 kb_search)、 L1c 公差配合(见上)、L1d 可制造性(闸 8)、L2 标准件算子、 L3 全行程扫掠干涉(与查看器同源的闭式解,超预算如实 complete=false)。 V6 发动机(31 件 / 17 运动件,scripts/gen_v6_engine.py 参数化生成)八闸全绿: 扫掠 24 相位 × 11832 次精确求交 0 命中、完整查全。
  • 闸 8 可制造性(DFM) ✅ 规则是数据(kb/dfm/rules.yaml,加规则/改门槛只改 YAML): 最小孔径、深径比、壁厚、孔壁、圆角、薄壁长厚比,门槛按工艺族(machining / casting / injection / sheet / spring)取,未登记族记 skipped 不套别的族;射线采样从真实几何 量最小壁厚与孔壁(不是读契约声明),超三角面/射线预算如实跳过并写明哪些件没量; 装配可达性沿装入方向扫掠(默认方向仅供参考,声明 dfm.assembly_direction 才判错), 并单列名义几何干涉(过盈配合请走 tolerances.fits)。外购件不套自制工艺门槛。
  • 约束图分析(闸 2 补充) ✅ mate 图连通分量 / 环秩 / 自由度掩码并集 / 逐条约束回代残差 —— 抓"位姿解得出来但图纸自相矛盾"(后写的约束把先写的顶掉);angle 类需初始姿态, 如实记 skipped;无基准子装配单独报出(否则整个子装配可整体漂移)。
  • 计算深度扩展(闸 4) ✅ 工程公式是数据不是代码(kb/calc/*.yaml):轴承寿命 L10/L10h、 螺栓预紧与拧紧等效应力、弹簧刚度与 Wahl 修正切应力、带传动几何、角焊缝、动平衡许用 不平衡量、电机电流/损耗自洽/温升、过盈接触压力与可传扭矩。求值器不用 eval(AST 白名单: 属性/下标/lambda/导入/超长全拒),单位按 KB 别名换算(kN/GPa/r·min⁻¹/g·mm…,未登记不猜); 缺必需输入 → missing_input 点名符号,取 KB 默认值 → skipped 留痕,依赖可选参数的判据 → 单独记 skipped 不装作查过。整库有自洽性扫描(符号齐备 / 表达式可解析 / 消息占位可 格式化 / id 唯一)与跨模块一致性断言(过盈压力必须等于闸 7 的 Lamé 解)。
  • PLM / ERP 导出 ✅ plm_export 出导入格式而不是报告:物料主数据(一物一码, 装配体虚拟件也有码,MAKE/BUY/ASSEMBLY + 类别码 + 单位 + 图号 + 工艺路线 + 单重 + 版本)、多级 BOM(父项/子项/数量/层级/项号 1·2.1/生效版本,根节点是产品)、 CSV(UTF-8-BOM,Excel/ERP 直接吃)+ JSON 双份。plm_ecn 对两版契约出工程变更单 (新增/删除/改材料/改图号/改数量/换父项 MOVE,比对键是物料号不是零件 id)。 诚实边界:自制件缺图号留空并告警(图号是图档系统的键,编了会打架;外购标准件靠 型号识别,没有图号是常态、不告警)、缺料缺价留空不编、 物料号撞车直接 ok=false(同码不同物进系统会把两件合成一条库存);不做 XLSX (不为导出引入 openpyxl,CSV 另存即可)。契约 bom 段声明的数量优先于"一零件一实例" 默认值(14 片磁钢不会在 ERP 里变成 1 片)。
  • 零件类别(kind)语义与编码归一 ✅ kind 是零件身份而不是材质标签:一句话决定 物料号类别码、序号分组、第 6 闸标准检索、配色与 PBR。kb/plm/coding.yaml 的 kind_aliases 归一复数 / 连字符 / 大小写 / 行业同义(magnets→magnet、 stator-core→stator、endcap→cover、hex_bolt→bolt),不做语义外推(metal、 part 这类含糊写法照样落 99,逼着人补契约);序号按归一后的 kind 分组,否则 endcap 与 cover 各排各的序号会撞出同一个物料号。未登记 kind 在契约校验(warning, 点名后果)与第 6 闸(skipped 说明"该类别无标准可拉")两处都喊出来 —— 静默跳过会 让整台机器"看起来全过闸",实际一条标准都没检视(BLDC 真实踩过:14 件 kind 全写 metal,物料号全落 99 且第 6 闸零覆盖)。
  • 参数化族 / 系列化变型 ✅ 契约里写 "{{表达式}}" 占位(与计算引擎同一安全求值器, 可写 {{bore/2}} / {{max(bore,40)}}),family_build 按参数矩阵展开成一组变型, 逐变型真的跑一遍流水线(闸门 / 质量 / 成本)并给出对比表与参数变化区间; 变型产物落在 产品名-<变型id> 目录互不覆盖。占位没解析出来 → 该变型失败并写明缺哪个 符号(绝不把 "{{bore}}" 当字符串交给几何层);跑不通的变型照样占一行,表不美化。
  • 可采购 BOM ✅ 物料编码规则引擎(kb/plm/coding.yaml)、自制/外购判定(显式 > 标准号 > kind)、 工艺路线与费率(kb/manufacturing/prices.yaml)、装配树展开、成本汇总; 未收录材料/工艺记 warning 而非编价。
  • 运动系统 ✅ revolute(含 oscillate)/ prismatic / orbit / slider_crank / screw; source 传动比链、parent 结构串链。
  • 构件级通用层 ✅ generate_part / generate_3d_part / validate / solve_assembly_layout。
  • CATIA 原生桥接 ✅ 真机验证(CATIA V5R21):单实体 STEP → .CATPart; 装配 STEP 直达 .CATProduct(各组件位姿随 STEP 保留,positioning="step"); 多零件路径逐件 CATPart + 恒等位姿(如实标 positioning="identity")。 关键实证已固化进代码注释:必须纯动态 Dispatch、DisplayFileAlerts=False、 先关同名文档、V5R21 无 AddExistingComponent、冷启动 30 s 不能 gate 探测。
  • AutoCAD 原生桥接 ✅ 真机验证(AutoCAD 2007):export_dwg 把 DXF 另存为 原生 DWG(R2007,magic AC1021,落盘后校验,中文路径正常)。关键实证: 必须用带版本号 ProgID(AutoCAD.Application.17,无后缀的报"无效的类字符串"); SaveAs(路径) 不能传格式参数——旧桩的 SaveAs(path, 1) 会把 DXF 文本 写进 .dwg(1 = acR13_dxf);目标 DWG 被已打开文档占用时 SaveAs 会被拒(点名占用者)。 能力边界诚实说明:2007 的 IMPORT 不支持 STEP(2013+ 才有), 3D 实体请走 CATIA / FreeCAD 桥,本桥只做 2D DXF→DWG。
  • FreeCAD 桥接 ✅ 真机验证(FreeCAD 1.1.4):STEP → .FCStd(可继续编辑)+ TechDraw 工程图; headless 下 DXF 是最完整的整页通道、SVG 按视图导出、PDF 无通道(如实记账,不伪装成功)。
  • 增量生成 ✅ 三道指纹闸门(整单 / 产物 / 零件)+ 内容寻址几何缓存 + stable 固定目录; V6 实测"契约没变再跑一次"从 66 s 降到 0.02 s,"只改一件"降到 35 s,桌面文件夹从 7 个收敛到 1 个。
  • 干涉检查完整性 ✅ 修复早停 break 只跳内层的缺陷;现在会报告 candidate_pairs / checked_pairs / complete,早停时明确写出还剩多少对没判定。

许可

专有软件,保留所有权利(Proprietary — All Rights Reserved)。

全文见 LICENSE。要点:

| 允许 | 禁止 | |---|---| | 在你自己的主体内部安装并运行(含商业/生产用途) | 复制、再分发、再许可、出售、出租 | | 用它的输出(图纸/模型/BOM/报告)做任何合法用途,输出归你 | 修改、改编、创作衍生作品 | | 为备份而保留副本 | 逆向工程、反编译、反汇编 | | — | 以 SaaS / API / 内嵌组件形式提供给第三方 | | — | 删除或篡改版权声明与水印 | | — | 用它开发、训练或改进竞品 |

需要再分发、OEM、SaaS 或源码访问,请单独洽谈商业授权(eiry16 @ npm / GitHub)。

关于历史版本:0.15.0 及更早版本曾以 MIT 发布。MIT 的授权一旦给出, 对已经拿到副本的人不可撤销——撤回 registry 上的版本只能阻止新的获取, 不能追回已经下载的副本。这是许可证本身的语义,不是执行疏漏。

工程输出在投入制造前必须由具备资质的工程师复核;本软件不附带任何担保, 详见 LICENSE 第 6、7 条。