SolidCraft 参数化 CAD 内幕:改 24mm 到 32mm 背后的三层链路
【免费下载链接】solidcraftSolidCraft: an open-source parametric 3D CAD application built in pure Rust. Desktop, web, and automation through MCP.项目地址: https://gitcode.com/gh_mirrors/so/solidcraft
把参数面板里一个尺寸从 24 改成 32,整机箱模型随即整体重建——这是 SolidCraft,一个用纯 Rust 写成的参数化 CAD,桌面端、浏览器与 MCP 自动化共用同一套代码。下文顺着源码拆解这次改动背后的三层链路:约束求解、参数驱动、增量重建。
全局图景:一次改动经过的三层链路
官方演示的收尾动作很固定:在 Parameters 面板把机箱高度从 24 mm 改成 32 mm,拉伸、立柱、通风槽全部跟着更新。这个动作不是"重画一遍模型",它按固定顺序穿过三层:
- 参数层:被改的数字先经过表达式求值(单位感知、可引用其他参数),得到新的参数值;
- 几何层:草图约束求解器以新的尺寸值为目标,把整套约束解算一遍,重新算出每个点的坐标;
- 重建层:特征时间线做增量评估,从第一个受影响的特征起,逐一走过几何内核(负责实体运算的底层引擎)生成新几何。
代码上每一层都有明确归属:crates/doc/src/params.rs与expr.rs管参数和表达式,crates/sketch/src/solver.rs管约束求解,crates/doc/src/eval.rs管时间线评估。下面逐层看。
参数层:从"数字"到模型参数 dN
在 SolidCraft 里,一个尺寸值或特征输入(拉伸距离、圆角半径)并不是以纯数字形式存进文件的。crates/doc/src/params.rs 的模块注释说清了这一点:
Parameters: user parameters, model parameters of sketch dimensions, and feature inputs (extrude distance, fillet radius…) which also appear as model parameters
dNwithout being stored twice: the feature keeps its expression and the parameter name refers to it.
解读:特征保留的是表达式,参数值只存一份,特征按名字引用它。特征不存数字,存表达式;参数才是数值的唯一真源。所有草图尺寸和特征输入都会按遍历顺序拿到d1、d2这样的模型参数名,任意表达式都能按名字引用任意参数。
用户参数与模型参数的差别只在"命名"这一步:模型参数自动分配,用户参数是你自己起的名字。在输入框里敲height = 12就会创建(或重命名)一个参数,此后的表达式都引用它(docs/book/src/guide/parameters.md)。
表达式系统有三条纪律,都写进了文档与注释:
- 单位感知。crates/doc/src/expr.rs 给出的例子是
2 * width + 5 mm、angle / 2;指南明确:长度填到角度位置(或反过来)"is an error, not a silent conversion"——直接报错,不悄悄换算。 - 循环依赖报路径。"Cycles are reported with their path (
a → b → a)"——报圈不只是一个"存在循环",而是把路径列出来。 - 重命名是全局操作。"renaming a parameter updates every expression, dimension and feature input that uses it, and a parameter in use can't be deleted"——重命名自动更新所有引用,使用中的参数禁止删除。
依赖图本身不用维护,它由表达式引用自动推出(params.rs 注释:the dependency graph and cycles come from the expression references)。给拉伸距离写height / 2时,联动关系就已经落在文件里了。
几何层:约束求解器如何把关系算成坐标
参数层只产出数字;数字要变成坐标,靠草图约束求解器。
约束全集在 crates/sketch/src/model.rs 的ConstraintKind枚举里,分两类:几何关系(Coincident、PointOnCurve、Horizontal/Vertical、Parallel、Perpendicular、Tangent、Equal、Concentric、Midpoint、Symmetric、Fix)和驱动尺寸(Distance、Length、Radius、Angle等)。值得注意:尺寸本身就是一种约束——它比几何关系多带一个value字段。无值的是关系,有值的才是驱动。
求解策略在 crates/sketch/src/solver.rs 的模块注释里:
Unknowns are the coordinates of every non-fixed point and the radius of every circle. Each constraint contributes residuals (zero when satisfied, in millimetres so that all rows have a comparable scale). The system is split into connected components and each is solved with a damped Gauss–Newton (Levenberg–Marquardt) iteration on a dense Jacobian built from local central differences.
拆成三步读:未知量是每个未固定点的 x/y 坐标、每个圆的半径;每条约束贡献一个以毫米计的残差,即"当前还不满足多少";系统按连通分量拆分(互不触及的约束互不相干,分开解),每个分量内做阻尼 Gauss–Newton 迭代——Gauss–Newton 是逐步线性化解非线性方程组的经典方法,"阻尼"指控制步长防止发散(Levenberg–Marquardt 就是它的标准实现),Jacobian(残差对每个未知量的偏导矩阵)用中心差分数值构造。
自由度分析是同一个 Jacobian 的直接副产品:
Afterwards the rank of the Jacobian gives the remaining degrees of freedom and which points are fully determined.
即Jacobian 的秩直接给出剩余自由度——几何还能自由移动的方向数。SolveReport里对应dof(0 即完全约束)、未满足约束failing、以及逐点/逐曲线的point_determined/curve_determined。UI 表现见 docs/book/src/guide/sketching.md:
Under-constrained geometry is blue and fully constrained geometry turns dark… A constraint or dimension that would over-constrain the sketch is refused rather than applied.
欠约束的几何显示为蓝色、仍可拖动;dof 归零后变深色;会造成过约束的新约束会被拒绝,而不是强行套用。
还有一处工程取舍:并非所有未知量都进迭代器。求解器注释写道,那些能从常数一步步"逐个钉出来"的未知量——比如两条已知轨迹(loci,已确定的几何元素集合)的交点——"are eliminated exactly, without the dense matrices":crates/sketch/src/decompose.rs的注释引用 Owen 1991 与 Fudos–Hoffmann 1997 的构造分解,把这类点先精确消去,剩下的才交给 LM 迭代。能算出精确解的就不走稠密矩阵,这是求解器既快又稳的来源。
重建层:特征时间线的增量评估
草图只是第一步。拉伸、旋转、扫掠、圆角、抽壳、阵列、镜像等全部特征挂在一条时间线上。crates/doc/src/lib.rs 顶部一句话定了评估规则:
Evaluation is incremental: a feature is recomputed only when it, a parameter it uses, or an earlier feature changed.
eval.rs 的主评估函数注释更直白:Re-evaluate the document, reusing results up to the first changed feature——找到第一个变化的特征,它之前的结果原样复用,从它开始往后重走。50 步的历史里改第 3 步的尺寸,前 2 步不参与重算。
"重走"要可靠,靠一套持久化命名兜底。docs/book/src/dev/naming.md 先讲原因:如果圆角存的是边的下标,上游一改、面多了一个,圆角就会挪到别的边上。"SolidCraft stores names, and finds them again every time the timeline evaluates."——名字不是几何猜测:内核对每次运算报告每个面/边的来历,名字沿历史传递(crates/doc/src/lineage.rs合成,crates/doc/src/name_lookup.rs查找);引用确实找不到了,特征会明说,而不是默默选错对象。
时间线本身也完全可交互:每个特征可编辑、抑制、重命名、重排(docs/book/src/guide/features.md:Each one can be edited, suppressed, renamed, reordered),底部还有逐步回滚的历史回放条。下面示例设计的浏览器面板列出了 Sketches 与 Features 层级,底栏即时间线:
源码速查:哪个文件对应哪一节
| 文件 | 职责 | 对应章节 |
|---|---|---|
| crates/doc/src/params.rs | dN 模型参数、用户参数、依赖与循环检测 | 参数层 |
| crates/doc/src/expr.rs | 单位感知表达式求值 | 参数层 |
| crates/sketch/src/model.rs | ConstraintKind:几何关系与驱动尺寸 | 几何层 |
| crates/sketch/src/solver.rs | LM 迭代求解器、Jacobian 秩与 dof 报告 | 几何层 |
| crates/sketch/src/decompose.rs | Owen/Fudos–Hoffmann 解析分解与精确消元 | 几何层 |
| crates/doc/src/lib.rs | 文档模型与增量评估规则 | 重建层 |
| crates/doc/src/eval.rs | 时间线评估、结果复用 | 重建层 |
| crates/doc/src/name_lookup.rs | 持久命名查找与降级 | 重建层 |
动手验证:两条 JSON 命令检查并驱动一个特征
时间线还有一整套命令接口,最小复现路径是timeline.inspect与timeline.edit(见 docs/book/src/guide/features.md)。前者查一个特征的定义与输入参数,后者写入新值:
{"command": "timeline.inspect", "params": {"feature": "Slab"}} → {"type": "extrude", "definition": {"extent": {"distance": "10", "direction": "positive"}, …}, "inputs": [{"label": "Distance", "param": "d1", "expression": "10", "unit": "mm"}]} {"command": "timeline.edit", "params": {"feature": "Slab", "set": {"extent": {"distance": "12"}}}}inputs字段是两层机制的交汇点:它把特征输入映射到参数名和当前表达式(d1→10)。timeline.edit合并新字段后触发重建,响应里报告哪些特征被recomputed、哪些被restored(复用)。演示 GIF 结尾的"高度 24 → 32 mm"走的正是同一条通路。
收尾:参数化模型为什么天然可脚本化
回头看这套机制,"改 24 为 32、全模型重建"之所以能被脚本驱动,是因为模型不是一份几何结果,而是一段可重放的描述——参数表、约束系统、特征序列,全部可序列化。同一套命令既可供 UI 使用,也可以经 JSON 控制通道(docs/book/src/dev/control-protocol.md)和 MCP 服务器(crates/mcp/)暴露给 AI 代理。当模型天生就是"描述 + 重放",自动化就不是附加功能,而是表示方式的直接推论。
【免费下载链接】solidcraftSolidCraft: an open-source parametric 3D CAD application built in pure Rust. Desktop, web, and automation through MCP.项目地址: https://gitcode.com/gh_mirrors/so/solidcraft
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考