feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
+15 -8
View File
@@ -12,10 +12,16 @@ CadSet 是一个面向机械零件的 STEP-first 参数化 CAD 系统。它覆
2. 根据文字和参考图片生成或修改可编辑 CAD。
3. 将上传的 STEP 重建为 DesignIR,独立生成新的 STEP,并继续参数化修改。
CAD Router 会在两个执行后端之间选择:
CAD Router 不再设置永久默认后端,而是根据已有源、标准件覆盖、语义
重放需求、几何词汇、装配关系和验收要求在两个平权建模后端之间选择:
- **text-to-cad / Build123d**通用机械零件、特征建模、几何验证和 STEP 输出。
- **SimpleCADAPI**可重放操作图、语义化机械结构和专用机械零件能力。
- **Build123d**定制机械零件、复杂轮廓与曲面、源级装配和 STEP 输出。
- **SimpleCADAPI 2.0.2**:标准机械工厂、ModelResult 可重放图、语义拓扑、
单位/公差、场景包、CAD 翻译和严格 B-Rep 对比。
`text-to-cad` 是 DesignIR、路由、修复、验收和交付编排层,不再因为它
承载 Build123d Skill 而给 Build123d 额外默认分。上传 STEP 由 SurfaceIR
独立重建,不伪装成任一文字生成后端。
## 系统架构
@@ -25,12 +31,12 @@ CAD Router 会在两个执行后端之间选择:
└─────────┬──────────┘
┌─────────▼──────────┐
CAD Router
│ CAD Router/DesignIR
└──────┬───────┬─────┘
│ │
┌───────────▼─┐ ┌─▼──────────────┐
│ Build123d │ │ SimpleCADAPI
└───────────┬─┘ └─┬──────────────┘
┌───────────▼─┐ ┌─▼────────────────
│ Build123d │ │ SimpleCADAPI 2.0.2
└───────────┬─┘ └─┬────────────────
└────┬────┘
DesignIR 3.0 + STEP
@@ -50,6 +56,7 @@ Teacher STEP
-> isolated compiler without Teacher STEP access
-> rebuilt STEP
-> independent Acceptance Agent
-> SimpleCADAPI strict B-Rep comparison
-> deterministic experience promotion
```
@@ -60,7 +67,7 @@ Teacher STEP
| [`cad-agent-studio/`](cad-agent-studio/README.md) | 独立 Next.js 前端,负责对话、图片与 STEP 上传、任务管理和模型预览。 |
| [`designir-pipeline/`](designir-pipeline/README.md) | DesignIR Schema、双 Agent、隔离编译、验收和经验晋升。 |
| [`text-to-cad/`](text-to-cad/README.md) | CAD Router、Build123d CAD Skill、CAD Viewer 和共享 CAD 运行时。 |
| [`SimpleCADAPI/`](SimpleCADAPI/README.md) | 第二建模后端,提供可重放操作图和机械建模 API。 |
| [`SimpleCADAPI/`](SimpleCADAPI/README.md) | `2.0.2` 平权建模与验收后端,提供标准件、可重放图、语义拓扑、单位/公差、场景和 B-Rep 工具。 |
| [`llm.config.yaml`](llm.config.yaml) | 前端共享的模型供应商、模型和密钥配置。 |
依赖目录、虚拟环境、构建缓存和批量运行数据不会进入 Git。锁文件、源码、Schema、Agent 定义和确定性测试会进入版本控制。
+186
View File
@@ -0,0 +1,186 @@
# SimpleCADAPI 2.0.2 融合说明
## 融合结果
CadSet 已将内置 SimpleCADAPI 从 `2.0.1b1` 对齐到 `2.0.2`,并完成运行
环境、DesignIR 编译器、CAD Router、上传 STEP 验收、Skill、测试和文档
的同步接入。
这次不是简单复制依赖目录。新增能力已经进入实际执行链:
| 新能力 | CadSet 使用位置 | 当前状态 |
|---|---|---|
| `@model` / `ModelResult` | DesignIR SimpleCAD 编译器 | 已使用 |
| 语义标签与拓扑来源 | 每个 DesignIR 特征和布尔工具 | 已使用 |
| STEP B-Rep 严格比较 | 上传 STEP 独立验收区 | 已使用 |
| 螺栓、螺母、链轮、锥齿轮工厂 | cad-router 标准件识别 | 已纳入路由 |
| 单位与公差链 | cad-router 需求识别 | 已纳入路由,待扩展 DesignIR 映射 |
| Scene Schema / `.scene.zip` | SimpleCADAPI 产物能力 | 源码已具备,尚未替换现有 Viewer |
| Fusion 360 / SolidWorks 翻译 | SimpleCADAPI 派生导出能力 | 源码已具备,尚未开放前端按钮 |
| B-Rep 多视图与截面 XOR | 诊断工具 | 源码已具备,严格 compare 已接入 |
CAD Agent Studio 已同步读取新的路由结果,不再在路由字段缺失时静默
假定 Build123d。上传 STEP 的任务清单会保存 `routing_policy`
`strict_brep_acceptance` 报告,并在聊天结果中区分 `brep_exact` 与仅几何
通过的情况。
## 新的角色边界
### text-to-cad
继续是系统核心,但核心含义变为“编排层”,不是“默认建模后端”:
- CAD Router
- DesignIR 3.0 所有权
- SurfaceIR 上传 STEP 重建
- 需求细化和视觉修复
- 验收、产物和 Viewer 交付
### Build123d
作为平权建模后端,适合:
- 定制支架、壳体、轴、法兰和夹具
- 复杂草图、自由曲面、放样和扫掠
- 已有 build123d 源码的连续修改
- 源级装配与基准关系
### SimpleCADAPI 2.0.2
提升为平权建模与验收后端,适合:
- 齿轮、齿条、内齿圈、锥齿轮和摆线件
- 轴承、螺栓、螺母和滚子链轮
- 减速器、执行器和带运动约束的机构
- 需要 Model JSON 重放、语义标签和拓扑来源的模型
- 需要单位、公差链或 FreeCAD/Fusion 360/SolidWorks 翻译的模型
- 上传 STEP 的严格 B-Rep 几何与拓扑验收
## CAD Router 2.0
路由取消永久默认后端。两个后端从相同基础分开始,按以下证据评分:
1. 已有可编辑源属于哪个后端。
2. 是否存在直接覆盖的标准机械工厂。
3. 是否要求模型图重放、语义拓扑、单位、公差或 CAD 翻译。
4. 是否需要复杂自定义轮廓或自由曲面。
5. 是否需要装配和运动约束。
6. 是否需要特定验收能力。
主要决策示例:
| 请求 | 结果 | 原因 |
|---|---|---|
| 模数 2 直齿轮 | SimpleCADAPI | 有确定性标准工厂 |
| M8 螺栓 | SimpleCADAPI | 有紧固件工厂 |
| 滚子链轮 | SimpleCADAPI | 有链轮工厂 |
| 带语义标签和公差链的法兰 | SimpleCADAPI | 语义、单位和公差是主要要求 |
| 普通定制法兰 | Build123d,低置信度 | 两者都能表达,Build123d 的通用特征略优 |
| NURBS 自由曲面壳体 | Build123d | 定制曲面词汇更适合通用后端 |
| 修改已有 SimpleCAD Model JSON | SimpleCADAPI | 保持原生编辑源 |
| 上传 STEP 重建 | SurfaceIR | 重建引擎固定;后端评分只用于后续语义修改 |
路由输出新增:
- `routing_policy.version`
- `routing_policy.requirements`
- `routing_policy.native_source_backend`
- `routing_policy.score_margin`
- `routing_policy.effective_execution_engine`
- `routing_policy.validation_engines`
上传 STEP 时,`selected_backend` 表示后续语义修改的优选后端,
`effective_execution_engine` 明确为 `surfaceir`,避免把 SurfaceIR 重建
错误描述为 Build123d 或 SimpleCADAPI 重建。
## DesignIR → SimpleCADAPI
SimpleCAD 编译器现在使用:
```text
DesignIR feature
-> SimpleCAD primitive/feature
-> feature.<id> topology tag
-> boolean lineage
-> feature.<id>.result semantic tag
-> capture_result
-> ModelResult.model_json
-> STEP
```
每个基础体、孔工具和最终特征结果都会进入模型图。这样 Model JSON 不再
只是低层操作列表,还可以根据特征 ID 定位语义对象,为后续参数修改和
非目标区域保持检查提供锚点。
## 上传 STEP 验收
上传 STEP 仍严格执行教师隔离:
```text
Teacher STEP
-> 私有证据提取
-> DesignIR 3.0 / SurfaceIR
-> 无教师文件的独立重建
-> SurfaceIR 几何验收
-> SimpleCADAPI B-Rep 硬验收
```
SimpleCADAPI 比较器在验收区同时读取教师 STEP 和候选 STEP,检查:
- `target - candidate` 差集体积
- `candidate - target` 差集体积
- 几何点集是否一致
- 带几何标签的面/边/点邻接图是否同构
新增质量等级:
| 等级 | 含义 |
|---|---|
| `brep_exact` | 几何集合一致且拓扑邻接图同构 |
| `geometry_equal_topology_differs` | 几何通过,但拓扑结构不同,需要复查 |
| `geometry_accepted_brep_unavailable` | 原验收通过,但补充比较器不可用 |
| `review_required` | 只达到可用重建或存在其他验收问题 |
严格比较器不可用时会记录原因,不会伪造成通过;拓扑不一致时模型仍可供
查看,但任务状态为 `review_required`
## 依赖更新
两个本地 Python 环境均已安装 SimpleCADAPI `2.0.2` 的新增运行依赖:
- `jsonschema`
- `rfc8785`
- `typing-extensions`
仓库的 `SimpleCADAPI/uv.lock` 已随上游版本同步。虚拟环境仍由 `.gitignore`
排除,不会提交。
## 已完成验证
本次融合已通过:
- cad-router24 项测试
- DesignIR 编译器:11 项测试
- 上传 STEP 完整重建与 B-Rep 硬验收:1 项测试
- SimpleCADAPI 完整回归:934 项通过,3 项按可选环境条件跳过
- CAD Agent Studio8 项测试,生产构建通过
完整回归包含:
- `@model` / `ModelResult`
- 单位
- 公差链
- B-Rep 逆向检查
- 螺栓和螺母
- 滚子链轮
## 后续扩展边界
本次没有直接把所有新能力暴露到网页:
- `.scene.zip` 与现有 CAD Viewer 存在功能重叠,需要先确定统一场景合同。
- Fusion 360 和 SolidWorks 翻译必须在对应软件或命令环境中做真实执行验收。
- 单位和公差已进入路由,但 DesignIR schema 仍需增加完整公差图映射。
- B-Rep 截面 XOR 适合失败诊断,不应默认对每个大模型高密度运行。
这些能力已经保留在源码和 Skill 中,后续接入不需要再次升级依赖。
+549
View File
@@ -0,0 +1,549 @@
# SimpleCADAPI 2.0.2 更新对比与 CadSet 集成建议
> 对比日期:2026-07-28
> 当前集成版本:`/Users/jerry/linkhand/CadSet/SimpleCADAPI``2.0.1b1`
> 待评估版本:`/Users/jerry/Downloads/SimpleCADAPI-master``2.0.2`
## 1. 结论摘要
SimpleCADAPI `2.0.2` 相比 CadSet 当前集成的 `2.0.1b1` 是一次明显的增量升级,不是接口重写。
本次静态对比得到:
| 指标 | 结果 |
|---|---:|
| 当前版本 | `2.0.1b1` |
| 新版本 | `2.0.2` |
| 当前文件数 | 555 |
| 新版文件数 | 767 |
| 内容相同 | 351 |
| 新增文件 | 240 |
| 删除文件 | 28 |
| 修改文件 | 176 |
| 当前顶层公开导出 | 214 |
| 新版顶层公开导出 | 303 |
| 新增顶层公开导出 | 89 |
| 删除顶层公开导出 | 0 |
| Python 测试文件 | 34 → 51 |
| Model JSON schema | 仍为 `2.0` |
文件统计排除了 `.git`、虚拟环境、缓存、构建输出和 `node_modules` 等非源码内容。由于下载目录没有上游 Git 历史或正式 CHANGELOG,本报告基于两个目录的文件内容、公开导出、函数签名、测试、文档和依赖配置进行对比。
对 CadSet 的总体判断:
- 可以升级,现有调用大概率不会因公开 API 删除而直接失效。
- 不建议只覆盖 `SimpleCADAPI/` 后结束;这样只能获得兼容性更新,无法充分利用新增能力。
- 对我们最有价值的是新版的语义标签、拓扑跟踪、单位/公差、模型入口以及 STEP B-Rep 比较工具。
- 新版 B-Rep 工具适合增强 DesignIR 重建验收,但它不是 STEP 参数化历史恢复器,也不能单独替代 SurfaceIR。
- 建议先在升级分支完成回归测试,再将新增能力分阶段接入 `designir-pipeline``cad-router` 和前端查看器。
## 2. 新增内容
### 2.1 统一的可重放模型入口
新版增加:
- `@model`
- `ModelResult`
- `capture_result(...)`
- `requires_session`
- `get_active_session`
新版推荐使用一个 `@model` 入口管理 `GraphSession`,模型函数执行后返回 `ModelResult`。结果对象可直接提供:
- 模型图 JSON
- 重放结果
- 场景包路径
- 会话和结果节点
这比我们目前在 `designir_pipeline.py` 中手动创建 `GraphSession`、手动导出 JSON 更完整,也更容易建立统一的生成合同。
对 CadSet 的价值:
- DesignIR 编译结果可统一包装为一个可重放模型。
- 生成、重放、导出和验收可以绑定同一个模型标识。
- 以后更容易追踪“哪个参数、哪个操作生成了哪个结果”。
### 2.2 物理单位和公差链
新版新增完整的单位系统:
- 长度:毫米、厘米、米、英寸、英尺
- 面积和体积单位
- 角度:度、弧度
- 百分比和无量纲单位
- 单位换算、维度推导和表达式单位检查
主要公开 API 包括:
- `Unit``Dimension``UnitLike`
- `get_unit(...)`
- `convert_value(...)`
- `infer_dimension(...)`
- `canonical_unit_for_dimension(...)`
- `expression_uses_units(...)`
同时新增公差分析:
- `DimensionTolerance`
- `ToleranceGraph`
- `ToleranceRequirement`
- `ToleranceAnalysis`
- `ToleranceReport`
- `analyze_tolerance(...)`
- `check_tolerance(...)`
对 CadSet 的价值:
- DesignIR 参数可以显式区分长度、角度、数量和无量纲值。
- 上传 STEP 推断出的毫米尺寸与文本中出现的英寸尺寸可以在进入后端前统一换算。
- 参数修改可以提前阻止长度与角度相加等无效表达式。
- 可以把制造公差和参数允许范围纳入修改验收。
需要注意:新版默认以毫米和度计算,但保留变量声明时的单位。旧式无单位变量仍被支持,不过不能与带单位变量任意混合。
### 2.3 语义标签、来源映射和拓扑跟踪增强
新增或扩展的能力包括:
- `apply_tag_rselection(...)`
- `explain_tag(...)`
-`scope``list_tags(...)`
- `TrackingPolicy`
- `LineagePolicy`
- `TopologyPropagation`
- `TagBinding``TagTarget``TagEvidence`
- `TagProducer``TagPropagation``TagLineageWitness`
- `source_binding``source_topology``output_role``solids` 等 QL 查询
基础体和特征操作现在可以直接声明角色标签。例如盒体可分别标记顶面、底面、前后左右面;圆柱可标记起始面、结束面、侧面、圆边和接缝边。拉伸、扫掠、圆角、倒角、布尔运算也增加了结果角色和跟踪策略。
对 CadSet 的价值非常直接:
- DesignIR 中的 `feature_id` 可以映射为 SimpleCADAPI 的语义 tag。
- 参数的 `affected_face_ids` 不必只依赖庞大的原始面 ID 列表,可逐步转成“特征角色 + 稳定选择条件”。
- 修改孔径、板厚或法兰间距后,可以通过标签验证目标特征是否变化、非目标特征是否保持。
- 对布尔运算后的拓扑命名问题,新版提供了更明确的来源证据和传播策略。
这部分是最值得优先接入 DesignIR 3.0 的更新。
### 2.4 Scene Schema 1.0 与 `.scene.zip`
新版新增完整的场景编译与归档子系统:
- `SceneRoot`
- `SceneSource`
- `SceneCompileOptions`
- `CompiledScenePackage`
- `compile_scene(...)`
- `export_scene(...)`
- GLB 三角面和边线生成
- 场景清单、规范化 JSON、哈希和资源校验
- 浏览器端 Scene Viewer
使用 `@model(export_dir=...)` 时可以生成一个自包含的 `<graph_id>.scene.zip`。包内包含:
- `scene.json`
- `model/model.json`
- 操作来源映射涉及的 Python 文件
- Viewer 所需的 GLB、实体和选择资源
新版同时加入:
- `scene-contract/`:独立 TypeScript 场景合同、规范化、校验和 ZIP 工具
- `viewer/`Vite/TypeScript 浏览器查看器
对 CadSet 的价值:
- 可把“可视化几何 + 模型图 + 源码映射 + 选择信息”打成一个可验证产物。
- CAD Agent Studio 目前分别管理 STEP、GLB 和 DesignIR;后续可以评估把 `.scene.zip` 作为 SimpleCADAPI 模型的附加交付物。
- 场景包适合预览、审计和语义选择,但不能替代 STEP、URDF 或 MJCF。
### 2.5 STEP B-Rep 逆向检查和比较
新版新增 `simplecadapi.inverse_engineer.brep` 及命令:
```text
simplecad-brep inspect
simplecad-brep compare
simplecad-brep render
simplecad-brep slices
```
主要能力:
- 检查 STEP 的实体、壳、面、边、曲面和曲线类型。
- 输出结构化 B-Rep 报告。
- 对目标 STEP 与候选 STEP 做双向几何和拓扑检查。
- 渲染一致的正交视图和等轴测视图。
- 对指定物理截面采样并生成 XOR 差异图。
其中 `render` 和截面图片依赖可选的 `matplotlib`
对 CadSet 的价值:
- 可作为上传 STEP 重建前的几何证据提取补充。
- 可作为验收 Agent 的第二套独立检查器。
- 截面 XOR 对内部孔、腔体和薄壁差异尤其有帮助。
- 可为失败零件生成更易分析的结构化报告。
边界说明:
- 该模块明确是检查、比较和诊断工具。
- 它不会从 STEP 恢复原始草图、约束和真实建模历史。
- 它不能证明两个文件拥有相同的建模意图。
- 是否达到我们的“1:1”标准,仍需结合体积、质心、包围盒、双向表面距离、对称差体积、关键截面和参数修改测试。
### 2.6 新增 CAD 翻译后端
原有 FreeCAD 翻译器被重构为公共的翻译器后端合同,并新增:
- Fusion 360 translator
- SolidWorks translator
- 共享的 translator base、types 和 errors
- 各后端 capability 描述
对 CadSet 的建议:
- 初期将其视为可选派生产物,不要把它们作为 DesignIR 的权威重建后端。
- 先用一组参数化零件验证生成脚本在目标软件中的可执行性、拓扑一致性和参数可编辑性。
- FreeCAD 原有流程也需要回归,因为内部实现已经拆分重构。
### 2.7 标准件和机械零件库扩展
新增标准库模块:
- `std/fastener.py`
- `std/chain.py`
新增或正式公开的工厂能力:
- `make_bolt_rsolid`
- `make_nut_rsolid`
- `make_roller_chain_sprocket_rsolid`
- `make_straight_bevel_gear_rsolid`
现有轴承和齿轮能力仍然保留。
对 CadSet 的价值:
- `cad-router` 可将螺栓、螺母、滚子链轮和直齿锥齿轮优先路由到 SimpleCADAPI。
- DesignIR 可用语义化标准件特征代替大量低层几何操作。
- 对标准件修改,参数命名和约束可以更稳定。
### 2.8 新增几何操作
新增公开操作:
- `twisted_sweep_rsolid`
适用于带扭转角的扫掠实体。它拓展了现有拉伸、旋转、放样和普通扫掠的表达范围,但仍需在我们的后端能力表中明确其限制并增加测试。
## 3. 修改内容
### 3.1 现有函数以增量参数为主
抽查 CadSet 当前使用的核心函数,新版没有删除原有必需参数,主要增加可选的关键字参数:
| 函数 | 主要变化 |
|---|---|
| `make_box_rsolid` | 新增结果和六个方向面的标签参数 |
| `make_cylinder_rsolid` | 新增结果、端面、侧面、圆边和接缝标签参数 |
| `union_rsolid` | 新增 `tracking_policy` |
| `cut_rsolid` | 新增 `tracking_policy` |
| `export_model_json` | 新增 `result_node_ids` |
| `list_tags` | 新增 `scope` |
| 拉伸/扫掠/圆角/倒角 | 新增输出角色、结果标签或跟踪相关参数 |
因此现有代码不是立即失效。普通命名参数建议改为关键字调用,避免参数顺序变化;`union_rsolid``cut_rsolid` 的实体输入仍是 `*solids` 可变位置参数,不能写成不存在的 `solids=``solid=``cutters=` 关键字。
CadSet 当前存在的典型调用:
```python
scad.union_rsolid([shape, tool], clean=True)
scad.cut_rsolid(shape, cutters, skip_non_intersecting=False)
```
建议将布尔实体作为明确的位置参数传入,并将控制项保留为关键字参数:
```python
scad.union_rsolid(shape, tool, clean=True)
scad.cut_rsolid(
shape,
*cutters,
skip_non_intersecting=False,
)
```
新版也继续兼容嵌套序列,因此当前的 `scad.union_rsolid([shape, tool], clean=True)``scad.cut_rsolid(shape, cutters, ...)` 仍符合源码合同。新代码采用展平的位置参数只是为了表达更清晰。
### 3.2 Skill 推荐工作流发生变化
当前 Skill 主要指导 Agent 手动管理 `GraphSession` 和导出函数;新版 Skill 将以下方式设为标准:
1. 一个 `@model` 模型入口。
2. 子构建器使用 `@requires_session`
3. 使用 `capture_result(...)` 明确最终结果。
4. 通过 `ModelResult` 获取模型 JSON、重放结果和场景产物。
5. 使用标签解释和来源映射,而不是只依赖面/边遍历顺序。
如果升级源码但不更新 Skill,Agent 仍会继续按旧范式生成代码,新增能力无法被稳定利用。
### 3.3 FreeCAD 翻译器内部重构
FreeCAD 翻译器从较集中的实现拆分为:
- API
- capability 分析
- code generation
- context
- exporter
- script translator
- backend translator
公开用途仍然是从 Model JSON 生成 FreeCAD Python 或 `.FCStd`,但内部重构意味着必须重新跑翻译器测试,不能只依据顶层 API 未删除就判断行为完全一致。
### 3.4 示例与文档重组
新版增加了单位公差链示例,同时删除或移出了多个旧的独立示例和复杂项目示例。保留并修改了部分大型减速器和 BLDC 执行器示例。
这是仓库内容整理,不代表对应的公开建模 API 被删除。升级时不应以覆盖目录的方式误删我们可能引用的旧示例;需要先确认这些示例是否被 CadSet 文档或测试使用。
## 4. 依赖和工具链变化
运行时新增:
```toml
jsonschema = ">=4.25.1,<5.0.0"
rfc8785 = ">=0.1.4,<0.2.0"
typing-extensions = ">=4.12,<5"
```
可选依赖新增:
```toml
inverse-engineer = ["matplotlib>=3.7.0"]
```
开发依赖新增:
```toml
datamodel-code-generator = "==0.32.0"
```
CLI 新增:
```text
simplecad-brep
```
这些依赖分别服务于场景 schema 校验、RFC 8785 规范化 JSON、类型兼容、B-Rep 可视化和 schema 模型生成。
升级后必须同步锁文件和部署环境。否则源码替换后可能在导入场景模块时出现缺包,而不是在安装阶段明确失败。
## 5. CadSet 当前实际使用情况
CadSet 目前对 SimpleCADAPI 的核心集成位于:
```text
designir-pipeline/scripts/designir_pipeline.py
```
当前适配器主要使用:
- `GraphSession`
- `make_box_rsolid`
- `make_cylinder_rsolid`
- `union_rsolid`
- `cut_rsolid`
- `export_model_json`
- `export_step`
路由和前端相关位置包括:
```text
text-to-cad/skills/cad-router/
text-to-cad/viewer/
cad-agent-studio/
```
当前尚未接入的 `2.0.2` 新能力:
- `@model``ModelResult`
- 单位与公差链
- 丰富的角色标签和来源解释
- Scene Schema 1.0 与 `.scene.zip`
- STEP B-Rep 检查、比较和截面 XOR
- Fusion 360、SolidWorks 翻译器
- 新的紧固件、链轮和锥齿轮工厂
因此,当前项目不会因为把下载目录放在旁边就自动获得这些能力。
## 6. 对 DesignIR 3.0 和上传 STEP 重建的意义
### 6.1 可以直接帮助的部分
1. **重建前证据提取**
-`simplecad-brep inspect` 输出面、边、曲面、曲线和拓扑摘要。
- 将摘要作为 DesignIR 特征推断的证据之一。
2. **独立验收**
-`compare` 检查候选与源 STEP 的双向几何和拓扑。
-`slices` 比较关键截面并输出 XOR。
- 与现有 SurfaceIR 验收指标交叉验证。
3. **稳定参数修改**
- 把 DesignIR feature ID 映射为语义标签。
- 用来源映射和拓扑跟踪验证修改影响范围。
- 用单位和公差阻止不合法参数修改。
4. **可审计产物**
- 对 SimpleCADAPI 生成结果附带 Model JSON 或 `.scene.zip`
- 让查看器、验收器和后续 Agent 使用同一份模型语义。
### 6.2 不能直接解决的部分
- STEP 本身没有原始草图和建模历史,新版也不会凭空恢复真实历史。
- B-Rep inspection 只能提供几何与拓扑证据,特征树仍然是推断结果。
- `.scene.zip` 是 SimpleCADAPI 自有模型的场景包,不是把任意 STEP 自动转成参数化模型的万能格式。
- 语义标签只能在我们生成或映射标签后生效,不能自动让所有历史 STEP 拥有可靠参数名。
- 自由曲面、复杂过渡面和多实体装配的独立参数化重建仍需 DesignIR/SurfaceIR 扩展。
## 7. 兼容性与风险
| 风险项 | 级别 | 说明 |
|---|---|---|
| 顶层 API 删除 | 低 | 静态对比未发现删除的顶层公开导出 |
| 函数行为变化 | 中 | 特征、跟踪、标签和翻译器内部有明显修改 |
| 依赖缺失 | 中 | 新增三个运行时依赖和一个可选可视化依赖 |
| Model JSON 兼容 | 低到中 | schema 仍为 2.0,但节点负载和语义证据可能增加 |
| FreeCAD 翻译回归 | 中 | 后端内部大幅拆分,并新增统一 translator contract |
| Viewer 重复建设 | 中 | 新 Scene Viewer 与 CadSet 现有 Viewer 功能有重叠 |
| 上传 STEP 能力被高估 | 高 | B-Rep 工具不是自动参数化历史恢复工具 |
| 直接目录覆盖 | 高 | 可能覆盖本地改动、旧示例、Skill 和未提交文件 |
## 8. 推荐升级方案
### 阶段 A:安全升级源码
1. 新建升级分支,不直接覆盖 `main`
2. 记录 CadSet 当前 `SimpleCADAPI/` 是否存在本地改动。
3.`2.0.2` 更新源码、文档、Skill 和依赖配置。
4. 保留 CadSet 自己的适配层,不把项目逻辑写入上游目录。
5. 同步 Python 锁文件和部署安装脚本。
6. 运行 SimpleCADAPI 上游测试和 CadSet 集成测试。
### 阶段 B:保持功能等价
首先只做兼容迁移:
- 将调用统一改为关键字参数。
- 验证盒体、圆柱、布尔、导出 STEP 和 Model JSON。
- 验证现有 DesignIR 示例在升级前后:
- 实体数一致
- 包围盒一致
- 体积误差在容差内
- STEP 导出成功
- Model JSON 可重放
这一阶段不改变路由决策,也不宣称重建精度提高。
### 阶段 C:接入语义能力
建立 DesignIR → SimpleCADAPI 映射:
```text
DesignIR feature.id
→ SimpleCAD tag
→ operation output role
→ topology/source evidence
→ edit validation contract
```
优先覆盖:
- base/body
- bore/hole
- boss
- pocket/slot
- fillet/chamfer
- linear/polar pattern
- mounting face
- datum axis/plane
修改后验证目标 tag 的几何变化,并检查非目标 tag 是否保持。
### 阶段 D:接入上传 STEP 验收
建议将新工具接入验收 Agent,而不是直接让重建 Agent把报告当成答案:
```text
源 STEP
├─ SurfaceIR/DesignIR 现有证据
└─ SimpleCAD B-Rep inspection
重建候选 STEP
┌──────────┴──────────┐
│ 现有几何验收 │
│ SimpleCAD compare │
│ 关键截面 XOR │
└──────────┬──────────┘
统一验收报告
```
两个检查器得出冲突结论时不自动晋升经验,应进入人工审查或失败聚类。
### 阶段 E:扩展路由和交付物
回归稳定后再做:
- 路由新增 bolt、nut、sprocket、roller chain、bevel gear 关键词。
- 对 SimpleCADAPI 模型可选生成 `.scene.zip`
- 评估 CAD Agent Studio 是否复用 Scene Contract,而不是立刻替换现有 Viewer。
- 将 Fusion 360、SolidWorks 脚本作为可选下载项。
## 9. 建议的验收矩阵
### 9.1 基础回归
| 类型 | 用例 |
|---|---|
| 基础体 | box、cylinder |
| 特征 | extrude、revolve、loft、sweep、twisted sweep |
| 布尔 | union、cut、intersect |
| 修饰 | fillet、chamfer、shell |
| 阵列 | linear、polar |
| 导出 | STEP、STL、Model JSON |
| 重放 | Model JSON replay |
### 9.2 CadSet 集成回归
- `designir-pipeline` 的 SimpleCADAPI 后端测试全部通过。
- `cad-router` 的后端选择测试全部通过。
- 文本生成、图文生成和 DesignIR 参数修改各跑一组真实用例。
- 升级前后同一 DesignIR 输出做几何差异比较。
- 前端仍可加载 STEP/GLB,下载链路不受影响。
### 9.3 新能力验收
- 单位混合与非法维度表达式测试。
- 公差链通过与失败测试。
- 标签在布尔、圆角和倒角后的追踪测试。
- `.scene.zip` schema、哈希、资源集合和 Viewer 加载测试。
- `simplecad-brep inspect/compare/render/slices` CLI 测试。
- FreeCAD、Fusion 360、SolidWorks 翻译能力边界测试。
- 新标准件尺寸、轴线、体积和参数修改测试。
## 10. 最终建议
建议升级到 `2.0.2`,但采用“先兼容、再利用”的方式:
1. **先完成源码和依赖升级,证明现有 CadSet 功能没有回退。**
2. **优先接入标签、来源映射和拓扑跟踪,增强参数修改的可解释性。**
3. **把 B-Rep inspection、compare 和 slices 接入上传 STEP 的独立验收。**
4. **再接入单位、公差、新标准件和场景包。**
5. **Fusion 360、SolidWorks 和新 Viewer 放在后续可选阶段。**
对我们的核心目标“上传 STEP → 自有 DesignIR → 独立重建 → 参数修改 → 自动验收”而言,`2.0.2` 最重要的贡献不是让 STEP 自动恢复建模历史,而是提供了更好的语义记录、拓扑来源、模型重放和独立几何诊断基础。正确接入后,它能提升重建与修改闭环的可验证性;是否真正提高 1:1 重建率,仍必须通过同一批真实 STEP 的升级前后基准测试来证明。
+6
View File
@@ -15,6 +15,7 @@ output/
# Sandbox artifacts
sandbox/
design-doc/
# Generated skills archives
skills/*.tar.gz
@@ -25,3 +26,8 @@ skills/*.tar.gz
*.step
*.stl
examples.py
# Browser viewer dependencies and build output
viewer/node_modules/
viewer/dist/
viewer/pnpm-lock.yaml
+31
View File
@@ -0,0 +1,31 @@
# First of all, you should follow the phylosophy below whatever you are going to do:
## Completeness Principle — Boil the Lake
AI-assisted coding makes the macrginal cost of completeness near-zero. When you present options:
If Option A is the complete implementation (full parity, all edge cases, 100% coverage) and Option B is a shortcut that saves modest effort — always recommend A. The delta between 80 lines and 150 lines is meaningless with Codex. "Good enough" is the wrong instinct when "complete" costs minutes more.
Lake vs. ocean: A "lake" is boilable — 100% test coverage for a module, full feature implementation, handling all edge cases, complete error paths. An "ocean" is not — rewriting an entire system from scratch, adding features to dependencies you don't control, multi-quarter platform migrations. Recommend boiling lakes. Flag oceans as out of scope.
When estimating effort, always show both scales: human team time and Codex time. The compression ratio varies by task type — use this reference:
Task type Human team Codex Compression
Boilerplate / scaffolding 2 days 15 min ~100x
Test writing 1 day 15 min ~50x
Feature implementation 1 week 30 min ~30x
Bug fix + regression test 4 hours 15 min ~20x
Architecture / design 2 days 4 hours ~5x
Research / exploration 1 day 3 hours ~3x
This principle applies to test coverage, error handling, documentation, edge cases, and feature completeness. Don't skip the last 10% to "save time" — with AI, that 10% costs seconds.
Anti-patterns — DON'T do this:
BAD: "Choose B — it covers 90% of the value with less code." (If A is only 70 lines more, choose A.)
BAD: "We can skip edge case handling to save time." (Edge case handling costs minutes with Codex.)
BAD: "Let's defer test coverage to a follow-up PR." (Tests are the cheapest lake to boil.)
BAD: Quoting only human-team effort: "This would take 2 weeks." (Say: "2 weeks human / ~1 hour Codex.")
# Second: Only do necessary action, rather than do much is better.
# Third: Never use complex English Terms in chinese, you should say normal chinese.
# Before editing or adding code, plan for module splitting rather than putting code in one file which is not human readable.
# If you have any experience during working, you can write it down here, with detailed reasons.
+1
View File
@@ -8,6 +8,7 @@ recursive-include docs *.md
# 包含源码
recursive-include src *.py
recursive-include src/simplecadapi/scene/contracts *.json *.md
include src/simplecadapi/py.typed
recursive-include src/auto_tools *.py
+91 -28
View File
@@ -25,20 +25,22 @@ in a compact public API for creating solids, applying features, tagging semantic
intent, querying topology, exporting manufacturing files, and translating recorded
models into FreeCAD workflows.
Current beta: `simplecadapi==2.0.1b1`.
Current release: `simplecadapi==2.0.2`.
## What It Provides
- OCP-native shape types: `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`.
- Functional modeling operations for primitives, profiles, extrude, revolve,
loft, sweep, booleans, transforms, patterns, fillets, chamfers, and shells.
- Replayable modeling with `GraphSession`, `export_model_json(...)`,
- Replayable modeling with `@model`, `ModelResult`, `capture_result(...)`,
`import_model_json(...)`, and `replay_model_json(...)`.
- Expression parameters with `var(...)`, arithmetic expressions, and serialized
expression graphs.
- Physical units with automatic dimension inference, canonical CAD conversion,
and manufacturing tolerance-chain validation.
- QL selectors for geometry grounding, topology queries, and stable feature
selections.
- Semantic tags through `apply_tag(shape, tag)` and `list_tags(shape)`.
- Semantic tags through `apply_tag(shape=..., tag=...)` and `list_tags(shape=...)`.
- STEP/STL export and FreeCAD translation helpers for script or `.FCStd` output.
## Install
@@ -69,53 +71,113 @@ import simplecadapi as scad
out = Path("out")
out.mkdir(exist_ok=True)
base = scad.make_box_rsolid(60.0, 36.0, 8.0, bottom_face_center=(0.0, 0.0, 0.0))
hole = scad.make_cylinder_rsolid(5.0, 14.0, bottom_face_center=(0.0, 0.0, -3.0))
slot = scad.make_box_rsolid(18.0, 8.0, 14.0, bottom_face_center=(14.0, 0.0, -3.0))
base = scad.make_box_rsolid(
width=60.0, height=36.0, depth=8.0, bottom_face_center=(0.0, 0.0, 0.0)
)
hole = scad.make_cylinder_rsolid(
radius=5.0, height=14.0, bottom_face_center=(0.0, 0.0, -3.0)
)
slot = scad.make_box_rsolid(
width=18.0, height=8.0, depth=14.0, bottom_face_center=(14.0, 0.0, -3.0)
)
part = scad.cut_rsolid(base, hole, slot)
boss = scad.make_cylinder_rsolid(8.0, 7.0, bottom_face_center=(-18.0, 0.0, 8.0))
boss = scad.make_cylinder_rsolid(
radius=8.0, height=7.0, bottom_face_center=(-18.0, 0.0, 8.0)
)
part = scad.union_rsolid(part, boss)
part = scad.apply_tag(part, "role.demo.bracket")
part = scad.apply_tag(shape=part, tag="role.demo.bracket")
print("volume", round(part.get_volume(), 3))
print("faces", len(part.get_faces()))
print("tags", scad.list_tags(part))
print("tags", scad.list_tags(shape=part))
scad.export_step(part, str(out / "bracket.step"))
scad.export_stl(part, str(out / "bracket.stl"))
scad.export_step(shapes=part, filename=str(out / "bracket.step"))
scad.export_stl(shapes=part, filename=str(out / "bracket.stl"))
```
## Replayable Modeling
Use `GraphSession` when a model should be inspectable, serializable, replayable,
or translated into another CAD environment.
Use one `@scad.model` entry point when a model should be inspectable,
serializable, replayable, or translated into another CAD environment. The
decorated function owns its `GraphSession` and returns a `ModelResult`.
```python
import simplecadapi as scad
from simplecadapi import ql as Q
with scad.GraphSession() as session:
body = scad.make_box_rsolid(40.0, 24.0, 10.0, bottom_face_center=(0.0, 0.0, 0.0))
cutter = scad.make_cylinder_rsolid(4.0, 16.0, bottom_face_center=(0.0, 0.0, -3.0))
@scad.model(graph_id="chamfered_block")
def build_model():
body = scad.make_box_rsolid(
width=40.0, height=24.0, depth=10.0,
bottom_face_center=(0.0, 0.0, 0.0),
)
cutter = scad.make_cylinder_rsolid(
radius=4.0, height=16.0, bottom_face_center=(0.0, 0.0, -3.0)
)
drilled = scad.cut_rsolid(body, cutter)
bottom_circle = (
Q.edges()
.where(Q.curve_type("circle"))
.order_by(Q.center_axis("z"))
.where(Q.curve_type(kind="circle"))
.order_by(Q.center_axis(axis="z"))
.take(1)
.exactly(1)
)
final = scad.chamfer_rsolid(drilled, bottom_circle, 0.6)
final = scad.chamfer_rsolid(solid=drilled, edges=bottom_circle, distance=0.6)
scad.capture_result(value=final)
return final
model_json = scad.export_model_json(session)
rebuilt = scad.replay_model_json(model_json)
result = build_model()
model_json = result.model_json
rebuilt = result.replay()
print("recorded_nodes", session.graph.node_count)
print("recorded_nodes", result.session.graph.node_count)
print("replayed_outputs", len(rebuilt))
```
Pass `export_dir=...` to `@scad.model` when the invocation should also write
one self-contained `<graph_id>.scene.zip`. The package contains `scene.json`,
`model/model.json`, the complete project-relative Python files referenced by
operation source mappings under `sources/`, and the GLB/entity assets required
by the Viewer. Automatic export does not write adjacent model/session JSON,
STEP, STL, or FCStd files; those explicit export APIs remain available. The
package path is `result.artifact_paths["scene"]`. Without `export_dir`, model
execution remains in memory.
## Physical Units And Tolerances
Declare nominal and manufacturing-tolerance units at the variable boundary.
SimpleCAD evaluates lengths in millimeters and angles in degrees while preserving
the declaration units in model JSON:
```python
import simplecadapi as scad
width = scad.var(
"width",
1.0,
unit="in",
tolerance=0.1,
tolerance_unit="mm",
)
height = scad.var("height", 40.0, unit="mm", tolerance=0.2)
diagonal = scad.sqrt(width**2 + height**2)
analysis = scad.analyze_tolerance(diagonal)
check = scad.check_tolerance(diagonal, 0.3, tolerance_unit="mm")
print(analysis.dimension.name, analysis.unit.symbol)
print(analysis.nominal, analysis.lower_bound, analysis.upper_bound)
print("passes", check.passed)
```
Addition and subtraction require matching dimensions. Multiplication, division,
integer powers, and square root derive dimensions. Trigonometric functions require
angle or dimensionless inputs as appropriate. Legacy variables without `unit`
remain supported, but cannot be mixed with unit-declared variables in one
expression.
## Modeling Mental Model
- Start from design intent: reference axes, critical profiles, and the features
@@ -162,15 +224,12 @@ Explicit compound projections remain available for geometry-only STEP export.
Run examples from the source checkout:
```bash
uv run python examples/01_basic_modeling.py
uv run python examples/02_graph_replay.py
uv run python examples/03_expressions.py
uv run python examples/05_loft_sweep_revolve.py
uv run python examples/06_parametric_gear_model.py
uv run python examples/07_serialization_operation_tree.py
uv run python examples/04_dimension_tolerance_chain.py
uv run python examples/08_constrained_sketch.py
uv run python examples/09_naca0016_blade_freecad.py
uv run python examples/10_part_assembly.py
uv run python examples/16_compact_two_stage_planetary_reducer/main.py
uv run python examples/20_integrated_bldc_joint_actuator/main.py
```
## Documentation
@@ -179,6 +238,10 @@ uv run python examples/10_part_assembly.py
- Core type and modeling notes: [`docs/core/`](docs/core/)
- Serialization and replay details:
[`docs/core/serialization/README.md`](docs/core/serialization/README.md)
- Dimension tolerance chains:
[`docs/core/dimension-tolerance-chains.md`](docs/core/dimension-tolerance-chains.md)
- Physical units and dimension inference:
[`docs/core/physical-units.md`](docs/core/physical-units.md)
- Operation graph JSON spec:
[`docs/core/operation_graph_json_spec.md`](docs/core/operation_graph_json_spec.md)
+38 -16
View File
@@ -21,16 +21,16 @@
SimpleCADAPI 是一个基于 OCP 的 Python CAD SDK,提供清晰的函数式建模操作和可重放的模型图。它在 OpenCascade 几何内核之上提供精简的公共 API,可用于创建实体、应用特征、添加语义标签、查询拓扑、导出制造文件,以及将记录的模型转换为 FreeCAD 工作流。
当前测试版本:`simplecadapi==2.0.1b1`
当前正式版本:`simplecadapi==2.0.2`
## 核心能力
- 基于 OCP 的 `Vertex``Edge``Wire``Face``Solid` 类型。
- 支持基本体、轮廓、拉伸、旋转、放样、扫掠、布尔运算、变换、阵列、圆角、倒角和抽壳等函数式建模操作。
- 通过 `GraphSession``export_model_json(...)``import_model_json(...)``replay_model_json(...)` 记录并重放建模过程。
- 通过 `@model``ModelResult``capture_result(...)``import_model_json(...)``replay_model_json(...)` 记录并重放建模过程。
- 通过 `var(...)`、算术表达式和可序列化表达式图定义参数。
- 使用 QL 选择器定位几何、查询拓扑并稳定选择特征。
- 通过 `apply_tag(shape, tag)``list_tags(shape)` 管理语义标签。
- 通过 `apply_tag(shape=..., tag=...)``list_tags(shape=...)` 管理语义标签。
- 支持 STEP/STL 导出,以及 FreeCAD 脚本和 `.FCStd` 转换。
## 安装
@@ -63,37 +63,59 @@ import simplecadapi as scad
out = Path("out")
out.mkdir(exist_ok=True)
base = scad.make_box_rsolid(60.0, 36.0, 8.0, bottom_face_center=(0.0, 0.0, 0.0))
hole = scad.make_cylinder_rsolid(5.0, 14.0, bottom_face_center=(0.0, 0.0, -3.0))
base = scad.make_box_rsolid(
width=60.0, height=36.0, depth=8.0, bottom_face_center=(0.0, 0.0, 0.0)
)
hole = scad.make_cylinder_rsolid(
radius=5.0, height=14.0, bottom_face_center=(0.0, 0.0, -3.0)
)
part = scad.cut_rsolid(base, hole)
part = scad.apply_tag(part, "role.demo.bracket")
part = scad.apply_tag(shape=part, tag="role.demo.bracket")
print("volume", round(part.get_volume(), 3))
print("tags", scad.list_tags(part))
print("tags", scad.list_tags(shape=part))
scad.export_step(part, str(out / "bracket.step"))
scad.export_stl(part, str(out / "bracket.stl"))
scad.export_step(shapes=part, filename=str(out / "bracket.step"))
scad.export_stl(shapes=part, filename=str(out / "bracket.stl"))
```
## 可重放建模
当模型需要检查、序列化、重放或转换到其他 CAD 环境时,请使用 `GraphSession`
当模型需要检查、序列化、重放或转换到其他 CAD 环境时,请使用唯一的
`@scad.model` 顶层入口。该入口拥有自己的 `GraphSession` 并返回 `ModelResult`
```python
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(40.0, 24.0, 10.0, bottom_face_center=(0.0, 0.0, 0.0))
cutter = scad.make_cylinder_rsolid(4.0, 16.0, bottom_face_center=(0.0, 0.0, -3.0))
@scad.model(graph_id="drilled_block")
def build_model():
body = scad.make_box_rsolid(
width=40.0, height=24.0, depth=10.0,
bottom_face_center=(0.0, 0.0, 0.0),
)
cutter = scad.make_cylinder_rsolid(
radius=4.0, height=16.0, bottom_face_center=(0.0, 0.0, -3.0)
)
drilled = scad.cut_rsolid(body, cutter)
scad.capture_result(value=drilled)
return drilled
model_json = scad.export_model_json(session)
rebuilt = scad.replay_model_json(model_json)
result = build_model()
model_json = result.model_json
rebuilt = result.replay()
print("recorded_nodes", session.graph.node_count)
print("recorded_nodes", result.session.graph.node_count)
print("replayed_outputs", len(rebuilt))
```
如果模型调用还需要写出最终文件,请向 `@scad.model` 传入
`export_dir=...`。显式 `capture_result(...)` 的结果会生成一个自包含的
`<graph_id>.scene.zip`,其中包含 `scene.json``model/model.json`、operation
source mapping 引用的完整项目相对 Python 源文件(位于 `sources/`)以及
Viewer 所需的 GLB/entity 资源。自动导出不会在旁边生成 model/session JSON、
STEP、STL 或 FCStd;这些格式仍可通过显式导出 API 生成。文件路径为
`result.artifact_paths["scene"]`。省略 `export_dir` 时不会写文件。
## FreeCAD 转换
可以把记录的模型 JSON 转换为 FreeCAD Python 脚本:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,964 @@
# CADQL Tagging 与 Auto Semantics 重构设计
## 文档状态
- 状态:Partially implemented (bounded transition)
- 目标版本:Semantic Binding Schema 1.0
- 依赖:`design-docs/cadql-brep-query-language.md`
- 目标项目:SimpleCADAPI 2.x
- 主要影响模块:`tagging.py``autotag.py``core.py``topology.py``tracking.py``graph.py``serializer.py``cadql/`、各 translator backend
- 非目标:继续扩展当前 `ql.py`、为每种 feature 增加专用 selector、用 tag 替代完整 topology history
### 当前实现边界
当前 2.0 graph/model schema 已实现 Semantic Binding Schema 1.0 的 bounded subsetsource-preserving `TagBinding``local`/`inherited`/`effective`/`lineage` scope、显式 downward policy、evidence-gated tracking projection、semantic-only graph node、model-level `semantic_bindings` registry 与 strict replay。当前 `ql.py` 仅作为过渡层增加 scoped tag predicate、typed operation/origin/output-role predicate,以及 projected source-binding/source-topology predicate,不是完整 CADQL Query IR。
尚未实现完整 typed CADQL Query IR、完整 Topology Evolution Graph、GraphQL-like parser、所有 topology/use 类型以及全部 translator backend parity。当前实现继续使用 graph/model schema `2.0`;本文中依赖 schema `3.0` 或完整 provider contract 的部分仍为 proposed。
### 已实现的 feature output role 子集
当前 `TopoDelta.roles: Tuple[TopoRoleEntry, ...]` 将 output role 与
`TopoEntry.event` 分开保存。每个 role entry 使用 geometry-node-owned durable
`TopoRef`,并要求 operation-owned OCC witness、`coverage=complete`
`status=proven` 与 exact result membership。以下是当前 public contract
| Operation | Role | Kind | Requested cardinality |
| --- | --- | --- | --- |
| Extrude | `extrusion.start` | Face | exactly one |
| Extrude | `extrusion.end` | Face | exactly one |
| Extrude | `extrusion.side` | Face | at least one, bind all |
| Revolve | `revolution.start` | Face | exactly one |
| Revolve | `revolution.end` | Face | exactly one |
| Revolve | `revolution.side` | Face | at least one, bind all |
| Fillet | `fillet.patch` | Face | at least one, bind all |
| Chamfer | `chamfer.patch` | Face | at least one, bind all |
| Shell | `shell.body_face` | Face | at least one, bind all |
| Shell | `shell.offset_face` | Face | at least one, bind all |
| Shell | `shell.closing_descendant` | Face | at least one, bind all |
| Shell | `shell.wall` | Edge | at least one, bind all |
| Loft | `loft.start` | Face | exactly one |
| Loft | `loft.end` | Face | exactly one |
| Loft | `loft.side` | Face | at least one, bind all |
| Sweep | `sweep.start` | Face | exactly one |
| Sweep | `sweep.end` | Face | exactly one |
| Sweep | `sweep.side` | Face | at least one, bind all |
这些 role 由 `FirstShape``LastShape``Generated``Modified`、Fillet/Chamfer
contour API 和 exact result membership 产生;不使用 center/normal、enumeration
index 或 unmatched-to-generated fallback。Public feature 的 named tag arguments
lower 为独立 `apply_tag_rselection` node,用户
binding 是 `USER_OPERATION + ASSERTED`,自动 role binding 是
`AUTO_RULE + PROVEN + RECOMPUTE`。Geometry node params 不保存这些 tag。
Strict replay 会重建 feature 后比较 serialized/recomputed role entries,并验证
role allowlist、feature node ownership、output slot、coverage/status、topology
kind、binding ancestry、cardinality 和 exact selected refs。Full revolve 不具有
独立 start/end capOCC 未证明的 shell role 也保持 unavailable。调用方请求这些
role 时整个 operation 失败,不以 geometry guess 补齐。
当前 bounded QL 增加:
```python
ql.output_role(role_name="extrusion.end")
ql.source_binding(binding_id="tag_binding_...")
ql.source_topology(topo_id="edge_...")
```
后两个 predicate 只查询 canonical local `TagBinding.evidence`。Extrude 的
source Edge 到 generated side Face 投影只复制完整 witness 下的 local
`USER_OPERATION` binding,并保存 source binding/topology、target topology、
operation、role 和 evidence method。Replay-safe authoring 必须把
`apply_tag_rselection(...)` 返回的 semantic view 作为 feature inputdetached
semantic branch 不会引入隐藏 graph coupling。Predicate payload
deserialization 对 unknown kind、unknown/missing fields 与非法 child cardinality
fail closed。
## 1. 摘要
下一代 tagging 系统不再把所有语义压入 topology wrapper 上的 `_tags: set[str]`。Canonical truth 是可序列化、保留来源的 `TagBinding`CADQL 将 tag 作为 `semantic.tags` 属性查询,并在执行时计算 `local``inherited``effective``lineage` 四种 scope。
Auto tagging 也不再表示“给所有结果实体补一些字符串”。它是一组 evidence-gated semantic rules:规则只能在所需事实可证明时产生 `TagBinding`,证据不足时产生 `UNKNOWN`/diagnostic 或不产生 assertion,不能把 unmatched entity 推断为 generated。
本设计遵循以下边界:
1. 用户或产品域赋予的 unary categorical meaning 使用 tag。
2. Current BRep 结构事实使用 topology property/relation。
3. 数值、枚举、几何分类和诊断使用 typed property/metadata。
4. Operation、source role、split/merge/generated/preserved 和多来源因果使用 Topology Evolution Graph。
5. Tag 可以作为稳定意图锚点,但不能成为 relation、history 或 evidence 的损失性副本。
## 2. 当前问题
当前实现由四套机制叠加而成:
| 机制 | 例子 | 当前问题 |
| --- | --- | --- |
| 用户显式 tag | `role.mounting_surface` | 与系统 tag 共用一个 set,无法识别作者和 assignment。 |
| Wrapper 结构 tag | `wire.outer``edge.boundary` | 把可直接从 BRep graph 得到的事实 materialize 为字符串。 |
| 几何分类 tag | `face.top``face.side` | frame、算法和置信度不明确;未知几何退化为 enumeration index。 |
| Tracking tag | `op.cut.generated``origin.tool` | 把 Change event 和 source role 降级为 unary string,且依赖 Face-only matcher。 |
具体缺陷包括:
- `_tags: set[str]` 不保存 assignment、producer、target query、attachment、propagation、evidence 和 lifecycle。
- Downward propagation 直接复制字符串,之后无法区分 local 与 inherited。
- `_carry_source_tags()` 直接复制 source Face 字符串,无法恢复 lineage witness。
- `apply_tracking_tags()` 只遍历 Face,不能消费 Edge/Wire/TopoUse history。
- Unmatched Face 默认被标记 `generated`,把 history 缺失伪装成确定事件。
- Boolean `section_edges` 是 Edge evidence,却用 Face ID 匹配,不能正确工作。
- Loft/sweep 提供 Edge history,但 Face-only auto tagger仍会给结果 Face生成看似完整的 operation tag。
- Transform 的 pure-preserve fallback 没有逐实体 correspondence,却对所有结果 Face声称 preserved。
- `face.face_0`、pattern index 和 sketch edge zip-order 依赖 backend enumeration,不是 durable semantic identity。
- Operation runtime 的 rich `delta_entries` 与 canonical `TopoDelta.entries` 分离,replay/translator 无法保证同一 semantic output。
当前测试中的“每个结果 Face 都有 operation tag”不是正确性证明,因为该覆盖率可以完全由 unmatched-to-generated fallback 产生。
## 3. 与 CADQL 的关系
CADQL 在三种图的联合视图上执行:
| 数据层 | 职责 | Tagging 是否拥有该事实 |
| --- | --- | --- |
| Operation Graph | 模型构建、assignment producer、query binding | 否;TagBinding 引用它。 |
| Current BRep Graph | contains、boundary、incident、adjacent、TopoUse | 否;tag 只作为 entity 的 semantic property。 |
| Topology Evolution Graph | Change input/output、role、event、derivation | 否;lineage scope 依赖它计算。 |
| Semantic Binding Store | 用户/系统声明的稳定 unary meaning | 是。 |
CADQL 只有一种 tag predicate
```python
cadql.tag("role.mounting_surface", scope="effective")
```
它是对 `semantic.tags` 的 property predicate,不创建新的 selector、anchor 或 context 类型。Tag assignment 的 target 本身使用同一套 `SelectionQuery[T]`;不再引入另一套 tag selector DSL。
## 4. 设计原则
### 4.1 一项事实只有一个 canonical owner
以下映射是强制的:
| 事实 | Canonical owner | 可否派生为 tag |
| --- | --- | --- |
| Face 的 outer/inner loop | `topology.loop_role` | 默认否。 |
| Edge 是 Face boundary | `boundary_uses`/`use_entity` | 否。 |
| Surface 是 plane/cylinder | `geometry.surface_type` | 默认否。 |
| Face 当前在 world +Z 方向 | oriented geometry predicate + frame | 否。 |
| Entity 由某 operation 产生 | `provenance.produced_by` | 否。 |
| Entity 是 preserved/modified/generated | Change output event | 否。 |
| Entity 来自 body/tool/profile/path | Change input role/path | 否。 |
| 用户声明 mounting surface | TagBinding | 是,且这是 canonical truth。 |
| 业务规则证明某面是 sealing surface | TagBinding + rule evidence | 是。 |
| 长度、半径、pattern index | typed property/metadata | 否。 |
Tag materialized cache 可以为性能复制 relation/property 的结果,但必须标记为 derived cache、可丢弃且不能成为 replay truth。Semantic Binding Schema 1.0 不定义此优化。
CADQL 中的 `semantic.role``semantic.group` 是 registry-backed TagBinding projection,例如从 `role.*``group.*` bindings 得到结构化视图,不建立第二份 writable storage。`semantic.material` 等具有独立 schema、互斥或结构化值的属性使用 typed semantic property,不同时编码为 tag。查询 provider必须从同一个 canonical owner求值这些属性。
### 4.2 Tag 只表达 unary categorical semantics
适合 tag 的内容:
```text
role.mounting_surface
role.sealing_surface
anchor.datum.primary
group.fasteners
process.inspect
```
不适合 tag 的内容:
```text
op.make_cut_rsolid.generated
origin.tool
face.area.245
edge.index.3
face.normal.+z
wire.outer
```
判断标准:如果完整含义需要 source、target、operation、frame、单位、置信度或多个参与者,它就不是单个 tag。
### 4.3 Evidence 不足时保持未知
Auto semantic rule 的三值结果为:
```text
PROVEN 所需 evidence 完整,允许发出 binding。
NOT_APPLICABLE 规则已完整求值,candidate 不满足。
UNKNOWN evidence 或 capability 不足,禁止发出 binding。
```
`UNKNOWN` 不能被实现为 false、generated、preserved 或最佳猜测。Strict replay 中,如果 feature 依赖该 auto semantic`UNKNOWN` 必须产生结构化错误;非依赖型装饰 semantic 可以只生成 diagnostic。
### 4.4 Query intent 与 execution evidence 分离
Target query 描述要标记谁;assignment evidence 描述某次执行为何解析到这些 entities。Replay 先重新执行 query,再比较 evidence。`TopoRef.topo_id` 和 geometry fingerprint 只属于 evidence,不是 TagBinding identity。
### 4.5 Semantic state 是版本化 shape view
`apply_tag_rselection` 不修改 geometry,但产生新的 semantic state version。相同 BRep entity 在 assignment 前后可以有不同 effective tags。CADQL provider 必须相对当前 scope producer/version 读取 bindings,不能从全局 mutable wrapper set 读取历史无关的并集。
## 5. Canonical 数据模型
### 5.1 TagDefinition
Tag token 继续使用 normalized dot-token 格式:
```text
[a-z][a-z0-9_-]*(.[a-z][a-z0-9_-]*)*
```
可选 registry 为已知 namespace 声明行为:
```json
{
"tag": "role.mounting_surface",
"namespace": "role",
"value_kind": "categorical",
"allowed_target_types": ["face"],
"default_topology_propagation": "local",
"default_lineage_policy": "continuation_fragment"
}
```
Registry 用于 validation 和文档,不拥有 assignment。未注册但格式合法的 custom tag 可以被允许,但必须显式提供 propagation 和 lineage policy,不能从字符串前缀静默猜测。
### 5.2 TagBinding
Canonical assignment
```json
{
"schema_version": "1.0",
"binding_id": "tag_binding_mounting_surface",
"tag": "role.mounting_surface",
"producer": {
"kind": "user_operation",
"node_id": "node_tag_mounting_surface",
"rule_id": null,
"rule_version": null
},
"scope": {
"node_id": "node_body",
"output_slot": 0
},
"target": {
"kind": "selection_query",
"query_hash": "sha256:...",
"binding_hash": "sha256:..."
},
"attachment": "local",
"propagation": {
"topology": "local",
"lineage": "continuation_fragment"
},
"evidence": {
"kind": "query_execution",
"execution_hash": "sha256:...",
"selected_refs": []
},
"certainty": "asserted",
"lifecycle": "assertion"
}
```
字段语义:
| 字段 | Contract |
| --- | --- |
| `binding_id` | Assignment identity,不由 selected topology ID 生成。 |
| `tag` | Unary semantic token。 |
| `producer` | 用户 operation 或 versioned auto rule。 |
| `scope` | Target query 的 current snapshot/version boundary。 |
| `target` | Canonical Query IR binding 或仅用于 legacy import 的 explicit refs。 |
| `attachment` | 新 assignment 只允许 `local``inherited` 是计算结果。`effective_legacy` 仅用于迁移。 |
| `propagation.topology` | `local` 或显式 `downward``local` 表示不沿 topology传播。 |
| `propagation.lineage` | 允许沿哪些 derivation 计算 lineage visibility。 |
| `evidence` | Assignment execution witness,不参与 query intent hash。 |
| `certainty` | `asserted``proven`;猜测值不能进入 canonical bindings。 |
| `lifecycle` | replay 时重新解析、重新计算或保持 legacy snapshot。 |
### 5.3 TargetBinding
新 assignment 的 target 必须是:
```text
selection_query canonical Query IR + runtime bindings
scope_root lower 为选择当前 scope root 的 canonical query
```
以下 target 只用于迁移:
```text
explicit_refs execution-local refs,无 semantic intent
legacy_effective 只有旧 flat tag snapshot
```
Auto rule 可以直接在 producer operation 的已知 Change outputs 上产生 target,但 serialization 时仍 lower 为 canonical source-preserving target
- 引用 producer output variable 的 query;或
- 引用 versioned Change output set 的 internal binding。
不能只保存“当时第 3 个 Face”。
### 5.4 Producer
Producer kind
```text
user_operation
auto_rule
imported_sidecar
legacy_import
```
Auto rule 必须保存稳定 `rule_id``rule_version`。修改 rule 语义必须提升 versionreplay 可据此检测 semantic drift。
### 5.5 Evidence
Evidence 是 tagged union
| Kind | 必需内容 | 适用场景 |
| --- | --- | --- |
| `user_assertion` | assignment node、authoring source | 用户明确声明。 |
| `query_execution` | query/binding/execution hash、selected refs | Query target assignment。 |
| `topology_change` | Change IDs、input/output witnesses、coverage | 基于 lineage 的 auto rule。 |
| `geometry_classification` | algorithm/version、frame、tolerance、measured properties | 几何分类规则。 |
| `imported_sidecar` | source URI/hash/schema | 外部 semantic data。 |
| `legacy_snapshot` | imported tags、migration diagnostic | 旧模型兼容。 |
Evidence 可以证明 assignment,但不会把几何 fingerprint 升级为 durable target identity。
## 6. Tag Scope 的正式语义
### 6.1 Local
`local` 返回当前 semantic state version 中,target query 直接匹配该 entity/use 的 bindings。
```text
local(e, v) = {b.tag | binding b is visible in semantic version v
AND target(b, v) contains e}
```
它不包含 topology parent、model entity 或 lineage ancestor 的 tag。
### 6.2 Inherited
`inherited` 只来自显式 topology propagation policy
```text
inherited(e, v) = bindings attached to ancestor a
where topology propagation is downward
and contains-path(a, e) is valid in snapshot v
```
规则:
- 不再根据 `role.*``anchor.*``group.*` 前缀自动决定传播。
- Propagation 在 assignment 时显式冻结并序列化。
- Downward 传播计算于 Current BRep containment,不复制字符串。
- 默认是 `local`,即不传播。
- 给 Solid 标记 `role.mounting_plate` 不应默认让每条 Edge 都成为 mounting plate。
- 若确实表达 assembly/model membership,优先使用 model relation 或显式 group binding;不要依靠 topology descendants 模拟产品结构。
### 6.3 Effective
```text
effective(e, v) = local(e, v) union inherited(e, v)
```
`effective` 不自动包含 lineage。这样 current snapshot 内的 attachment semantics 与跨 operation ancestry 保持正交。
### 6.4 Lineage
`lineage` 通过 TagBinding 与 Topology Evolution Graph 联合计算:
```text
lineage(e, v) = source bindings whose tagged entities are reachable
through allowed Change output derivations
```
默认 policy
| Policy | 允许 derivation | 说明 |
| --- | --- | --- |
| `none` | 无 | 不跨 operation 可见。 |
| `continuation` | `CONTINUATION` | 只接受同角色延续。 |
| `continuation_fragment` | `CONTINUATION``FRAGMENT` | 允许 trim/split fragments。 |
| `explicit` | assignment 指定集合 | 允许 `MERGE`/`REPLACEMENT` 等业务定义。 |
`INTERSECTION``BOUNDARY` 默认不继承 source tag,因为新 entity 是由 source 参与生成,不等于 source 语义角色的延续。需要选择这类结果时使用 `generated_from``depends_on``cadql.change(...)``MERGE`/`REPLACEMENT` 也不默认继承,除非 assignment 显式声明。
`lineage` scope 只返回 lineage-derived tags;调用方若需要本地与 lineage 并集,应显式组合 predicates,不把 `effective` 的含义扩大。
History capability 为 `NONE` 或所需 path evidence 不完整时,请求 `lineage` 必须产生 `UnsupportedQueryCapabilityError`,不能返回空集冒充“没有 tag”。
### 6.5 TopoUse
TagBinding 可以 target entity 或 oriented use
- Entity tag 对同一 underlying entity 的所有 occurrences 可见。
- TopoUse tag 只对该 parent/orientation/occurrence 可见。
- TopoUse 的 inherited 与 lineage 依赖 use-level containment/evolution evidence。
- 缺少 use ancestry 时不能把 entity lineage tag 冒充 occurrence lineage tag。
## 7. Public API 与 Operation Graph
### 7.1 用户赋值
保留简单入口:
```python
apply_tag(body, "role.mounting_plate")
```
新 schema 中该 convenience API 的固定默认值是:
```text
topology_propagation = LOCAL
lineage_policy = CONTINUATION_FRAGMENT
```
因此它不会把 Solid 的 role 复制给所有 Face/Edge,但在 backend 提供完整 `CONTINUATION`/`FRAGMENT` evidence 时,该 assertion 可以通过 `scope="lineage"` 从后续版本查询。需要 topology downward inheritance 时必须调用带显式 policy 的 `apply_tag_rselection`。迁移期执行旧 model时可以保留旧 effective结果,但新 assignment不能继续按token prefix选择policy。
它 lower 为 scope-root query 和 canonical operation
```text
apply_tag_rselection(
scope: Shape<K>,
targets: SelectionSet<T>,
tag: Tag,
topology_propagation: LOCAL | DOWNWARD,
lineage_policy: NONE | CONTINUATION | CONTINUATION_FRAGMENT | EXPLICIT,
) -> Shape<K>
```
推荐的精确赋值:
```python
mounting_face = (
cadql.select(cadql.Face, in_=body)
.where(
cadql.surface_type.eq("plane")
& cadql.normal.same_direction_as(
(0, 0, 1),
relative_to=body,
angle_tolerance=cadql.deg(0.1),
)
)
.expect_one()
)
tagged_body = apply_tag_rselection(
body,
targets=mounting_face,
tag="role.mounting_surface",
topology_propagation="local",
lineage_policy="continuation_fragment",
)
```
Active `GraphSession` 必须记录 target Query IR 和 binding,不能只记录 resolved Face refs。
### 7.2 读取
CADQL 是 canonical query API
```python
cadql.select(cadql.Face, in_=body).where(
cadql.tag("role.mounting_surface", scope="effective")
)
```
`list_tags(shape)` 可以保留为 current wrapper convenience,但默认返回当前 version 的 `effective` tags,并应允许显式 scope
```python
list_tags(shape, scope="local")
list_tags(shape, scope="effective")
list_tags(shape, scope="lineage")
```
它不得绕过 provider 从 `_tags` 直接读取。迁移期旧签名等价于 `scope="effective"`
### 7.3 删除和替换
Tag 删除不是从 entity set 中 `discard()` 字符串,而是产生新的 semantic operation,撤销或 supersede 指定 binding
```text
remove_tag_rselection(scope, targets, tag, matching_producer?) -> Shape<K>
```
默认不能静默删除其他 producer 的同名 binding。例如移除 auto-rule binding 不应删除用户 assertion。
### 7.4 Conflict semantics
同一个 entity 可同时存在多个 producer 的同名 tag,查询结果按 token 去重,但 evidence/projection 保留所有 bindings。
Tag 1.0 不定义基于命名约定的互斥,例如 `state.open``state.closed`。真正互斥的数据应使用 typed enum property。若产品域 registry 声明 tag group 互斥,assignment validation 必须报告 conflict,不能按“最后写入者获胜”处理。
## 8. Auto Semantics Rule Contract
### 8.1 Rule schema
每条规则必须声明:
```json
{
"rule_id": "simplecad.primitive.box.face_roles",
"rule_version": "1.0",
"trigger": "make_box_rsolid",
"candidate_type": "face",
"required_capabilities": [
"operation_output_roles",
"oriented_face_geometry"
],
"required_evidence": {
"history": "operation_native",
"frame": "operation_context",
"coverage": "complete"
},
"output": {
"kind": "tag_binding",
"allowed_tags": ["role.cap.start", "role.cap.end", "role.side"]
},
"unknown_policy": "diagnostic",
"lifecycle": "recompute"
}
```
Rule evaluator 返回:
```text
bindings
status: PROVEN | NOT_APPLICABLE | UNKNOWN
evidence
diagnostics
coverage
```
### 8.2 权威等级
| Level | Source | 可产生的结果 |
| --- | --- | --- |
| 1 | 用户显式 assertion/imported authoritative sidecar | `certainty=asserted` binding。 |
| 2 | Kernel/operation-native topology history | Change graph;满足业务规则时可产生 `certainty=proven` binding。 |
| 3 | Exact current topology fact | topology property/relation;通常不 materialize 为 tag。 |
| 4 | Deterministic geometry classifier | typed geometry property;只有注册业务映射后才能产生 tag。 |
| 5 | Heuristic/fingerprint | diagnostic/evidence only。 |
低等级 evidence 不能覆盖或伪装为高等级事实。尤其 geometry proximity 不能产生 preserved/generated lineage。
### 8.3 Coverage
每个 rule evaluation 必须声明:
```text
COMPLETE 候选 universe 与所需 evidence 完整。
PARTIAL 只有部分 candidates/relations 有 evidence。
NONE 无法求值。
```
只有 `COMPLETE` 且 rule 对具体 candidate 得到 `PROVEN` 时才发出需要全局排他性的 tag,例如“唯一 start cap”。`PARTIAL` 不能通过给其余 candidates 标记 opposite role 来补齐结果。
### 8.4 禁止行为
以下规则在新系统中非法:
- unmatched result entity => `generated`
- pure-preserve delta => 所有 result entities preserved,而无逐实体 mapping。
- backend enumeration index => semantic tag。
- geometry look-alike => lineage continuation。
- 将 operation 名、event 和 origin role拼接为 tag作为唯一事实。
- 给所有 descendants 无条件复制 `role.*`
- 多来源 Change => 只保留一个 `origin.*` 字符串。
- rule 失败后静默回退到 fingerprint 并仍标记 `proven`
## 9. Operation-by-Operation 策略
### 9.1 Primitive
Primitive operation 自己知道构造参数和 output roles,优先使用 operation-native evidence,而不是事后猜测几何。
| 当前输出 | 新 canonical 表达 |
| --- | --- |
| `geom.primitive.box` | `provenance.operation_type == make_box_rsolid` 或 typed semantic metadata,不需要 tag。 |
| 裸 `box` | 删除。 |
| `face.top/bottom/front/...` | 不自动作为 world-direction tag。使用 geometry predicate + explicit frame。 |
| `face.surface` | `geometry.surface_type`。 |
如果产品 API 需要稳定 feature role,可定义 frame-aware operation role
```text
role.cap.start
role.cap.end
role.side
```
这些角色必须绑定 primitive 的参数方向和 operation context,并保存 rule evidence。对于 box 的四个侧面,如果没有额外业务区别,不自动命名 front/back/left/right;数学对称性应保留为集合。
Cone 必须与 cylinder 使用同一 role contract,不再因为当前实现遗漏 `auto_tag_faces()` 而表现不同。
### 9.2 Extrude 和 Revolve
Feature provenance 和 topology evolution进入 Change graph
- Profile Face/Coedge 是 `subject``support` input。
- Start/end continuation、side/boundary outputs使用 event + derivation表示。
- `face.extrusion.start/side/end` 若保留为用户友好 semantic,必须由 operation-native role mapping产生 TagBinding,不使用 center exact equality或 normal exact equality。
- `solid.extrusion` 改为 `provenance.operation_type`,不作为 tag。
- Revolve 使用相同 Change contractfull/partial revolve 的 caps必须按实际 operation evidence处理。
推荐 query
```python
side_faces = (
cadql.select(cadql.Face, in_=extruded)
.where(
cadql.related(
cadql.generated_from,
from_=cadql.this,
to=profile_boundary,
derivations=("boundary",),
depth=1,
)
)
)
```
只有业务代码确实需要可命名的 feature role 时才额外发出 `role.extrusion.side`
### 9.3 Boolean
Boolean 不再产生 canonical `op.*``origin.*` tags。
必须记录:
- 每个 operand 的 named input port。
- 每个 source entity 的 role`subject``tool``support``context`
- 每个 output entity 的 event 和 derivation。
- Intersection Edge 的多 source Change witness。
交界 edge 使用:
```python
cadql.change(
inputs=(
cadql.change_input(subject_faces, input_port="subjects", role="support"),
cadql.change_input(tool_faces, input_port="tools", role="tool"),
),
output=cadql.this,
derivation="intersection",
)
```
不能用 `role.section_face` 替代 Edge-level `INTERSECTION` Change。若产品域确实定义“section interface”业务角色,可在上述 query 上显式应用 tag;该 tag 的 target intent仍是 Change pattern。
多步 cut/intersect 必须保留每一步 Change,不能简单拼接 event buckets 后覆盖相同 `topo_id``delta_entries`
### 9.4 Fillet、Chamfer 和 Shell
这些 feature 的核心是 target selection 与 Change graph
- 用户选择的 Edge/Face 是 `target` input。
- 邻接 support Face 是 `support` input。
- Trimmed source entities 是 `CONTINUATION`/`FRAGMENT` output。
- Transition Face 和新 boundary Edge 是 `BOUNDARY` output。
- 完全消耗的 source entity保留为 historical input与 `DELETED` output event。
默认不产生 `op.fillet.generated` 等 tag。查询 transition geometry 应使用 `generated_from``depends_on``co_result_of`
用户已有 `role.mounting_surface` 的 source Face,只沿 `CONTINUATION`/`FRAGMENT` 进入 lineage scopefillet transition Face 不自动继承该角色。
### 9.5 Transform 和 Mirror
Rigid transform 不改变 topology role,但必须提供逐 entity/use `CONTINUATION` mapping。只有 mapping complete 时 lineage tag 才可查询。
- `solid.transform.mirrored` 改为 provenance/operation property。
- 不复制 source wrapper `_tags`
- Mirror 的 orientation-sensitive TopoUse/normal 必须在新 parent occurrence和 frame中重新求值。
- 没有逐实体 mapping 时 history capability 为 `PARTIAL`,不能把所有 Face声明 preserved。
### 9.6 Loft 和 Sweep
Loft/sweep 必须首先修复 evidence kind mismatch
- Profile/path Edge history进入 Change graph。
- Result Face/Edge 与 profile/path source的关系必须由 operation-native adapter提供。
- Auto semantic evaluator按 candidate type消费对应 Change outputs,不允许 Face-only fallback。
- Profile 与 path 是不同 input ports/roles,不能把二者 tags union到 result Solid后丢失来源。
如果 backend只能提供 Edge-level partial historyFace lineage query与依赖它的 auto rule必须报告 unsupported/unknown;不能给全部 Faces标记 generated。
### 9.7 Pattern
Pattern 应有显式 Pattern operation 和 instance output relation
```text
source EntityVersion -> Change(instance identity) -> instance EntityVersion
```
每个 instance 保存稳定 operation-local `instance_key`index只作为 typed property
```text
provenance.pattern_instance_key
provenance.pattern_ordinal
```
`solid.pattern.linear/radial` 改为 operation provenance,不是 semantic tag。Source tags通过 `CONTINUATION` lineage policy可见;不要复制到每个 instance wrapper。
对完全对称的 instances,若没有用户 tag或业务 keyCADQL返回集合或 ambiguity,不用 ordinal伪造 semantic identity。
### 9.8 Sketch Promotion
Sketch entity到 BRep Edge的映射是 source binding,不是 tag naming问题:
- Sketch entity/profile使用稳定 semantic refs。
- Promotion adapter记录每个 output Edge对应的 source entity ref和 evidence。
- `sketch.*``sketch_profile.*``sketch_entity.*` 不再作为唯一 mapping。
- 禁止仅通过 `zip(edges, entity_ids)` 声称 correspondence。
- 用户附着在 sketch entity上的 semantic tag可通过 proven promotion Change在 `lineage` scope查询。
## 10. Topology、Geometry、Provenance 与 Tag 的迁移表
| 现有 tag/数据 | 新位置 | 迁移行为 |
| --- | --- | --- |
| `wire.outer`/`wire.inner` | `topology.loop_role` on WireUse/Coedge | 不导入为 semantic tag。 |
| `edge.boundary` | `boundary_uses` + `use_entity` | 不导入为 semantic tag。 |
| `face.top/bottom/...` | Legacy effective tag;新模型使用 frame-aware geometry query或 explicit role binding | 导入并警告方向语义未声明。 |
| `face.face_N` | Legacy enumeration annotation | 不生成新 binding;仅保留 migration evidence。 |
| `geom.primitive.*` | `provenance.operation_type`/typed metadata | 可保留 legacy effective,不再新发出。 |
| `op.<op>.<event>` | Change output event + performed_by Operation | 不生成新 binding。 |
| `origin.<role>` | Change input role/path | 不生成新 binding。 |
| `solid.boolean.*` | Operation provenance | 不生成新 binding。 |
| `solid.pattern.*` | Pattern provenance/property | 不生成新 binding。 |
| `role.*` 用户 tag | TagBinding | 尽可能恢复 assignment;否则 `effective_legacy`。 |
| `anchor.*` 用户 tag | TagBinding | 同上。 |
| `group.*` 用户 tag | TagBinding 或 model membership relation | 按 domain schema迁移。 |
| Numeric/index tags | typed metadata/property | 不作为 semantic binding。 |
## 11. Legacy 兼容
旧模型只有 flat tags时,导入为:
```json
{
"producer": {"kind": "legacy_import"},
"target": {"kind": "legacy_effective", "refs": []},
"attachment": "effective_legacy",
"propagation": {"topology": "local", "lineage": "none"},
"certainty": "asserted",
"lifecycle": "snapshot",
"evidence": {"kind": "legacy_snapshot"}
}
```
Provider 只把 `effective_legacy` 暴露给 `scope="effective"`。它不能回答 local、inherited 或 lineage,并产生 migration diagnostic;该 attachment不是正常assignment可写入的值。
兼容规则:
- 旧 public `list_tags()` 继续看到原 effective tokens。
- 旧 QL tag predicate可由 adapter lower 为 CADQL `effective` predicate。
- 不从 token prefix反推旧 propagation。
- 不从 `op.*`/`origin.*` 反向构造可信 Change graph。
- 如果 graph中同时有真实 TopoDelta evidence,可以独立迁移为 Change;仍不能仅依据旧 tag补齐缺失 parent mapping。
## 12. Serialization 与 Provider Contract
Model schema 3.0 增加:
```text
semantic_bindings: TagBinding[]
semantic_rules: AutoSemanticRuleRef[]
semantic_diagnostics: SemanticDiagnostic[]
```
Operation node可以引用其产生的 binding IDs。TagBinding target query复用 model顶层 `query_objects`,不嵌入语义缩水的第二份 query。
Provider 增加:
```python
class SemanticProvider(Protocol):
def bindings(self, scope_version, entity_or_use) -> Sequence[TagBinding]: ...
def tags(self, entity_or_use, scope: TagScope) -> Sequence[str]: ...
def explain_tag(self, entity_or_use, tag: str, scope: TagScope) -> Sequence[TagWitness]: ...
```
`explain_tag()` 至少返回:
```text
binding_id
producer
attachment path
topology inheritance path, if any
lineage Change path, if any
evidence status
diagnostics
```
OCP 和 FreeCAD provider必须运行同一 scope和rule conformance suite。Backend不支持某 rule所需 capability时报告 unknown/unsupported,不自行更换算法。
## 13. Invalidation 与 Replay
TagBinding lifecycle
| Lifecycle | Replay behavior |
| --- | --- |
| `recompute` | 重跑 target query或auto rule,生成新 evidence并比较 drift。 |
| `assertion` | 重跑 target query;用户 semantic保留,但 query不再满足时失败。 |
| `snapshot` | 仅 legacy/import sidecar;不能声称动态 lineage。 |
参数变化后:
1. 重建 geometry和 Change graph。
2. 重新解析 assignment target query。
3. 重新运行 versioned auto rules。
4. 撤销本次 replay中不再 proven的 derived bindings。
5. 比较旧/新 evidence并生成 drift diagnostic。
6. Feature依赖的 semantic target违反 cardinality时按 CADQL错误模型失败。
Derived auto binding不能因为曾经写入 wrapper set而永久残留。Semantic state由当前有效 bindings重新计算,不执行增量字符串清理猜测。
## 14. 错误与诊断
新增结构化错误:
| Error | 条件 |
| --- | --- |
| `TagValidationError` | token、target type、policy或registry不合法。 |
| `TagTargetResolutionError` | Assignment target query无法满足 cardinality。 |
| `SemanticConflictError` | Registry定义的互斥 semantic同时成立。 |
| `AutoSemanticEvidenceError` | Strict dependency所需rule evidence为UNKNOWN/PARTIAL。 |
| `SemanticCapabilityError` | Provider不能计算请求的tag scope或rule。 |
| `SemanticDriftError` | Strict replay中assignment/rule结果发生不允许的漂移。 |
Diagnostic 至少包含:
```text
binding_id/rule_id
scope version
required and available capability
coverage
candidate count
missing witnesses
repair suggestions
```
## 15. 测试策略
### 15.1 Binding tests
- TagBinding JSON round-trip和stable hash。
- 同名tag、不同producer不会互相覆盖。
- Target query intent与execution refs分离。
- Remove/supersede只影响指定binding。
### 15.2 Scope tests
- local不包含parent和lineage tags。
- inherited只沿显式downward policy计算。
- effective严格等于local union inherited。
- lineage只沿允许的derivations计算。
- `BOUNDARY`/`INTERSECTION` 默认不继承source role tag。
- history缺失时lineage产生capability error而不是空结果。
- entity tag和TopoUse tag保持不同visibility。
### 15.3 Auto rule precision/recall
- 每个emitted binding有required evidence witness。
- 可证明的semantic不会遗漏。
- Unmatched candidate不会得到generated/preserved tag。
- Partial coverage不会产生排他性role。
- Rule version变化产生semantic drift diagnostic。
### 15.4 Operation fixtures
- Primitive roles使用operation frame,不依赖world axis或Face枚举。
- Extrude/revolve caps和sides使用operation-native mapping。
- Boolean interface保留多input ChangeEdge evidence不被当作Face。
- Fillet/chamfer source fragments、transition Faces和boundaries可通过evolution relation选择。
- Transform/mirror有逐entity continuation mapping。
- Loft/sweep在只有Edge evidence时拒绝Face lineage assertion。
- Pattern instances保留source lineage和instance key,不以ordinal作为semantic identity。
- Sketch promotion不依赖zip/enumeration order。
### 15.5 Replay与translator
- OCP和FreeCAD的tag scopes一致。
- Model JSON round-trip后producer/evidence/policies不丢失。
- 参数变化后过期auto bindings消失。
- Legacy effective tags仍可查询,但local/inherited/lineage明确unsupported。
## 16. 实施阶段
### Phase ACharacterization 与止损
- 锁定当前public tag行为和serialized fixtures。
- 给unmatched-to-generated、section Edge/Face mismatch、loft/sweep mismatch增加failing characterization tests。
- 停止新增`op.*``origin.*`和enumeration-derived tags。
- `apply_tracking_tags()`增加diagnostic mode,逐步取消unsound fallback。
退出标准:所有已知误报都有测试,不再扩大flat-tag contract。
### Phase BSemantic Binding Schema
- 实现TagBinding、producer、target、policy、evidence和serialization。
- 实现semantic state version与binding store。
- `apply_tag_rselection`记录target Query IR。
- Flat `_tags` 暂时作为effective cache,不再是canonical truth。
退出标准:用户assignment可JSON round-trip并解释来源。
### Phase CCADQL Scope Provider
- 实现local/inherited/effective。
- 移除prefix-driven runtime copying;迁移为binding-time explicit policy。
- 实现`explain_tag()`
- Legacy tags仅通过effective adapter暴露。
退出标准:三个current-snapshot scopes通过OCP conformance tests。
### Phase DTopology Evolution Graph
- 按CADQL设计建立完整Change inputs/outputs。
-`delta_entries`合并进canonical序列化schema。
- 修复Face-only consumer、section Edge、transform correspondence和多步Change。
- 实现lineage scope与history capability validation。
退出标准:continuation/fragment/boundary/intersection fixtures可查询且缺证据时失败。
### Phase EAuto Semantic Rules
- 引入versioned rule registry和三值求值。
- 先迁移primitive与extrude roles,再迁移其他feature。
- 删除unmatched-generated和operation/origin tags的canonical生成。
- Geometry heuristic仅作为typed property或diagnostic。
退出标准:每个auto binding都有rule、evidence、coverage和replay lifecycle。
### Phase FTranslator 与清理
- FreeCAD provider实现相同semantic binding/scope contract。
- 更新docs、skills和examples到CADQL tag query。
- Deprecate `Solid.auto_tag_faces()`和内部直接`_tags`复制。
- 删除flat tag作为truth的代码路径。
退出标准:OCP/FreeCAD semantic conformance一致,新model不依赖flat tags。
## 17. 推荐决策
1. 采用`TagBinding`作为唯一canonical tag assignment`_tags`只能是迁移期effective cache。
2. Tag仅表达用户/业务域的unary categorical semantics。
3. `wire.outer``edge.boundary`迁移到Current BRep properties/relations。
4. `op.*``origin.*`、preserved/modified/generated迁移到Operation/Topology Evolution Graph。
5. 几何类型、方向、数值和pattern ordinal迁移到typed properties,不再自动materialize为tag。
6. Downward propagation默认关闭并在每个binding中显式保存;不再按prefix复制字符串。
7. `effective = local union inherited`lineage保持独立scope并依赖完整Change evidence。
8. Lineage默认只允许`CONTINUATION`/`FRAGMENT``BOUNDARY``INTERSECTION``MERGE``REPLACEMENT`不自动继承semantic role。
9. Auto semantics使用versioned、evidence-gated、三值规则;`UNKNOWN`是一等结果。
10. 立即禁止unmatched => generated、enumeration => semantic role、geometry guess => proven history。
11. 所有tag target复用`cadql.select(...)`和Query IR,不建设第二套selector或anchor API。
12. Replay和translator必须保存并解释assignment intent、producer、scope、evidence和drift,不能只搬运最终字符串集合。
这套策略的核心不是让auto tagging覆盖更多实体,而是让每个可查询semantic都有明确作者、目标意图、适用scope、证明依据和失效规则。CADQL负责查询这些事实;Current BRep Graph和Topology Evolution Graph负责证明结构与历史;TagBinding只负责承载真正的semantic assertion。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
# Dimension
## Class Definition
```python
class Dimension(length: int = 0, angle: int = 0)
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import Dimension`
## Description
Physical dimension represented by integer length and angle exponents.
@@ -0,0 +1,20 @@
# DimensionTolerance
## Class Definition
```python
class DimensionTolerance(lower_deviation: float, upper_deviation: float)
```
*Source: expr.py*
## Import Surface
- top-level: `from simplecadapi import DimensionTolerance`
## Description
Permitted lower and upper deviations from a nominal dimension.
Deviations are signed: the lower deviation must be less than or equal to
zero and the upper deviation must be greater than or equal to zero.
@@ -21,6 +21,7 @@ Current design goals:
- Translate only from the canonical low-level `graph` IR
- Preserve node metadata and graph lineage as FreeCAD custom properties
- Preserve `expression_graph` as explicit translator metadata
- Preserve dimension tolerances and tolerance-chain requirements as metadata
- Preserve exported assembly constraints as document metadata objects
- Keep assembly metadata from the full model payload alongside the IR-driven
geometry translation
+11 -12
View File
@@ -16,16 +16,15 @@ class GraphSession(graph_id: Optional[str] = None)
Context manager that records CAD operations into a DAG.
with GraphSession() as session:
n1 = record_operation(
"make_line_redge", {"start": (0, 0, 0), "end": (1, 0, 0)}
)
n2 = record_operation(
"make_line_redge", {"start": (1, 0, 0), "end": (1, 1, 0)}
)
record_operation(
"make_wire_from_edges_rwire", {"edge_count": 2}, inputs=[n1, n2]
)
For new replayable model entry points, prefer `@scad.model`, which owns the
session and returns a `ModelResult`. Use `GraphSession` directly when composing
or testing lower-level graph workflows. `result_node_ids` reports the graph
nodes selected by `capture_result`:
# Access the graph after the session
print(session.graph.topological_order())
```python
with GraphSession(graph_id="demo") as session:
body = make_box_rsolid(width=10.0, height=6.0, depth=2.0)
session.capture_result(value=body)
print(session.result_node_ids)
```
+64
View File
@@ -0,0 +1,64 @@
# ModelResult
## Class Definition
```python
@dataclass(frozen=True)
class ModelResult:
value: Any
session: GraphSession
result_node_ids: Tuple[str, ...]
model_json: str
session_json: str
artifact_paths: Mapping[str, Path] = field(default_factory=dict)
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import ModelResult`
## Description
Immutable result returned by a function decorated with `@scad.model`.
It keeps the ordinary Python return value together with the owned session and
the durable graph artifacts produced for that model invocation.
## Attributes
- `value`: The value returned by the model function. It may be a shape, a
product assembly, or a tuple containing application-level reports and a
captured preview.
- `session`: The completed `GraphSession` that recorded the model.
- `result_node_ids`: Explicitly captured graph node ids used as model outputs.
- `model_json`: Canonical low-level operation graph JSON containing the captured
result leaves.
- `session_json`: Session JSON containing the complete session state.
- `artifact_paths`: Files written by automatic export. It contains the `scene`
key when captured geometry or product values produced a Scene ZIP.
## Artifact Export
```python
result = build_model()
exported = result.export_artifacts(output_dir="examples/out/bracket")
print(exported.artifact_paths["scene"])
```
The same export runs automatically when `@scad.model(export_dir=...)` is used.
Only values explicitly passed to `capture_result(...)` are considered final
geometry/product outputs. Automatic export writes one self-contained
`<graph_id>.scene.zip` containing the model JSON, mapped Python sources, and
render/selection assets; it does not write adjacent model/session JSON, STEP,
STL, or FCStd files. Without an export directory, no files are written.
## Replay
```python
result = build_model()
rebuilt = result.replay()
```
`replay()` is equivalent to replaying `result.model_json`. Pass
`strict=False` only when intentionally relaxing replay validation.
+54
View File
@@ -65,14 +65,38 @@ This index includes generated docs for the public SimpleCAD API surface, includi
- [loft_rsolid](loft_rsolid.md) *(from operations.py)* `top-level`
- [revolve_rsolid](revolve_rsolid.md) *(from operations.py)* `top-level`
- [sweep_rsolid](sweep_rsolid.md) *(from operations.py)* `top-level`
- [twisted_sweep_rsolid](twisted_sweep_rsolid.md) *(from operations.py)* `top-level`
## Tagging and Selection
- [apply_tag](apply_tag.md) *(from operations.py)* `top-level`
- [apply_tag_rselection](apply_tag_rselection.md) *(from operations.py)* `top-level`
- [explain_tag](explain_tag.md) *(from operations.py)* `top-level`
- [list_tags](list_tags.md) *(from operations.py)* `top-level`
- [select_edges_by_tag](select_edges_by_tag.md) *(from operations.py)* `top-level`
- [select_faces_by_tag](select_faces_by_tag.md) *(from operations.py)* `top-level`
Creation-time topology-identity tags are supported by profile constructors and
native feature primitives through `tag_prefix`. QL selectors also support `shared_boundary(...)`,
`intersection(...)`, `incident_to(...)`, `incident_face_count(...)`, and
`solids()` for relation-aware Edge selection.
## Unified Tag Contract
Topology identity and user semantics share one public tag model. Every binding
is inspected with `list_tags(...)` and `explain_tag(...)` and queried with
`ql.tag(...)`; one topology object may carry several tags for different uses.
`tag_prefix="housing"` creates topology-identity tags such as
`housing.face.top` and `housing.solid`. These bindings carry `topology_name`
evidence and project only when kernel history proves exact correspondence.
Role parameters such as `top_face_tag`, `side_faces_tag`, and
`generated_faces_tag`, plus `result_tag`, create tags whose evidence identifies
the kernel-proven role or result. Their tag text alone does not establish
topology identity. There is one public tag parameter per target role, not a
generic role-to-tag mapping.
## Boolean Operations
- [cut_rsolid](cut_rsolid.md) *(from operations.py)* `top-level`
@@ -98,6 +122,10 @@ This index includes generated docs for the public SimpleCAD API surface, includi
## Modeling Graph and Replay
- [GraphSession](GraphSession.md) *(from graph.py)* `top-level`
- [ModelResult](ModelResult.md) *(from graph.py)* `top-level`
- [capture_result](capture_result.md) *(from graph.py)* `top-level`
- [model](model.md) *(from graph.py)* `top-level`
- [requires_session](requires_session.md) *(from graph.py)* `top-level`
- [export_graph_json](export_graph_json.md) *(from serializer.py)* `top-level`
- [export_model_json](export_model_json.md) *(from serializer.py)* `top-level`
- [export_session_json](export_session_json.md) *(from serializer.py)* `top-level`
@@ -111,12 +139,34 @@ This index includes generated docs for the public SimpleCAD API surface, includi
## Expressions and Parameters
- [Const](Const.md) *(from expr.py)* `top-level`
- [DimensionTolerance](DimensionTolerance.md) *(from expr.py)* `top-level`
- [Expr](Expr.md) *(from expr.py)* `top-level`
- [ExpressionGraph](ExpressionGraph.md) *(from expr.py)* `top-level`
- [ToleranceAnalysis](ToleranceAnalysis.md) *(from tolerance.py)* `top-level`
- [ToleranceAnalysisError](ToleranceAnalysisError.md) *(from tolerance.py)* `top-level`
- [ToleranceCheck](ToleranceCheck.md) *(from tolerance.py)* `top-level`
- [ToleranceContribution](ToleranceContribution.md) *(from tolerance.py)* `top-level`
- [ToleranceGraph](ToleranceGraph.md) *(from tolerance.py)* `top-level`
- [ToleranceReport](ToleranceReport.md) *(from tolerance.py)* `top-level`
- [ToleranceRequirement](ToleranceRequirement.md) *(from tolerance.py)* `top-level`
- [ToleranceValidationError](ToleranceValidationError.md) *(from tolerance.py)* `top-level`
- [Var](Var.md) *(from expr.py)* `top-level`
- [analyze_tolerance](analyze_tolerance.md) *(from tolerance.py)* `top-level`
- [check_tolerance](check_tolerance.md) *(from tolerance.py)* `top-level`
- [const](const_function.md) *(from expr.py)* `top-level`
- [var](var_function.md) *(from expr.py)* `top-level`
## Physical Units
- [Dimension](Dimension.md) *(from units.py)* `top-level`
- [Unit](Unit.md) *(from units.py)* `top-level`
- [UnitValidationError](UnitValidationError.md) *(from units.py)* `top-level`
- [canonical_unit_for_dimension](canonical_unit_for_dimension.md) *(from units.py)* `top-level`
- [convert_value](convert_value.md) *(from units.py)* `top-level`
- [expression_uses_units](expression_uses_units.md) *(from units.py)* `top-level`
- [get_unit](get_unit.md) *(from units.py)* `top-level`
- [infer_dimension](infer_dimension.md) *(from units.py)* `top-level`
## Types and Errors
- [SimpleCADError](SimpleCADError.md) *(from errors.py)* `top-level`
@@ -207,11 +257,15 @@ This index includes generated docs for the public SimpleCAD API surface, includi
- [meta](meta.md) *(from ql.py)* `submodule:ql`
- [not_](not_.md) *(from ql.py)* `submodule:ql`
- [or_](or_.md) *(from ql.py)* `submodule:ql`
- [output_role](output_role.md) *(from ql.py)* `submodule:ql`
- [place_component_rassembly](place_component_rassembly.md) *(from operations.py)* `top-level`
- [radial_pattern_rsolidlist](radial_pattern_rsolidlist.md) *(from operations.py)* `top-level`
- [render_screenshot_rpath](render_screenshot_rpath.md) *(from operations.py)* `top-level`
- [select](select.md) *(from ql.py)* `submodule:ql`
- [solids](solids.md) *(from ql.py)* `submodule:ql`
- [solve_assembly_constraints_rassembly](solve_assembly_constraints_rassembly.md) *(from operations.py)* `top-level`
- [source_binding](source_binding.md) *(from ql.py)* `submodule:ql`
- [source_topology](source_topology.md) *(from ql.py)* `submodule:ql`
- [tag](tag.md) *(from ql.py)* `submodule:ql`
- [unground_component_rassembly](unground_component_rassembly.md) *(from operations.py)* `top-level`
- [value](value.md) *(from ql.py)* `submodule:ql`
+9
View File
@@ -22,3 +22,12 @@ Use `make_sketch_rsketch(...)`, `add_point_rsketch(...)`,
profiles. Public sketch construction APIs are functional and return an
updated `Sketch` document. The legacy `curves` constructor remains only for
reading already-built wire/edge containers.
Entity IDs are creation-time local identifiers, not geometry guesses. During
`make_wire_from_sketch_rwire(...)` or `make_face_from_sketch_rface(...)`, the
promotion map binds each ordered profile entity to exactly one generated Edge.
The canonical topology-identity tags are
`sketch.<sketch-name>.entity.<entity-id>` and
`sketch.<sketch-name>.profile.<profile-id>`, with `topology_name` evidence.
Downstream features can project those tags only when their kernel history
proves one-source/one-target correspondence.
@@ -0,0 +1,17 @@
# ToleranceAnalysis
## Class Definition
```python
class ToleranceAnalysis(target_expr_id: str, method: ToleranceMethod, nominal: float, lower_bound: float, upper_bound: float, lower_deviation: float, upper_deviation: float, dimension: Dimension | None = None, unit: Unit | None = None, contributions: Tuple[ToleranceContribution, ...] = ())
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceAnalysis`
## Description
Nominal value and propagated limits for an expression.
@@ -0,0 +1,17 @@
# ToleranceAnalysisError
## Class Definition
```python
class ToleranceAnalysisError
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceAnalysisError`
## Description
Raised when a tolerance chain cannot be propagated safely.
+17
View File
@@ -0,0 +1,17 @@
# ToleranceCheck
## Class Definition
```python
class ToleranceCheck(requirement: ToleranceRequirement, analysis: ToleranceAnalysis, passed: bool, lower_margin: float, upper_margin: float)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceCheck`
## Description
Validation result for one tolerance requirement.
@@ -0,0 +1,17 @@
# ToleranceContribution
## Class Definition
```python
class ToleranceContribution(variable_expr_id: str, variable_name: str, nominal: float, source_tolerance: DimensionTolerance, sensitivity: float | None, lower_deviation: float, upper_deviation: float, source_unit: Unit | None = None)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceContribution`
## Description
One source dimension's propagated contribution to a result.
+17
View File
@@ -0,0 +1,17 @@
# ToleranceGraph
## Class Definition
```python
class ToleranceGraph(expression_graph: ExpressionGraph)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceGraph`
## Description
Tolerance requirements attached to one expression graph.
+17
View File
@@ -0,0 +1,17 @@
# ToleranceReport
## Class Definition
```python
class ToleranceReport(checks: Tuple[ToleranceCheck, ...] = ())
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceReport`
## Description
Validation report for every requirement in a tolerance graph.
@@ -0,0 +1,17 @@
# ToleranceRequirement
## Class Definition
```python
class ToleranceRequirement(requirement_id: str, target_expr_id: str, tolerance: DimensionTolerance, method: ToleranceMethod = 'worst_case', name: str = '', tolerance_unit: Unit | None = None, target_dimension: Dimension | None = None)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceRequirement`
## Description
Permitted result deviations for one derived dimension.
@@ -0,0 +1,17 @@
# ToleranceValidationError
## Class Definition
```python
class ToleranceValidationError(report: 'ToleranceReport')
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceValidationError`
## Description
Raised when one or more declared tolerance requirements fail.
+20
View File
@@ -0,0 +1,20 @@
# Unit
## Class Definition
```python
class Unit(symbol: str, dimension: Dimension, scale_to_canonical: float)
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import Unit`
## Description
Named unit with a scale to SimpleCAD's canonical numeric units.
Custom units are supported and serialize their symbol, dimension, and scale.
Registered built-in units serialize as compact symbols.
@@ -0,0 +1,17 @@
# UnitValidationError
## Class Definition
```python
class UnitValidationError
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import UnitValidationError`
## Description
Raised when units or expression dimensions are physically inconsistent.
+6 -2
View File
@@ -3,7 +3,7 @@
## Class Definition
```python
class Var(name: str, default: float, comment: str | None = None, expr_id: str = field(default_factory=lambda : _make_expr_id('var')))
class Var(name: str, default: float, comment: str | None = None, expr_id: str = field(default_factory=lambda : _make_expr_id('var')), tolerance: DimensionTolerance | None = None, unit: Unit | None = None, tolerance_unit: Unit | None = None)
```
*Source: expr.py*
@@ -14,4 +14,8 @@ class Var(name: str, default: float, comment: str | None = None, expr_id: str =
## Description
Named scalar parameter with a default fallback value.
Named scalar parameter with optional physical-unit and tolerance intent.
``default`` and ``tolerance`` remain in their declared units. Evaluation,
geometry parameters, and tolerance propagation convert them to SimpleCAD's
canonical CAD units: millimeters for length and degrees for angle.
+3 -1
View File
@@ -14,4 +14,6 @@ def add_line_rsketch(sketch: Sketch, entity_id: str, start: Union[SketchRef, str
## Description
Add a named line entity and return an updated sketch document.
Add an identified line entity and return an updated sketch document. During
profile promotion, `entity_id` becomes the local segment of the canonical
topology-identity tag for the corresponding profile Edge.
@@ -0,0 +1,27 @@
# analyze_tolerance
## API Definition
```python
def analyze_tolerance(value: ScalarLike, *, method: ToleranceMethod = 'worst_case') -> ToleranceAnalysis
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import analyze_tolerance`
## Description
Propagate source manufacturing tolerances through a scalar expression.
``worst_case`` returns guaranteed interval bounds. Affine chains are
dependency-aware, so repeated variables such as ``x - x`` cancel exactly;
nonlinear chains use conservative interval arithmetic. ``rss`` performs a
first-order root-sum-square calculation using analytic sensitivities.
Unit-aware variables are converted to canonical CAD units before
propagation. The returned analysis reports the inferred physical dimension
and canonical result unit. Every variable in the expression must declare a
source tolerance.
+8 -5
View File
@@ -14,10 +14,13 @@ def apply_tag(shape: AnyShape, tag: str) -> AnyShape
## Description
Attach a normalized tag to a shape using the standard propagation policy.
Attach a normalized local user tag to a shape.
Tags must already be normalized lowercase tokens such as
``role.mounting_surface`` or ``group.fasteners``. Propagation is intentionally
not configurable from the public API; the default tag policy propagates
semantic role/anchor/group tags downward and keeps topology-specific tags
local.
`role.mounting_surface` or `group.fasteners`. The default topology policy is
`local` for every tag; token prefixes do not imply downward propagation.
Lineage visibility is limited to proven continuation and fragment witnesses.
`apply_tag(...)` preserves its historical in-place wrapper behavior. Use
`apply_tag_rselection(...)` when you need an independent semantic shape view,
explicit topology propagation, or a replayable multi-entity assignment.
@@ -0,0 +1,36 @@
# apply_tag_rselection
## API Definition
```python
def apply_tag_rselection(
scope: AnyShape,
targets: Union[ShapeSelector, Sequence[AnyShape]],
tag: str,
topology_propagation: str | TopologyPropagation = TopologyPropagation.LOCAL,
lineage_policy: str | LineagePolicy = LineagePolicy.CONTINUATION_FRAGMENT,
) -> AnyShape
```
*Source: operations.py*
## Import Surface
- top-level: `from simplecadapi import apply_tag_rselection`
## Description
Return an independent semantic view over the same geometry with one canonical
`TagBinding` attached to the selected entities. `targets` may be a serializable
QL `ShapeSelector` or a non-empty sequence of topology objects belonging to
`scope`.
Topology propagation defaults to `local`. Set `topology_propagation="downward"`
only when descendants should inherit the binding. Lineage defaults to proven
continuation and fragment derivations; it never makes lineage part of the
`effective` scope.
Inside `GraphSession`, the operation records the complete binding, target intent,
and selected-reference evidence. Replay re-resolves the target and checks that
the evidence has not drifted. The semantic node does not replace geometry-owned
topology references.
@@ -0,0 +1,19 @@
# canonical_unit_for_dimension
## API Definition
```python
def canonical_unit_for_dimension(dimension: Dimension) -> Unit
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import canonical_unit_for_dimension`
## Description
Return the canonical unit used by CAD and tolerance calculations.
Length, area, volume, and angle use ``mm``, ``mm^2``, ``mm^3``, and ``deg``.
+34
View File
@@ -0,0 +1,34 @@
# capture_result
## API Definition
```python
def capture_result(*, value: Any) -> Any
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import capture_result`
## Description
Mark the graph nodes represented by `value` as the explicit final outputs of the
active model session. The value is returned unchanged, so it can be used inline
or assigned back to a local variable.
```python
@scad.model(graph_id="multi_output_demo")
def build_model():
primary = scad.make_box_rsolid(width=10.0, height=4.0, depth=2.0)
secondary = scad.make_cylinder_rsolid(radius=2.0, height=5.0)
return scad.capture_result(value=(primary, secondary))
```
Explicit capture prevents unrelated intermediate graph leaves from becoming
model outputs. When the enclosing `@model` uses `export_dir=...`, captured
geometry and product values become roots in the single self-contained Scene ZIP.
The package embeds model JSON and mapped Python sources; automatic export does
not create adjacent model/session JSON, STEP, STL, or FCStd files. It must be
called inside `@model` or another active `GraphSession`.
+15 -2
View File
@@ -3,7 +3,14 @@
## API Definition
```python
def chamfer_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], distance: ScalarLike) -> Solid
def chamfer_rsolid(
solid: Solid,
edges: Union[Sequence[Edge], ShapeSelector],
distance: ScalarLike,
*,
result_tag: Optional[str] = None,
generated_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +21,10 @@ def chamfer_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], di
## Description
Apply chamfers to selected solid edges.
Apply chamfers to selected solid edges. `generated_faces_tag` targets every face
with the kernel-proven `chamfer.patch` role. OCC contour expansion is included
rather than treating only the seed edge as the feature boundary.
The operation fails if a requested patch role has no proven result. `result_tag`
tags the resulting solid, and graph recording lowers assignments to replayable
semantic nodes.
+22
View File
@@ -0,0 +1,22 @@
# check_tolerance
## API Definition
```python
def check_tolerance(value: ScalarLike, tolerance: ToleranceLike, *, method: ToleranceMethod = 'worst_case', name: str | None = None, tolerance_unit: UnitLike | None = None) -> ToleranceCheck
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import check_tolerance`
## Description
Propagate and verify one Length or Angle requirement.
``tolerance_unit`` defaults to the target dimension's canonical unit. When
provided, it must be dimensionally compatible and is converted before the
comparison. Legacy unitless requirements remain supported when no unit is
supplied.
+17
View File
@@ -0,0 +1,17 @@
# convert_value
## API Definition
```python
def convert_value(value: int | float, from_unit: UnitLike, to_unit: UnitLike) -> float
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import convert_value`
## Description
Convert a finite numeric value between dimensionally compatible units.
+9 -1
View File
@@ -3,7 +3,11 @@
## API Definition
```python
def cut_rsolid(*solids: Union[Solid, Sequence[Solid]], skip_non_intersecting: bool = True) -> Solid
def cut_rsolid(
*solids: Union[Solid, Sequence[Solid]],
skip_non_intersecting: bool = True,
tracking_policy: TrackingPolicy | str = TrackingPolicy.FULL,
) -> Solid
```
*Source: operations.py*
@@ -29,6 +33,10 @@ sequences, and returns a single `Solid`.
- **Description**: When True, tools with no meaningful intersection are ignored for interactive convenience. Graph replay records this flag and should use False for strict diagnostic workflows.
### tracking_policy
- **Description**: `TrackingPolicy.FULL` computes topology history and lineage. `TrackingPolicy.GRAPH` preserves the canonical cut node, parameters, inputs, result topology references, and replay while omitting `TopoDelta` and history-derived topology lineage. Intersection validation and `skip_non_intersecting` behavior are unchanged.
## Returns
Solid: The cut result solid.
+27
View File
@@ -0,0 +1,27 @@
# explain_tag
## API Definition
```python
def explain_tag(
shape: AnyShape,
tag: str,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[Dict[str, Any]]
```
*Source: operations.py*
## Import Surface
- top-level: `from simplecadapi import explain_tag`
## Description
Return every visible canonical binding that produces `tag` in the requested
scope. Explanations preserve binding identity, producer, attachment, evidence,
and policy-allowed lineage witnesses, so equal tag tokens from different
producers remain distinguishable.
As with `list_tags(...)`, `effective` excludes lineage. A lineage explanation
requires complete topology-history coverage.
+11 -1
View File
@@ -3,7 +3,12 @@
## API Definition
```python
def export_model_json(session: 'GraphSession', indent: int = 2) -> str
def export_model_json(
session: 'GraphSession',
indent: int = 2,
*,
result_node_ids: Optional[Sequence[str]] = None,
) -> str
```
*Source: serializer.py*
@@ -20,3 +25,8 @@ Current Phase 1 scope uses the active session as the container of:
- operation graph
- expression graph
- capabilities/schema metadata
When `result_node_ids` is omitted, explicitly captured session results are used
when available; otherwise export falls back to graph leaves. New top-level model
code normally reads `ModelResult.model_json` instead of calling this function
directly.
@@ -0,0 +1,17 @@
# expression_uses_units
## API Definition
```python
def expression_uses_units(value: 'ScalarLike') -> bool
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import expression_uses_units`
## Description
Return whether an expression contains an explicit unit declaration.
+35 -2
View File
@@ -3,7 +3,17 @@
## API Definition
```python
def extrude_rsolid(profile: Union[Wire, Face], direction: Tuple[float, float, float], distance: ScalarLike) -> Solid
def extrude_rsolid(
profile: Union[Wire, Face],
direction: Tuple[float, float, float],
distance: ScalarLike,
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,27 @@ def extrude_rsolid(profile: Union[Wire, Face], direction: Tuple[float, float, fl
## Description
Create a solid by extruding a profile.
Create a solid by extruding a profile. Kernel history assigns the output roles
`extrusion.start`, `extrusion.end`, and `extrusion.side`. Use the semantic role
tag arguments to attach user tags to those exact role sets.
When `tag_prefix` is supplied, profile Edge tags are recommended at creation
time through the profile API (`edge_tags` and `tag_prefix`). The feature
produces `<tag_prefix>.face.start`, `<tag_prefix>.face.end`, and one
`<tag_prefix>.face.side.<profile_edge_tag>` for each kernel-proven profile Edge.
The cap Face tags are inherited by their boundary Edges, so an exact Edge
can be selected from two tagged neighboring Faces with QL `incident_to` or
`shared_boundary` rather than an enumeration index.
Start and end roles require exactly one proven face. The side role requires one
or more proven faces and tags all of them. Missing, ambiguous, or unsupported
roles fail the whole operation instead of returning an untagged result.
`result_tag` attaches a local tag to the resulting solid. In a `GraphSession`,
all requested tags lower to replayable `apply_tag_rselection` semantic nodes;
they are not stored as geometry parameters.
Topology tags produced by `tag_prefix` remain directly queryable after
booleans when OCC provides a complete preserved or modified Face history. The
result binding retains the original semantic binding ID and source topology ID;
new boundary faces do not inherit the topology tag.
+15 -2
View File
@@ -3,7 +3,14 @@
## API Definition
```python
def fillet_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], radius: ScalarLike) -> Solid
def fillet_rsolid(
solid: Solid,
edges: Union[Sequence[Edge], ShapeSelector],
radius: ScalarLike,
*,
result_tag: Optional[str] = None,
generated_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +21,10 @@ def fillet_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], rad
## Description
Apply fillets to selected solid edges.
Apply fillets to selected solid edges. `generated_faces_tag` targets every face
with the kernel-proven `fillet.patch` role. OCC contour expansion is included,
so the role is not limited to the original seed edge.
The operation fails if a requested patch role has no proven result. `result_tag`
tags the resulting solid. In a `GraphSession`, assignments are separate replayable
semantic nodes with asserted user provenance.
+17
View File
@@ -0,0 +1,17 @@
# get_unit
## API Definition
```python
def get_unit(value: UnitLike) -> Unit
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import get_unit`
## Description
Resolve a built-in unit name/alias or return an existing ``Unit``.
+21
View File
@@ -0,0 +1,21 @@
# infer_dimension
## API Definition
```python
def infer_dimension(value: 'ScalarLike') -> Dimension | None
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import infer_dimension`
## Description
Infer and validate an expression's result dimension.
``None`` means the expression uses only legacy variables without unit
declarations. Expressions that contain explicit units are validated
strictly and cannot mix in legacy variables.
+13 -2
View File
@@ -3,7 +3,10 @@
## API Definition
```python
def list_tags(shape: AnyShape) -> List[str]
def list_tags(
shape: AnyShape,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[str]
```
*Source: operations.py*
@@ -14,4 +17,12 @@ def list_tags(shape: AnyShape) -> List[str]
## Description
Return shape tags in deterministic sorted order.
Return shape tags in deterministic sorted order for one semantic scope.
- `local`: bindings attached directly to the entity.
- `inherited`: bindings visible through explicit downward topology propagation.
- `effective`: local plus inherited bindings. Lineage is not included.
- `lineage`: bindings visible through complete, policy-allowed topology history.
Lineage queries fail with a semantic capability error when complete topology
history is unavailable; they do not guess from geometry or enumeration order.
+21 -2
View File
@@ -3,7 +3,17 @@
## API Definition
```python
def loft_rsolid(profiles: List[Wire], ruled: bool = False) -> Solid
def loft_rsolid(
profiles: List[Wire],
ruled: bool = False,
*,
tracking_policy: TrackingPolicy | str = TrackingPolicy.FULL,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,13 @@ def loft_rsolid(profiles: List[Wire], ruled: bool = False) -> Solid
## Description
Create a solid by lofting multiple profiles.
Create a solid by lofting multiple profiles. Kernel history assigns
`loft.start`, `loft.end`, and `loft.side` roles. Start and end tags require one
proven face each; side tags apply to all proven side faces. `result_tag` targets
the solid. Recorded assignments are replayable semantic nodes.
`TrackingPolicy.FULL` is the default and preserves complete kernel topology
history. `TrackingPolicy.GRAPH` skips topology-history queries while still
recording and replaying the `make_loft_rsolid` graph node. In `GRAPH` mode,
`result_tag` remains available, but face-role tags and `tag_prefix` require
`FULL` tracking.
+31 -2
View File
@@ -3,7 +3,21 @@
## API Definition
```python
def make_box_rsolid(width: ScalarLike, height: ScalarLike, depth: ScalarLike, bottom_face_center: Tuple[float, float, float] = (0, 0, 0)) -> Solid
def make_box_rsolid(
width: ScalarLike,
height: ScalarLike,
depth: ScalarLike,
bottom_face_center: Tuple[float, float, float] = (0, 0, 0),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
bottom_face_tag: Optional[str] = None,
top_face_tag: Optional[str] = None,
front_face_tag: Optional[str] = None,
back_face_tag: Optional[str] = None,
left_face_tag: Optional[str] = None,
right_face_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +28,19 @@ def make_box_rsolid(width: ScalarLike, height: ScalarLike, depth: ScalarLike, bo
## Description
Create a box solid.
Create a native box solid. The exact kernel-backed Face roles are `box.bottom`,
`box.top`, `box.front`, `box.back`, `box.left`, and `box.right`. Each role has
exactly one Face.
The face tag arguments attach tags to those roles. `result_tag` targets the
result Solid. `tag_prefix="housing"` creates the topology tags
`housing.solid`, `housing.face.bottom`,
`housing.face.top`, `housing.face.front`, `housing.face.back`,
`housing.face.left`, and `housing.face.right`.
The roles and topology tags come directly from OCC Box Face witnesses, not Face
enumeration or geometric classification. The current OCP Box builder does not
expose equivalent direct Edge witnesses, so Box Edge output roles are
unsupported. Select an exact Edge from two tagged incident Faces with
`Q.edges().incident_to(face_a, face_b, distinct=True)` or
`face_a.shared_boundary(face_b)`.
+9 -2
View File
@@ -3,7 +3,13 @@
## API Definition
```python
def make_circle_redge(center: Tuple[float, float, float], radius: ScalarLike, normal: Tuple[float, float, float] = (0, 0, 1)) -> Edge
def make_circle_redge(
center: Tuple[float, float, float],
radius: ScalarLike,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
) -> Edge
```
*Source: operations.py*
@@ -14,4 +20,5 @@ def make_circle_redge(center: Tuple[float, float, float], radius: ScalarLike, no
## Description
Create a circular edge.
Create a circular edge. `tag_prefix` optionally creates the topology tag
`<tag_prefix>.edge`.
+13 -2
View File
@@ -3,7 +3,14 @@
## API Definition
```python
def make_circle_rface(center: Tuple[float, float, float], radius: ScalarLike, normal: Tuple[float, float, float] = (0, 0, 1)) -> Face
def make_circle_rface(
center: Tuple[float, float, float],
radius: ScalarLike,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tag: Optional[str] = None,
) -> Face
```
*Source: operations.py*
@@ -14,4 +21,8 @@ def make_circle_rface(center: Tuple[float, float, float], radius: ScalarLike, no
## Description
Create a circular face.
Create a circular face. `tag_prefix` creates `<tag_prefix>.face`, while
`edge_tag` supplies the final segment of `<tag_prefix>.edge.<edge_tag>` for its
boundary Edge, or the complete Edge tag when `tag_prefix` is omitted. The Face
topology tag is visible to effective boundary-Edge QL queries without copying
arbitrary local Face tags to every Edge.
+13 -2
View File
@@ -3,7 +3,14 @@
## API Definition
```python
def make_circle_rwire(center: Tuple[float, float, float], radius: ScalarLike, normal: Tuple[float, float, float] = (0, 0, 1)) -> Wire
def make_circle_rwire(
center: Tuple[float, float, float],
radius: ScalarLike,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tag: Optional[str] = None,
) -> Wire
```
*Source: operations.py*
@@ -14,4 +21,8 @@ def make_circle_rwire(center: Tuple[float, float, float], radius: ScalarLike, no
## Description
Create a circular wire.
Create a circular wire. `tag_prefix` creates `<tag_prefix>.wire`, while
`edge_tag` supplies the final segment of `<tag_prefix>.edge.<edge_tag>` for its
single circular Edge, or the complete Edge tag when `tag_prefix` is omitted.
These topology tags are preserved only where a downstream operation has
complete, kernel-proven correspondence.
+32 -2
View File
@@ -3,7 +3,22 @@
## API Definition
```python
def make_cone_rsolid(bottom_radius: ScalarLike, height: ScalarLike, top_radius: ScalarLike = 0.0, bottom_face_center: Tuple[float, float, float] = (0, 0, 0), axis: Tuple[float, float, float] = (0, 0, 1)) -> Solid
def make_cone_rsolid(
bottom_radius: ScalarLike,
height: ScalarLike,
top_radius: ScalarLike = 0.0,
bottom_face_center: Tuple[float, float, float] = (0, 0, 0),
axis: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_face_tag: Optional[str] = None,
start_edge_tag: Optional[str] = None,
end_edge_tag: Optional[str] = None,
seam_edge_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +29,19 @@ def make_cone_rsolid(bottom_radius: ScalarLike, height: ScalarLike, top_radius:
## Description
Create a cone or truncated cone solid.
Create a native cone or frustum. The kernel-backed Face roles are `cone.start`,
`cone.end`, and `cone.side`. The Edge roles are `cone.start_boundary`,
`cone.end_boundary`, and `cone.seam`.
For a pointed cone (`top_radius=0`), no top cap exists, so `cone.end` is absent
and requesting `end_face_tag` fails. `cone.end_boundary` remains available as
the kernel's degenerate apex Edge. A frustum (`top_radius>0`) has all six roles.
The role tag arguments attach tags to exact roles. `result_tag` targets the
Solid. `tag_prefix="adapter"` creates `adapter.solid`, the
Face tags `adapter.face.start`, `adapter.face.end` when present, and
`adapter.face.side`, plus `adapter.edge.start`, `adapter.edge.end`, and
`adapter.edge.seam`.
All roles and topology tags use direct OCC Cone witnesses. They are not inferred from
topology enumeration, size, normal, or position.
+33 -2
View File
@@ -3,7 +3,21 @@
## API Definition
```python
def make_cylinder_rsolid(radius: ScalarLike, height: ScalarLike, bottom_face_center: Tuple[float, float, float] = (0, 0, 0), axis: Tuple[float, float, float] = (0, 0, 1)) -> Solid
def make_cylinder_rsolid(
radius: ScalarLike,
height: ScalarLike,
bottom_face_center: Tuple[float, float, float] = (0, 0, 0),
axis: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_face_tag: Optional[str] = None,
start_edge_tag: Optional[str] = None,
end_edge_tag: Optional[str] = None,
seam_edge_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +28,21 @@ def make_cylinder_rsolid(radius: ScalarLike, height: ScalarLike, bottom_face_cen
## Description
Create a cylinder solid.
Create a native cylinder solid. The kernel-backed output roles are:
- `cylinder.start`, `cylinder.end`, and `cylinder.side` for Faces.
- `cylinder.start_boundary`, `cylinder.end_boundary`, and `cylinder.seam` for Edges.
The role tag arguments attach tags to those exact roles.
`tag_prefix="shaft"` creates the topology tag prefix `shaft`: the cap and
lateral Faces receive `shaft.face.start`, `shaft.face.end`, and
`shaft.face.side`; their boundary Edges inherit the corresponding Face tags.
The three native Edge roles additionally receive `shaft.edge.start`,
`shaft.edge.end`, and `shaft.edge.seam`.
Topology tag prefixes must be supplied while creating the profile or feature. The implementation
uses OCC primitive witnesses and exact incident topology, not face or edge
enumeration, area, normal, or position heuristics. Use QL relation/set queries
to disambiguate an Edge shared by two tagged Faces, for example
`Q.edges().incident_to(face_a, face_b, distinct=True)` or
`face_a.shared_boundary(face_b)`.
@@ -14,4 +14,12 @@ def make_face_from_sketch_rface(sketch: Sketch, profile: int | str = 0, *, requi
## Description
Promote a sketch profile to a concrete face, solving internally.
Promote a sketch profile to a concrete face, solving internally. The promoted
Face receives the canonical profile topology-identity tag and each boundary
Edge receives the exact Sketch entity tag from the promotion map. For example,
a Sketch with `name="rect"` and profile `bottom` produces `sketch.rect.profile.bottom` and
`sketch.rect.entity.bottom`.
These are creation-time tags with `topology_name` evidence backed by the solved Sketch
promotion map. They replay from the Sketch payload and promotion parameters;
ordinary compatibility tags such as `sketch_entity.bottom` remain separate.
@@ -3,7 +3,12 @@
## API Definition
```python
def make_face_from_wire_rface(wire: Wire, normal: Tuple[float, float, float] = (0, 0, 1)) -> Face
def make_face_from_wire_rface(
wire: Wire,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
) -> Face
```
*Source: operations.py*
@@ -14,4 +19,6 @@ def make_face_from_wire_rface(wire: Wire, normal: Tuple[float, float, float] = (
## Description
Create a face from a closed wire.
Create a face from a closed wire. Existing proven Edge topology tags on the
wire are copied to corresponding Face boundary Edges. `tag_prefix` optionally
adds the Face tag `<tag_prefix>.face`.
+10 -2
View File
@@ -3,7 +3,12 @@
## API Definition
```python
def make_line_redge(start: Tuple[ScalarLike, ScalarLike, ScalarLike], end: Tuple[ScalarLike, ScalarLike, ScalarLike]) -> Edge
def make_line_redge(
start: Tuple[ScalarLike, ScalarLike, ScalarLike],
end: Tuple[ScalarLike, ScalarLike, ScalarLike],
*,
tag_prefix: Optional[str] = None,
) -> Edge
```
*Source: operations.py*
@@ -14,4 +19,7 @@ def make_line_redge(start: Tuple[ScalarLike, ScalarLike, ScalarLike], end: Tuple
## Description
Create a straight edge between two points.
Create a straight edge between two points. When `tag_prefix` is provided, the
edge receives the topology tag `<tag_prefix>.edge`. Downstream profile and
feature operations may preserve that tag when kernel history proves the
correspondence.
+15 -2
View File
@@ -3,7 +3,15 @@
## API Definition
```python
def make_rectangle_rface(width: ScalarLike, height: ScalarLike, center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0), normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1)) -> Face
def make_rectangle_rface(
width: ScalarLike,
height: ScalarLike,
center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0),
normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tags: Optional[Sequence[str]] = None,
) -> Face
```
*Source: operations.py*
@@ -14,4 +22,9 @@ def make_rectangle_rface(width: ScalarLike, height: ScalarLike, center: Tuple[Sc
## Description
Create a rectangular face.
Create a rectangular face. `tag_prefix` creates `<tag_prefix>.face`, and
`edge_tags` supplies one tag for each of its four boundary Edges. With
`tag_prefix`, each is a local segment under `<tag_prefix>.edge`; without it,
each is a complete Edge tag. These topology tags can be projected to proven
feature Faces and queried with the same `list_tags(...)` and `ql.tag(...)`
surfaces as other tags.
+15 -2
View File
@@ -3,7 +3,15 @@
## API Definition
```python
def make_rectangle_rwire(width: ScalarLike, height: ScalarLike, center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0), normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1)) -> Wire
def make_rectangle_rwire(
width: ScalarLike,
height: ScalarLike,
center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0),
normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tags: Optional[Sequence[str]] = None,
) -> Wire
```
*Source: operations.py*
@@ -14,4 +22,9 @@ def make_rectangle_rwire(width: ScalarLike, height: ScalarLike, center: Tuple[Sc
## Description
Create a rectangular wire.
Create a rectangular wire. `tag_prefix` creates `<tag_prefix>.wire`.
`edge_tags` must contain one tag for each generated profile Edge, in kernel
construction order. With `tag_prefix`, each value is the local segment of
`<tag_prefix>.edge.<edge_tag>`; without it, each value is the complete Edge tag.
These topology tags are stable anchors for operations such as `extrude_rsolid`
when correspondence is proven.
+2 -1
View File
@@ -17,4 +17,5 @@ def make_sketch_rsketch(name: Optional[str] = None, *, plane: Any = 'XY', sketch
Create an empty declarative sketch document.
Use this API, not concrete edge/wire constructors, when the intent is to
build a sketch profile with constraints.
build a sketch profile with constraints. The sketch name and explicit entity
IDs are stable local identifiers used by constrained profile promotion.
@@ -3,7 +3,7 @@
## API Definition
```python
def make_wire_from_edges_rwire(edges: List[Edge]) -> Wire
def make_wire_from_edges_rwire(edges: List[Edge], *, tag_prefix: Optional[str] = None) -> Wire
```
*Source: operations.py*
@@ -14,4 +14,6 @@ def make_wire_from_edges_rwire(edges: List[Edge]) -> Wire
## Description
Create a wire from a list of connected edges.
Create a wire from a list of connected edges. Existing proven Edge topology
tags are preserved by exact topology identity; `tag_prefix` optionally adds
`<tag_prefix>.wire` to the resulting wire.
@@ -14,4 +14,11 @@ def make_wire_from_sketch_rwire(sketch: Sketch, profile: int | str = 0, *, requi
## Description
Promote a sketch profile to a concrete wire, solving internally.
Promote a sketch profile to a concrete wire, solving internally. The promotion
map preserves the exact ordered `entity_id` to Edge correspondence. A Sketch
with `name="rect"` and profile `bottom` produces the tag
`sketch.rect.profile.bottom`; entity ID `right` produces
`sketch.rect.entity.right` with `topology_name` evidence.
Promotion fails if the kernel returns a different Edge count, because the SDK
does not guess correspondence from Edge order, geometry, or measurements.
+50
View File
@@ -0,0 +1,50 @@
# model
## API Definition
```python
def model(
func=None,
*,
graph_id: Optional[str] = None,
export_dir: Optional[str | Path] = None,
) -> Callable
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import model`
## Description
Decorate the single top-level entry point of a replayable model. Each invocation
creates and owns exactly one `GraphSession`, activates it while the function
runs, captures model/session JSON in memory, and returns a `ModelResult`. When
`export_dir` is provided, explicitly captured geometry/product values produce
one `<graph_id>.scene.zip` in that directory. The package embeds
`model/model.json`, mapped project-relative Python source files under
`sources/`, and the GLB/entity assets required for rendering and selection. It
does not create adjacent model/session JSON, STEP, STL, or FCStd files.
```python
import simplecadapi as scad
@scad.model(graph_id="bracket")
def build_bracket():
body = scad.make_box_rsolid(width=20.0, height=10.0, depth=3.0)
scad.capture_result(value=body)
return body
result = build_bracket()
print(result.result_node_ids)
```
Use `result.artifact_paths["scene"]` to locate the package, or call
`result.export_artifacts(output_dir=...)` after a model has run without an
export directory. The explicit `export_dir` opt-in avoids unexpected filesystem
writes for library callers and tests.
Do not nest `@model` functions or create another `GraphSession` inside a model
function. Use `@requires_session` for child builders.
+23
View File
@@ -0,0 +1,23 @@
# output_role
## API Definition
```python
def output_role(role_name: str) -> SerializablePredicate
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Return a serializable predicate matching a kernel-proven operation output role
in `metadata["track"]`. Role matching never falls back to face order, geometry,
or flat tags. Use it with a typed selector, for example:
```python
end = ql.faces().where(ql.output_role(role_name="extrusion.end")).exactly(1)
```
@@ -3,7 +3,7 @@
## API Definition
```python
def render_screenshot_rpath(shapes: Union[Solid, Sequence[Solid]], output_path: str, highlight_tags: Optional[Sequence[str]] = None, tag_labels: Optional[Dict[str, str]] = None, image_size: Tuple[int, int] = (1400, 900), view: Union[Tuple[float, float], str] = 'auto', show_axes: bool = True, show_legend: bool = True, zoom: float = 4.0) -> str
def render_screenshot_rpath(shapes: Union[Solid, Sequence[Solid]], output_path: str, highlight_tags: Optional[Sequence[str]] = None, tag_labels: Optional[Dict[str, str]] = None, image_size: Tuple[int, int] = (1400, 900), view: Union[Tuple[float, float], str] = 'auto', show_axes: bool = True, show_legend: bool = True, zoom: float = 4.0, show_callouts: bool = True) -> str
```
*Source: operations.py*
@@ -14,4 +14,6 @@ def render_screenshot_rpath(shapes: Union[Solid, Sequence[Solid]], output_path:
## Description
Render a screenshot of shapes and save it to a file.
Render a screenshot of shapes and save it to a file. Set `show_callouts=False`
to retain highlighted material colors and the legend without placing tag labels
over the model.
+28
View File
@@ -0,0 +1,28 @@
# requires_session
## API Definition
```python
def requires_session(func=None) -> Callable
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import requires_session`
## Description
Decorate a reusable graph-producing builder that must run inside the caller's
active `GraphSession`. The decorator reuses the session owned by the enclosing
`@model` function and validates that returned graph values belong to it.
```python
@scad.requires_session
def make_bracket_body():
return scad.make_box_rsolid(width=20.0, height=10.0, depth=3.0)
```
Calling a `@requires_session` builder without an active session raises
`RuntimeError`. Builders must not create their own `GraphSession`.
+21 -2
View File
@@ -3,7 +3,18 @@
## API Definition
```python
def revolve_rsolid(profile: Union[Wire, Face], axis: Tuple[float, float, float] = (0, 0, 1), angle: ScalarLike = 360, origin: Tuple[float, float, float] = (0, 0, 0)) -> Solid
def revolve_rsolid(
profile: Union[Wire, Face],
axis: Tuple[float, float, float] = (0, 0, 1),
angle: ScalarLike = 360,
origin: Tuple[float, float, float] = (0, 0, 0),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +25,12 @@ def revolve_rsolid(profile: Union[Wire, Face], axis: Tuple[float, float, float]
## Description
Create a solid by revolving a profile around an axis.
Create a solid by revolving a profile around an axis. Kernel history assigns
`revolution.start`, `revolution.end`, and `revolution.side` output roles.
Start and end roles require exactly one proven face; side tags apply to every
proven side face. A full 360-degree revolve normally has no separate start or end
face, so requesting either cap tag raises a capability error rather than guessing.
The semantic role tag arguments attach tags to the proven cap or side Faces.
`result_tag` tags the resulting Solid. These assignments are replayable
semantic nodes.
+12
View File
@@ -15,3 +15,15 @@ def select(items: Iterable[Any]) -> Query
## Description
Start a QL query over a shape collection or selector scope.
For topology-aware selection, use `ql.faces()`, `ql.edges()`, `ql.wires()`,
`ql.vertices()`, or `ql.solids()`. `ShapeSelector.intersection(other)` forms a
serializable set intersection. `selector.shared_boundary(other,
to_kind="edge")` intersects the boundaries of two selectors. Edge selectors
also support `incident_to(face_selector, ..., distinct=True)` and
`incident_face_count(exactly=2)` to select edges by exact incident Face
witnesses and reject open or non-manifold edges.
These selectors resolve by topology identity, not enumeration order, area,
normal, or position heuristics. Their `to_dict()` payloads can be restored with
`ql.selector_from_dict(...)`.
+8 -2
View File
@@ -3,7 +3,11 @@
## API Definition
```python
def select_edges_by_tag(shape: Union[Face, Solid], tag: str) -> List[Edge]
def select_edges_by_tag(
shape: Union[Face, Solid],
tag: str,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[Edge]
```
*Source: operations.py*
@@ -14,4 +18,6 @@ def select_edges_by_tag(shape: Union[Face, Solid], tag: str) -> List[Edge]
## Description
Select edges by tag.
Select edges by an exact normalized tag in the requested semantic scope.
`effective` does not include lineage; request `scope="lineage"` explicitly when
selection depends on complete topology-history evidence.
+8 -2
View File
@@ -3,7 +3,11 @@
## API Definition
```python
def select_faces_by_tag(solid: Solid, tag: str) -> List[Face]
def select_faces_by_tag(
solid: Solid,
tag: str,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[Face]
```
*Source: operations.py*
@@ -14,4 +18,6 @@ def select_faces_by_tag(solid: Solid, tag: str) -> List[Face]
## Description
Select faces by tag.
Select faces by an exact normalized tag in the requested semantic scope.
`effective` does not include lineage; request `scope="lineage"` explicitly when
selection depends on complete topology-history evidence.
+24 -2
View File
@@ -3,7 +3,17 @@
## API Definition
```python
def shell_rsolid(solid: Solid, faces_to_remove: Union[Sequence[Face], ShapeSelector], thickness: ScalarLike) -> Solid
def shell_rsolid(
solid: Solid,
faces_to_remove: Union[Sequence[Face], ShapeSelector],
thickness: ScalarLike,
*,
result_tag: Optional[str] = None,
body_faces_tag: Optional[str] = None,
offset_faces_tag: Optional[str] = None,
closing_faces_tag: Optional[str] = None,
wall_edges_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,16 @@ def shell_rsolid(solid: Solid, faces_to_remove: Union[Sequence[Face], ShapeSelec
## Description
Shell a solid to create a hollow part.
Shell a solid to create a hollow part. The operation can expose these exact
kernel roles:
- `shell.body_face`: surviving or modified source body faces.
- `shell.offset_face`: generated offset faces.
- `shell.closing_descendant`: descendants of removed closing faces.
- `shell.wall`: generated closing-boundary edges.
The named arguments map directly to those roles. A role is available only when
OCC provides a complete witness;
requesting an unavailable role fails instead of deriving one from enumeration or
geometry. `result_tag` tags the resulting solid. Recorded assignments replay as
semantic nodes and preserve face versus edge target kinds.
+19
View File
@@ -0,0 +1,19 @@
# solids
## API Definition
```python
def solids() -> ShapeSelector
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Create a serializable selector over Solid topology. Solid selectors can be
traversed to boundary Edges and combined with `intersection(...)` or
`shared_boundary(...)` to find topology common to two named Solid selectors.
+19
View File
@@ -0,0 +1,19 @@
# source_binding
## API Definition
```python
def source_binding(binding_id: str) -> SerializablePredicate
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Match a local projected `TagBinding` whose topology-change evidence preserves the
exact source `binding_id`. Objects without canonical local binding evidence raise
an unsupported-query capability error instead of consulting flat tags.
+19
View File
@@ -0,0 +1,19 @@
# source_topology
## API Definition
```python
def source_topology(topo_id: str) -> SerializablePredicate
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Match a local projected `TagBinding` by the exact source topology identity stored
in its kernel-history evidence. This predicate queries source-preserving evidence;
it does not infer ancestry from geometry.
+18 -2
View File
@@ -3,7 +3,17 @@
## API Definition
```python
def sweep_rsolid(profile: Face, path: Wire, is_frenet: bool = False) -> Solid
def sweep_rsolid(
profile: Face,
path: Wire,
is_frenet: bool = False,
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,10 @@ def sweep_rsolid(profile: Face, path: Wire, is_frenet: bool = False) -> Solid
## Description
Create a solid by sweeping a profile along a path.
Create a solid by sweeping a profile along a path. Kernel history assigns
`sweep.start`, `sweep.end`, and `sweep.side` roles. Start and end tags require one
proven face each; side tags apply to all proven side faces. `result_tag` targets
the solid, and recorded assignments are replayable semantic nodes.
Profiles with inner wires are rejected because the current PipeShell operation
receives only the outer wire; silently dropping profile holes is not allowed.
@@ -0,0 +1,46 @@
# twisted_sweep_rsolid
## API Definition
```python
def twisted_sweep_rsolid(
profile: Face,
distance: ScalarLike,
twist_angle: ScalarLike,
axis: Tuple[float, float, float] = (0.0, 0.0, 1.0),
origin: Tuple[float, float, float] = (0.0, 0.0, 0.0),
*,
guide_radius: ScalarLike = 1.0,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
## Import Surface
- top-level: `from simplecadapi import twisted_sweep_rsolid`
## Description
Sweep a planar profile along a straight axis while rotating it linearly by the
signed total `twist_angle` in degrees. The profile must lie at the sweep start,
be planar, and be normal to `axis`.
The OCP implementation uses a one-edge straight spine and a one-edge
cylindrical auxiliary spine. This normally creates one continuous side face per
profile edge rather than splitting every side at intermediate loft sections.
Profiles with inner wires are rejected.
Kernel history assigns `twisted_sweep.start`, `twisted_sweep.end`, and
`twisted_sweep.side` roles. The operation records one canonical
`make_twisted_sweep_rsolid` graph node containing `axis`, `origin`, `distance`,
`twist_angle`, and `guide_radius`; strict replay invokes the same public
operation with the recorded parameters.
`guide_radius` controls only the auxiliary orientation guide and must be a
positive finite value. It does not set the swept profile radius.
+11 -1
View File
@@ -3,7 +3,13 @@
## API Definition
```python
def union_rsolid(*solids: Union[Solid, Sequence[Solid]], clean: bool = True, glue: bool = _DEFAULT_UNION_GLUE, tol: Optional[float] = None) -> Solid
def union_rsolid(
*solids: Union[Solid, Sequence[Solid]],
clean: bool = True,
glue: bool = _DEFAULT_UNION_GLUE,
tol: Optional[float] = None,
tracking_policy: TrackingPolicy | str = TrackingPolicy.FULL,
) -> Solid
```
*Source: operations.py*
@@ -40,6 +46,10 @@ returning multiple pieces.
- **Type**: `Optional fuzzy-boolean tolerance used by the OCC union kernel. When`
- **Description**: omitted, SimpleCAD chooses a conservative scale-aware tolerance.
### tracking_policy
- **Description**: `TrackingPolicy.FULL` computes topology history and lineage. `TrackingPolicy.GRAPH` preserves the canonical union node, parameters, inputs, result topology references, and replay while omitting `TopoDelta` and history-derived topology lineage. Geometry options `clean`, `glue`, and `tol` are unchanged.
## Returns
Solid: The merged union result.
+11 -2
View File
@@ -3,7 +3,7 @@
## API Definition
```python
def var(name: str, default: int | float, comment: str | None = None) -> Var
def var(name: str, default: int | float, comment: str | None = None, tolerance: ToleranceLike | None = None, *, unit: UnitLike | None = None, tolerance_unit: UnitLike | None = None) -> Var
```
*Source: expr.py*
@@ -14,4 +14,13 @@ def var(name: str, default: int | float, comment: str | None = None) -> Var
## Description
Create a named variable node for v2 expression-driven parameters.
Create a physical or legacy scalar variable.
``tolerance=0.1`` declares a symmetric ``+/-0.1`` tolerance. Use a
``(lower_deviation, upper_deviation)`` pair for an asymmetric tolerance.
``tolerance_unit`` defaults to ``unit`` when a nominal unit is declared.
Values are converted to canonical CAD units only when evaluated, so the
declaration and serialized expression node preserve the user's units.
Variables without ``unit`` retain legacy unitless behavior. A unit-aware
expression cannot mix declared-unit variables with legacy variables.
@@ -33,7 +33,7 @@ SimpleCADAPI 的顶层 API 选择是正确的:它没有把用户拖进传统 C
- historical scalar-field/SDF implementation, now removed from the active public/support surface
- `docs/core/serialization/`
- `docs/core/operation_graph_json_spec.md`
- `examples/07_serialization_operation_tree.py`
- retained replayable examples under `examples/`
- `test/``tests/`
## 顶层 API 评价
@@ -46,7 +46,7 @@ SimpleCADAPI 的顶层 API 选择是正确的:它没有把用户拖进传统 C
- `__init__.py` 明确导出建模函数、核心类型、graph/session、serializer、expression 和 `ql` 子模块,公开面可见性好。参考:`src/simplecadapi/__init__.py`
- 大多数核心创建函数遵守返回类型后缀,例如 `make_point_rvertex -> Vertex``make_line_redge -> Edge``make_circle_rwire -> Wire``make_box_rsolid -> Solid`。参考:`src/simplecadapi/operations.py:691``src/simplecadapi/operations.py:723``src/simplecadapi/operations.py:1465`
- composite convenience API 在 `GraphSession` 内降低为 canonical low-level graph,这个设计非常正确。用户调用 `make_box_rsolid`,图里记录 rectangle/profile/extrude,这是 CAD DSL 和 replay IR 分层的正确方式。参考:`src/simplecadapi/operations.py:1480``examples/07_serialization_operation_tree.py:52``examples/07_serialization_operation_tree.py:272`
- composite convenience API 在 `GraphSession` 内降低为 canonical low-level graph,这个设计非常正确。用户调用 `make_box_rsolid`,图里记录 rectangle/profile/extrude,这是 CAD DSL 和 replay IR 分层的正确方式。参考:`src/simplecadapi/operations.py``test/test_serialization.py`
- `union_rsolid(...)` 明确返回单个 `Solid`,失败就报错,不默默返回多个实体。这对机械 CAD 是正确的默认语义。参考:`src/simplecadapi/operations.py:2856``src/simplecadapi/operations.py:2876`
- `GraphSession` + `export_model_json` + `replay_model_json` 的主线是对的。它把脚本建模提升为可交换、可 replay 的模型记录。参考:`src/simplecadapi/graph.py:44``src/simplecadapi/serializer.py:416``src/simplecadapi/serializer.py:732`
- `ql` 被放到子模块而非全部塞进顶层,是正确的边界意识。参考:`src/simplecadapi/__init__.py``docs/api/README.md:5`
@@ -154,7 +154,7 @@ QL 是架构上最应该继续投资的部分。
- `OperationGraph` 是 DAG,节点保存 op、params、param_exprs、inputs、context、semantic_delta、topo_delta、tags。结构完整。参考:`src/simplecadapi/topology.py:318``src/simplecadapi/topology.py:476`
- `export_model_json` 输出 graph、leaf_ids、expression_graph、frame_graph、registries、delta logs、canonical contract。方向很对。参考:`src/simplecadapi/serializer.py:627`
- `_assert_graph_is_canonical` 在 export model 前拒绝非 canonical ops,这是保持 IR 干净的正确动作。参考:`src/simplecadapi/serializer.py:246``src/simplecadapi/serializer.py:624`
- `examples/07_serialization_operation_tree.py` 非常有价值。它把 source API 到 operation tree 的映射讲清楚,是 SDK 最该保留和扩展的示例。参考:`examples/07_serialization_operation_tree.py:1``examples/07_serialization_operation_tree.py:217`
- `test/test_serialization.py` 系统覆盖 source API 到 operation tree 的映射,是该契约的主要回归入口
严重问题:
@@ -164,7 +164,7 @@ QL 是架构上最应该继续投资的部分。
- graph schema version 是 `1.0`model schema 是 `2.0-draft`canonical contract 是 `2.0-final-state`。这三个版本信号混在一起,会让外部消费者不知道哪个才是稳定承诺。参考:`src/simplecadapi/topology.py:19``src/simplecadapi/serializer.py:221``src/simplecadapi/serializer.py:628`
- node.context 和 frame_graph 被记录,但 replay 不恢复 frame/workplane;所有 ops 按当前 ambient world context 执行。参考:`src/simplecadapi/graph.py:132``src/simplecadapi/serializer.py:991`
- `leaf_ids` 自动来自 graph leaves,而不是用户显式声明的 final outputs。debug/intermediate 独立节点会成为 replay 输出。参考:`src/simplecadapi/serializer.py:624``src/simplecadapi/topology.py:563`
- expression replay 是 numeric snapshot,不是 parametric replay。这个可以接受,但必须在 SDK 主文档里反复强调。参考:`examples/07_serialization_operation_tree.py:40``examples/07_serialization_operation_tree.py:287`
- expression replay 是 numeric snapshot,不是 parametric replay。这个可以接受,但必须在 SDK 主文档里反复强调。参考:`test/test_serialization.py`
建议:replay 应该默认 strict。所有 missing input、missing param、unknown op、leaf missing output、selection cardinality mismatch 都应该 hard failure。需要宽松模式时显式 `strict=False`
@@ -0,0 +1,101 @@
# Translator Backend Contract
This contract defines the package structure and dependency boundaries for every
SimpleCAD translator backend.
## Backend Naming
Backend packages live under `simplecadapi.translator` and use the name
`<backend>_translator`, where `<backend>` is a stable lowercase backend ID.
Examples:
- `freecad_translator`
- `openscad_translator`
- `onshape_translator`
Backends are exported explicitly from `simplecadapi.translator`. Importing a
backend package must not require the target CAD runtime to be installed.
## Required Files
Every backend package contains:
| File | Responsibility |
| --- | --- |
| `__init__.py` | Defines the complete public backend surface through `__all__`. |
| `api.py` | Contains public convenience functions and user-facing error boundaries. |
| `translator.py` | Contains the `<Backend>Translator` implementation. |
| `capabilities.py` | Declares backend targets and support for every canonical operation. |
The translator class must inherit `BaseTranslator`. `capabilities.py` exports
`BACKEND_NAME`, `CAPABILITIES`, and `OP_SUPPORT`. The operation support map must
contain exactly the canonical operation set. Unsupported operations require a
non-empty reason.
## Conditional Files
Use these standard names when the corresponding responsibility exists:
| File or directory | Responsibility |
| --- | --- |
| `exporter.py` | File output, external process or remote API execution, and output validation. |
| `context.py` | State owned by one translation invocation. |
| `analysis.py` | Backend-specific graph analysis and lowering decisions. |
| `codegen.py` | Pure target-code formatting and literal helpers. |
| `emitters/` | Canonical operation emitters and their single registry. |
| `runtime/` | Source fragments embedded into an artifact for execution in the target runtime. |
An `emitters/registry.py` file is the only operation-to-emitter mapping. Runtime
fragments must remain valid Python source, must not execute target APIs when the
SimpleCAD package is imported, and must be assembled in one declared order.
## Translation And Export
Translation is an in-memory operation. It consumes canonical model JSON or an
already imported canonical payload and returns a `TranslationArtifact`.
Export is an effectful operation. It may write files, invoke an executable, or
call a remote API. Those effects belong in `exporter.py`, not in the translator
or emitters.
Public naming follows these forms:
- `translate_model_json_to_<artifact>` for in-memory conversion.
- `export_model_json_to_<format>` for file or external-runtime output.
Existing public names may remain as compatibility aliases.
## Dependencies
The allowed dependency direction is:
```text
backend/__init__.py -> api.py, translator.py, capabilities.py
api.py -> translator.py, exporter.py
translator.py -> context.py, analysis.py, emitters/, runtime/
emitters/ -> context and pure code-generation helpers
exporter.py -> shared types/errors and external execution
```
The following reverse dependencies are prohibited:
- Translator or emitters importing `api.py`.
- Translator importing `exporter.py`.
- Emitters importing the public translator class.
- Runtime fragments importing the backend package.
- Capabilities importing translator implementation modules.
## Compatibility
The generated artifact and its persisted target metadata are treated as a
compatibility boundary. Pure module moves must not rename generated runtime
helpers, registries, target object properties, or public import paths. Behavior
fixes are made separately from structural migrations.
## Verification
The shared backend contract test verifies required files, public exports,
translator inheritance, backend naming, and canonical operation coverage.
Backend tests additionally verify generated artifact syntax and, where the
target runtime is available, real exported files.
+16 -8
View File
@@ -62,7 +62,7 @@ SimpleWorkplane ← local modeling context
- **Shape-first API**: users work with `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`, not graph nodes.
- **Functional modeling style**: public operations return new geometry values, e.g. `make_box_rsolid(...)`, `cut_rsolid(...)`, `fillet_rsolid(...)`.
- **OCP-native runtime**: geometry construction, topology traversal, properties, booleans, transforms, and export use OCP/OpenCascade helpers.
- **Replayable graph workflows**: `GraphSession` can record a canonical low-level operation graph and `export_model_json()` can serialize it for `replay_model_json()`.
- **Replayable graph workflows**: `@scad.model` owns one `GraphSession` and returns a `ModelResult`; `@scad.requires_session` composes child builders, and `scad.capture_result()` selects canonical output nodes for replay and export.
- **Tags and metadata**: tags are useful for lightweight semantics; structured numeric facts should be stored in metadata such as `metadata["geo"]`.
- **Indexed topology access**: use plural methods such as `get_edges()` and `get_faces()` for enumeration, and pass an index to the same getter, such as `get_edges(index)` or `get_faces(index)`, for intentional indexed picks that should become graph selection nodes.
@@ -74,11 +74,14 @@ import simplecadapi as scad
with scad.SimpleWorkplane(origin=(0, 0, 0)):
box = scad.make_box_rsolid(width=5, height=3, depth=2)
scad.apply_tag(box, "role.bracket")
scad.apply_tag(shape=box, tag="role.bracket")
box.set_metadata("material", "6061-T6")
box.auto_tag_faces("box")
top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(face)]
top_faces = [
face for face in box.get_faces()
if "face.top" in scad.list_tags(shape=face)
]
print(len(top_faces))
```
@@ -87,13 +90,18 @@ print(len(top_faces))
```python
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(10, 10, 4)
hole = scad.make_cylinder_rsolid(1.5, 8, bottom_face_center=(0, 0, -2))
@scad.model(graph_id="drilled_block")
def build_model():
body = scad.make_box_rsolid(width=10, height=10, depth=4)
hole = scad.make_cylinder_rsolid(
radius=1.5, height=8, bottom_face_center=(0, 0, -2)
)
part = scad.cut_rsolid(body, hole)
scad.capture_result(value=part)
return part
payload = scad.export_model_json(session)
rebuilt = scad.replay_model_json(payload)
result = build_model()
rebuilt = result.replay()
print(len(rebuilt))
```
@@ -0,0 +1,256 @@
# Dimension Tolerance Chains
SimpleCADAPI can attach units and manufacturing tolerances to declared dimension
variables, infer dimensions through the expression DAG, propagate source
variation, and verify derived dimensions against design requirements.
This feature is separate from sketch-solver residual tolerances, boolean fuzzy tolerances, mesh resolution, and geometric fitting tolerances. A dimension tolerance describes permitted manufacturing variation around a nominal design value; it never changes a CAD operation's numerical robustness settings.
## Declare Source Dimensions
Every variable that participates in a tolerance chain must declare a tolerance:
```python
import simplecadapi as scad
width = scad.var("width", 10.0, unit="mm", tolerance=0.1)
shaft = scad.var(
"shaft",
0.315,
unit="in",
tolerance=(-0.05, 0.0),
tolerance_unit="mm",
)
```
A scalar tolerance is symmetric. `tolerance=0.1` means `-0.1/+0.1`
around the nominal value. A two-value sequence contains signed
`(lower_deviation, upper_deviation)` values. The lower deviation must be at most
zero, the upper deviation must be at least zero, and all values must be finite.
`tolerance_unit` defaults to `unit`. It may differ from the nominal unit, but the
dimensions must match. Geometry and tolerance propagation use canonical
millimeters for length and degrees for angle. Declaration-space values remain on
the `Var` for display and serialization.
The same values can be represented explicitly:
```python
tolerance = scad.DimensionTolerance(
lower_deviation=-0.05,
upper_deviation=0.0,
)
shaft = scad.var("shaft", 8.0, unit="mm", tolerance=tolerance)
```
Tolerance identity follows `expr_id`, not the human-readable variable name. Two variables with the same name remain separate tolerance sources.
## Propagate A Chain
```python
housing = scad.var("housing", 100.0, unit="mm", tolerance=0.15)
bearing = scad.var(
"bearing", 2.0, unit="cm", tolerance=(-0.04, 0.05), tolerance_unit="mm"
)
spacer = scad.var("spacer", 79.4, unit="mm", tolerance=0.05)
clearance = housing - bearing - spacer
result = scad.analyze_tolerance(clearance, method="worst_case")
print(result.nominal)
print(result.lower_bound, result.upper_bound)
print(result.lower_deviation, result.upper_deviation)
for contribution in result.contributions:
print(contribution.variable_name, contribution.lower_deviation, contribution.upper_deviation)
```
`ToleranceAnalysis` contains absolute bounds, deviations from nominal, inferred
`dimension`, canonical `unit`, and one contribution record per source variable.
Each contribution reports its nominal and source tolerance in canonical units.
The unit system validates the expression before propagation. This permits
physically meaningful nonlinear chains such as:
```python
width = scad.var("width", 30.0, unit="mm", tolerance=0.1)
height = scad.var("height", 40.0, unit="mm", tolerance=0.2)
diagonal = scad.sqrt(width**2 + height**2)
analysis = scad.analyze_tolerance(diagonal)
assert analysis.dimension == scad.LENGTH
assert analysis.unit == scad.MM
```
Area and volume expressions can be inferred and analyzed. Persisted manufacturing
requirements currently accept final Length and Angle results only.
## Propagation Methods
### Worst Case
`method="worst_case"` is the default and the safety-oriented validation method.
- Affine chains preserve variable identity and combine coefficients exactly. Repeated use is not treated as an independent source, so `x - x` has zero propagated tolerance.
- Nonlinear chains use conservative interval propagation.
- Multiplication and division consider all endpoint sign combinations.
- Integer, negative, fractional, and varying powers validate their mathematical domains.
- `sin` and `cos` include interior extrema; `tan` rejects intervals crossing a discontinuity.
- `sqrt`, `asin`, and `acos` validate the entire input interval.
- Division rejects denominator intervals containing zero.
- `atan2` rejects tolerance regions containing its undefined origin.
Conservative interval propagation can intentionally overestimate a strongly correlated nonlinear expression. It must not underestimate a safety bound.
### RSS
`method="rss"` uses first-order analytic sensitivities and root-sum-square combination:
```python
result = scad.analyze_tolerance(clearance, method="rss")
```
Distinct variables are assumed independent. Repeated occurrences of the same variable are merged before RSS, so `x - x` still has zero sensitivity and `x + x` has twice the sensitivity of `x`.
RSS validates the full declared tolerance interval before calculating the first-order estimate. A nominal point cannot hide a division singularity, trigonometric discontinuity, or invalid function domain elsewhere in the source range.
Covariance matrices, correlation groups, probability distributions, and Monte Carlo analysis are not represented by the current API. Use `worst_case` when the independence assumption is unavailable or when a guaranteed envelope is required.
## Check One Requirement
`check_tolerance()` returns a result without raising when the derived tolerance exceeds the permitted deviations:
```python
check = scad.check_tolerance(
clearance,
tolerance=(-0.25, 0.24),
method="worst_case",
name="axial_clearance",
tolerance_unit="mm",
)
print(check.passed)
print(check.lower_margin, check.upper_margin)
```
A non-negative lower and upper margin means the requirement passes, subject to a
small floating-point comparison epsilon. Requirement deviations are converted
from `tolerance_unit` to the target's canonical unit before comparison.
## Session Requirements And Automatic Validation
Use `GraphSession.require_tolerance()` for design requirements that must travel with the model:
```python
with scad.GraphSession() as session:
body = scad.make_box_rsolid(housing, 10.0, 10.0)
session.require_tolerance(
clearance,
(-0.25, 0.24),
method="worst_case",
name="axial_clearance",
requirement_id="req.axial_clearance",
tolerance_unit="mm",
)
report = session.validate_tolerances(raise_on_failure=True)
model_json = scad.export_model_json(session)
```
Automatic validation occurs when:
1. `validate_tolerances(raise_on_failure=True)` is called.
2. A session or model JSON payload is exported.
3. A model JSON payload is imported or replayed.
4. A model is translated to FreeCAD through the model importer.
A failed requirement raises `ToleranceValidationError` at the tolerance layer. Model import, export, and replay expose it through the existing structured `SimpleCADError` harness where applicable.
Declaring a requirement validates that the chain is complete and mathematically defined, but it still records a failing requirement so callers can inspect its margins. Export and replay are the enforcement boundaries.
## Serialization
Variable tolerances are stored on variable nodes in `expression_graph`:
```json
{
"expr_id": "var_width",
"kind": "var",
"name": "width",
"default": 1.0,
"unit": "in",
"tolerance": {
"lower_deviation": -0.1,
"upper_deviation": 0.1
},
"tolerance_unit": "mm"
}
```
Design requirements and their latest validation evidence are stored in the top-level `tolerance_graph`:
```json
{
"requirements": [
{
"requirement_id": "req.axial_clearance",
"target_expr_id": "expr_clearance",
"tolerance": {
"lower_deviation": -0.25,
"upper_deviation": 0.24
},
"method": "worst_case",
"name": "axial_clearance",
"tolerance_unit": "mm",
"target_dimension": {
"length": 1,
"angle": 0
}
}
],
"validation": {
"passed": true,
"checks": []
}
}
```
Validation evidence is recomputed during import; serialized evidence is not
trusted as an authority. The target expression dimension is inferred again and
must match `target_dimension`; `tolerance_unit` must have the same dimension.
Payloads created before units or `tolerance_graph` existed remain valid as legacy
unitless expressions and import with an empty tolerance graph when it is absent.
Nominal geometry replay still uses the numeric snapshots in operation-node `params`. Tolerance validation does not sample or regenerate worst-case geometry.
## FreeCAD Translation
FreeCAD translation keeps the full tolerance graph as document metadata. The
`SimpleCADExpressions` spreadsheet stores lower/upper deviations in columns E/F,
nominal unit in G, tolerance unit in H, and inferred dimension in I. Spreadsheet
values and formulas use canonical CAD values so inch/radian declarations remain
consistent with operation-node snapshots. The translator preserves tolerance
intent but does not convert it into FreeCAD geometric-tolerance objects or
statistical solvers.
## Failure Conditions
Tolerance analysis fails explicitly for:
- a source variable without a declared tolerance
- non-finite nominal values or deviations
- malformed signed lower/upper deviations
- unknown, malformed, non-finite, incompatible, overflowing, or underflowing units
- addition/subtraction or `atan2` with incompatible dimensions
- invalid powers, square roots, or trigonometric dimensions
- mixing unit-declared and legacy variables in one expression
- a requirement unit or persisted target dimension that disagrees with the target
- an Area, Volume, Dimensionless, or compound-dimension requirement target
- duplicate requirement IDs
- unknown or dangling expression references
- malformed, cyclic, duplicate-ID, or unsupported expression nodes
- an undefined expression anywhere in the declared tolerance interval
- an RSS derivative at a non-differentiable nominal point
- an unsupported propagation method
See [Physical Units And Dimension Inference](physical-units.md) for the complete
unit registry, dimension algebra, custom-unit payload, and legacy behavior.
@@ -117,7 +117,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
```json
{
"schema_version": "2.0",
"producer_version": "2.0.1b1",
"producer_version": "2.0.2",
"capabilities": {
"selection_ref_strategies": true,
"geo_select_nodes": true,
@@ -128,7 +128,8 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
"topology_delta_summary": false,
"assembly_graph": false,
"scalar_field_graph": false,
"expression_graph": true
"expression_graph": true,
"dimension_tolerances": true
},
"graph_id": "graph_xxxxxxxx",
"nodes": [...],
@@ -163,6 +164,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
| `assembly_graph` | `bool` | 当前 graph JSON 本身不承载 assembly graph |
| `scalar_field_graph` | `bool` | 当前为 `false`SDF / scalar field graph 暂时不在支持范围内 |
| `expression_graph` | `bool` | session/model payload 支持 expression graph |
| `dimension_tolerances` | `bool` | session/model payload supports variable tolerances and a tolerance requirement graph |
## 5. Operation Node Schema
@@ -334,7 +336,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
- `make_*_face``make_face_from_wire` -> `Sketch`
- `make_*_edge` / `make_*_wire` -> `Profile`
- `make_extrude_rsolid` / `make_revolve_rsolid` / `make_loft_rsolid` / `make_sweep_rsolid` / `make_fillet_rsolid` / `make_chamfer_rsolid` / `make_shell_rsolid` / `make_cut_rsolid` / `make_union_rsolid` / `make_intersect_rsolid` -> `Feature`
- `make_extrude_rsolid` / `make_revolve_rsolid` / `make_loft_rsolid` / `make_sweep_rsolid` / `make_twisted_sweep_rsolid` / `make_fillet_rsolid` / `make_chamfer_rsolid` / `make_shell_rsolid` / `make_cut_rsolid` / `make_union_rsolid` / `make_intersect_rsolid` -> `Feature`
## 7. Topology Delta Schema
@@ -667,7 +669,13 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
"expr_id": "var_119b16e4",
"kind": "var",
"name": "r",
"default": 2.0
"default": 2.0,
"unit": "mm",
"tolerance": {
"lower_deviation": -0.1,
"upper_deviation": 0.2
},
"tolerance_unit": "mm"
}
]
}
@@ -692,7 +700,13 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
"expr_id": "var_xxx",
"kind": "var",
"name": "radius",
"default": 2.0
"default": 2.0,
"unit": "mm",
"tolerance": {
"lower_deviation": -0.05,
"upper_deviation": 0.1
},
"tolerance_unit": "mm"
}
```
@@ -722,6 +736,82 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
- `cos`
- `tan`
- `sqrt`
- `acos`
- `asin`
- `atan`
- `atan2`
### 10.3 Unit And Dimension Semantics
Variable nodes may include:
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `unit` | `string | unit object` | no | nominal declaration unit |
| `tolerance` | `object` | no | signed deviations in `tolerance_unit` |
| `tolerance_unit` | `string | unit object` | no | source tolerance unit; defaults to `unit` in Python declarations |
Built-in units serialize as symbols such as `mm`, `in`, `deg`, or `rad`. Custom
units serialize in full:
```json
{
"symbol": "thou",
"dimension": {"length": 1, "angle": 0},
"scale_to_canonical": 0.0254
}
```
Dimensions contain required integer `length` and `angle` exponents. Canonical
numeric values used by operation-node `params` and tolerance analysis are `mm`,
`mm^2`, `mm^3`, `deg`, or `1` for the named dimensions.
Import rebuilds the complete DAG and reruns dimension inference. Addition and
subtraction require matching dimensions; multiplication/division combine
exponents; dimensioned powers require supported constant exponents; square root
requires even exponents; and trigonometric operations enforce Angle/Dimensionless
inputs. A graph cannot mix unit-declared variables with legacy variables lacking
units. Unitless legacy graphs remain accepted.
### 10.4 Dimension Tolerance Graph
Variable `tolerance` values are signed deviations from `default`. A scalar source dimension must use `lower_deviation <= 0 <= upper_deviation`.
Session/model payloads may include a sibling `tolerance_graph`:
```json
{
"requirements": [
{
"requirement_id": "req.clearance",
"target_expr_id": "expr_clearance",
"tolerance": {
"lower_deviation": -0.2,
"upper_deviation": 0.3
},
"method": "worst_case",
"name": "clearance",
"tolerance_unit": "mm",
"target_dimension": {
"length": 1,
"angle": 0
}
}
],
"validation": {
"passed": true,
"checks": []
}
}
```
Supported methods are `worst_case` and `rss`. Unit-aware requirements must target
Length or Angle. `tolerance_unit` must match the inferred target dimension and is
converted to the canonical unit before comparison. Importers recompute validation
from `expression_graph`, compare the inferred result to `target_dimension`, and do
not trust serialized `validation` evidence. Missing `tolerance_graph` is treated as
an empty graph for backward compatibility. Legacy requirements may omit both unit
fields when their target expression is unitless.
## 11. Frame Graph Schema
@@ -765,6 +855,7 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
"canonical_contract": {...},
"graph": {...},
"expression_graph": {...},
"tolerance_graph": {...},
"frame_graph": {...},
"geometry_registry": [...],
"semantic_entity_registry": [...],
@@ -784,6 +875,7 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
| `graph` | `graph object` | yes | canonical low-level graph and only source of truth |
| `leaf_ids` | `array<string>` | yes | explicit result set for multi-output graph replay/export |
| `expression_graph` | `object` | yes | expression DAG |
| `tolerance_graph` | `object` | no | dimension-chain requirements and validation evidence; defaults to an empty graph |
| `frame_graph` | `object` | yes | frame snapshots |
| `geometry_registry` | `array<object>` | yes | output geometry registry |
| `semantic_entity_registry` | `array<object>` | yes | semantic entity registry |
@@ -936,6 +1028,7 @@ New canonical profile nodes use the `make_*_r*` names listed in `canonical_contr
- `make_revolve_rsolid`
- `make_loft_rsolid`
- `make_sweep_rsolid`
- `make_twisted_sweep_rsolid`
- `make_translate_rshape`
- `make_rotate_rshape`
- `make_mirror_rshape`
@@ -1296,6 +1389,23 @@ Important:
| --- | --- | --- |
| `is_frenet` | `bool` | sweep orientation mode |
#### `make_twisted_sweep_rsolid`
- Inputs: 1 profile `Face`
- Outputs: 1 `Solid`
- Params:
| Key | Type | Meaning |
| --- | --- | --- |
| `axis` | 3-number array | sweep and rotation-axis direction in caller coordinates |
| `origin` | 3-number array | sweep start and a point on the rotation axis |
| `distance` | positive number | axial sweep distance |
| `twist_angle` | number | signed total rotation in degrees |
| `guide_radius` | positive number | internal auxiliary-spine radius |
Replay rebuilds the auxiliary spine deterministically inside the operation. It
does not infer section counts or lower to transient loft nodes.
### 14.6 Boolean Ops
#### `make_union_rsolid`
+202
View File
@@ -0,0 +1,202 @@
# Physical Units And Dimension Inference
SimpleCADAPI attaches physical meaning at `Var` declarations and infers the
dimension of every derived scalar expression. This catches invalid formulas before
they reach geometry, tolerance analysis, model export, replay, or FreeCAD
translation.
## Canonical CAD Units
Declaration values are preserved on each `Var`, but evaluation converts them to a
single CAD coordinate system:
| Dimension | Canonical unit |
| --- | --- |
| Dimensionless | `1` |
| Length | `mm` |
| Area | `mm^2` |
| Volume | `mm^3` |
| Angle | `deg` |
Degrees are canonical because existing SimpleCAD rotation and angular APIs use
degrees. Trigonometric evaluation converts to radians internally and converts
inverse-trigonometric results back to degrees.
```python
import math
import simplecadapi as scad
width = scad.var("width", 1.0, unit="in")
angle = scad.var("angle", math.pi / 2, unit="rad")
assert width.default == 1.0
assert width.evaluate() == 25.4
assert angle.evaluate() == 90.0
assert math.isclose(scad.sin(angle).evaluate(), 1.0)
```
Bindings use the variable's declaration unit. `width.evaluate({"width": 2.0})`
therefore returns `50.8` millimeters for an inch-declared variable.
## Declaring Units And Tolerances
```python
width = scad.var(
"width",
1.0,
unit="in",
tolerance=(-0.1, 0.2),
tolerance_unit="mm",
)
```
- `default` is in `unit`.
- `tolerance` is in `tolerance_unit`.
- `tolerance_unit` defaults to `unit` when a tolerance is present.
- Nominal and tolerance units may differ, but their dimensions must match.
- A `tolerance_unit` requires both `unit` and `tolerance`.
- Values must be finite and representable after canonical conversion.
`width.default` and `width.tolerance` preserve declaration-space values.
`width.canonical_default` and `width.canonical_tolerance` expose values used by
geometry and tolerance propagation.
## Built-In Units
| Dimension | Symbols |
| --- | --- |
| Dimensionless | `1`, `%` |
| Length | `mm`, `cm`, `m`, `in`, `ft` |
| Area | `mm^2`, `cm^2`, `m^2`, `in^2`, `ft^2` |
| Volume | `mm^3`, `cm^3`, `m^3`, `in^3`, `ft^3` |
| Angle | `deg`, `rad` |
Common singular/plural names are accepted by `get_unit()`, including
`millimeters`, `inches`, `feet`, `degrees`, `radians`, `square feet`, and
`cubic inches`. `ml` aliases `cm^3`.
Use constants such as `MM`, `INCH`, `DEGREE`, `RADIAN`, `LENGTH`, and `ANGLE`, or
resolve strings:
```python
assert scad.get_unit("inch") == scad.INCH
assert scad.convert_value(1.0, "in", "mm") == 25.4
assert math.isclose(scad.convert_value(180.0, "deg", "rad"), math.pi)
```
Incompatible conversion raises `UnitValidationError`.
## Dimension Algebra
`Dimension` stores integer exponents for length and angle. Named dimensions are:
- `DIMENSIONLESS = Dimension()`
- `LENGTH = Dimension(length=1)`
- `AREA = Dimension(length=2)`
- `VOLUME = Dimension(length=3)`
- `ANGLE = Dimension(angle=1)`
`infer_dimension(expression)` applies these rules:
| Operation | Rule |
| --- | --- |
| `a + b`, `a - b` | dimensions must match |
| `a * b` | add dimension exponents |
| `a / b` | subtract dimension exponents |
| `a ** n` | multiply exponents by constant integer `n` |
| `sqrt(a)` or `a ** 0.5` | every exponent must be even |
| unary `-a`, `abs(a)` | preserve dimension |
| `sin`, `cos`, `tan` | input must be Angle; result is Dimensionless |
| `asin`, `acos`, `atan` | input must be Dimensionless; result is Angle |
| `atan2(y, x)` | inputs must have the same dimension; result is Angle |
Arbitrary and varying powers are permitted for dimensionless bases. A dimensioned
base requires a constant integer exponent, except `0.5` is accepted when all base
exponents are even.
```python
width = scad.var("width", 30.0, unit="mm")
height = scad.var("height", 40.0, unit="mm")
area = width * height
diagonal = scad.sqrt(width**2 + height**2)
assert scad.infer_dimension(area) == scad.AREA
assert scad.infer_dimension(diagonal) == scad.LENGTH
assert diagonal.evaluate() == 50.0
```
## Numeric Constants
Numeric literals are dimensionless coefficients in multiplication and division.
For addition and subtraction, a literal adopts the other operand's dimension as a
contextual offset:
```python
length = scad.var("length", 10.0, unit="mm")
assert scad.infer_dimension(length * 2.0) == scad.LENGTH
assert scad.infer_dimension(length + 2.0) == scad.LENGTH
```
The literal is already expressed in the canonical result unit. `length + 2.0`
therefore means two millimeters, not two units of `length.unit`. Prefer explicit
variables when declaration-unit intent must be retained.
## Legacy Unitless Expressions
Variables without `unit` retain the previous behavior:
- `infer_dimension()` returns `None`.
- Trigonometric inputs and results use radians.
- Existing arbitrary expression and tolerance behavior remains available.
- A legacy variable cannot be mixed with a unit-declared variable in one
expression because no safe physical meaning can be inferred.
Pure numeric constant expressions infer `Dimensionless`.
## Custom Units
Custom linear-scale units use the same canonical system:
```python
thou = scad.Unit("thou", scad.LENGTH, 0.0254)
width = scad.var("width", 1000.0, unit=thou)
assert width.evaluate() == 25.4
```
Built-in units serialize as symbols. Custom units serialize a definition:
```json
{
"symbol": "thou",
"dimension": {"length": 1, "angle": 0},
"scale_to_canonical": 0.0254
}
```
Units are scale-only. Offset units such as Celsius/Fahrenheit are not represented.
## Validation Boundaries
Unit and dimension validation runs when:
1. A `Var`, `Dimension`, or `Unit` is created.
2. An expression is directly evaluated.
3. An expression is registered or imported through `ExpressionGraph`.
4. A tolerance is analyzed or a requirement is declared.
5. Session/model JSON is imported, exported, replayed, or translated.
Malformed units, cyclic graphs, duplicate expression IDs, mixed legacy/typed
variables, incompatible dimensions, invalid roots, and invalid trigonometric inputs
are rejected before graph mutation or geometry replay.
## Manufacturing Requirement Scope
Area, volume, inverse length, and other compound dimensions can be inferred and
analyzed. Manufacturing requirements created by `check_tolerance()` or
`GraphSession.require_tolerance()` currently accept final `Length` and `Angle`
results only. This prevents an area or volume variation from being presented as a
linear dimension requirement without explicit engineering semantics.
See [Dimension Tolerance Chains](dimension-tolerance-chains.md) for propagation,
RSS assumptions, enforcement boundaries, and serialized requirement fields.
+30 -12
View File
@@ -10,14 +10,19 @@ The long-form schema reference remains [`../operation_graph_json_spec.md`](../op
import json
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(10, 6, 2)
hole = scad.make_cylinder_rsolid(1, 4, bottom_face_center=(0, 0, -1))
@scad.model(graph_id="drilled_block")
def build_model():
body = scad.make_box_rsolid(width=10, height=6, depth=2)
hole = scad.make_cylinder_rsolid(
radius=1, height=4, bottom_face_center=(0, 0, -1)
)
result = scad.cut_rsolid(body, hole)
scad.capture_result(value=result)
return result
model_json = scad.export_model_json(session)
payload = json.loads(model_json)
rebuilt = scad.replay_model_json(model_json)
model = build_model()
payload = json.loads(model.model_json)
rebuilt = model.replay()
```
Inspect these fields:
@@ -29,6 +34,17 @@ Inspect these fields:
- `node["inputs"]`: upstream node ids used by replay.
- `payload["leaf_ids"]`: explicit final result node ids.
- `payload["expression_graph"]`: expression DAG used by expression-backed parameters.
- `payload["tolerance_graph"]`: dimension-chain requirements and validation evidence.
For new top-level models, `ModelResult.model_json` is the preferred artifact
accessor. Use `@scad.requires_session` for reusable builders and
`scad.capture_result(...)` when the final output should not be inferred from
all graph leaves. If a model invocation also needs durable CAD/viewer files,
pass `export_dir=...` to `@scad.model`; its captured geometry/product values
then produce one self-contained `<graph_id>.scene.zip`. It embeds
`model/model.json`, mapped project-relative Python sources, and the evaluated
render/selection assets. It does not create adjacent model/session JSON, STEP,
STL, or FCStd files. No files are written when `export_dir` is omitted.
## Important rule: source API is not always graph API
@@ -52,11 +68,13 @@ Many user-facing functions are convenience APIs. During an active `GraphSession`
- [Primitive and profile operations](primitives-and-profiles.md)
- [Features, booleans, transforms, patterns, and selectors](features-booleans-transforms.md)
- [Expressions and replay behavior](expressions-and-replay.md)
- [Physical units and dimension inference](../physical-units.md)
- [Dimension tolerance chains](../dimension-tolerance-chains.md)
## Example
## Examples
See [`../../../examples/07_serialization_operation_tree.py`](../../../examples/07_serialization_operation_tree.py). It intentionally exercises every canonical core operation and writes:
- `examples/out/serialization_operation_tree.model.json`
- `examples/out/serialization_operation_tree.summary.md`
- `examples/out/serialization_operation_tree.step`
The retained examples use the same model/session contract. See
[`../../../examples/08_constrained_sketch.py`](../../../examples/08_constrained_sketch.py)
for sketch promotion and replay, and
[`../../../examples/10_part_assembly.py`](../../../examples/10_part_assembly.py)
for product hierarchy and automatic artifact export.
@@ -15,14 +15,15 @@ This lets consumers choose between:
```python
import simplecadapi as scad
width = scad.var("width", 24.0, comment="plate width")
height = scad.var("height", 12.0, comment="plate height")
thickness = scad.var("thickness", 4.0, comment="plate thickness")
width = scad.var("width", 24.0, unit="mm", comment="plate width", tolerance=0.1)
height = scad.var("height", 12.0, unit="mm", comment="plate height", tolerance=0.1)
thickness = scad.var("thickness", 4.0, unit="mm", comment="plate thickness", tolerance=(-0.05, 0.1))
with scad.GraphSession() as session:
plate = scad.make_box_rsolid(width, height, thickness)
rib = scad.make_box_rsolid(width / 4.0, height, thickness * 2.0)
part = scad.union_rsolid(plate, rib)
session.require_tolerance(width + height, 0.2, tolerance_unit="mm", name="plate_envelope")
model_json = scad.export_model_json(session)
```
@@ -48,7 +49,9 @@ A node with expression-backed params may look like:
}
```
`params.distance` is the evaluated snapshot. `param_exprs.distance` says the value came from expression node `var_thickness`.
`params.distance` is the evaluated canonical snapshot. Unit-aware lengths are
stored in millimeters and angles in degrees. `param_exprs.distance` says the value
came from expression node `var_thickness` and preserves its declaration metadata.
For tuple/list params, `param_exprs` mirrors the shape of the parameter and uses `null` where no expression is present:
@@ -78,12 +81,24 @@ Consumers that want parameterization should:
Consumers that only want geometry can ignore `param_exprs` and `expression_graph`.
Variable nodes may contain `unit`, `tolerance`, and `tolerance_unit`. Registered
units use string symbols; custom units use `{symbol, dimension,
scale_to_canonical}` objects. Import reconstructs the expression graph and reruns
dimension inference rather than trusting external dimension claims.
Session/model payloads store derived-dimension requirements in `tolerance_graph`.
See [Physical Units](../physical-units.md) and [Dimension Tolerance
Chains](../dimension-tolerance-chains.md) for inference, propagation, and
validation semantics.
## Replay policy in current implementation
`replay_model_json(model_json)` currently uses the canonical low-level `graph` and the numeric values in `node.params`.
That means replay is deterministic with respect to the exported snapshot. It does not currently re-solve expressions with changed variable values.
Replay does validate stored tolerance requirements before rebuilding the nominal geometry. A failing tolerance chain blocks replay, but passing bounds do not cause replay to sample or regenerate limit geometry.
In practical terms:
```python
@@ -122,6 +122,40 @@ Replay effect:
2. Replay path wire from input 1.
3. Call `sweep_rsolid(profile, path, is_frenet=...)`.
## Twisted Sweep
Source:
```python
profile = scad.make_rectangle_rface(width=2.0, height=1.0)
solid = scad.twisted_sweep_rsolid(
profile=profile,
distance=8.0,
twist_angle=30.0,
)
```
Serialized node:
```json
{
"op": "make_twisted_sweep_rsolid",
"params": {
"axis": [0.0, 0.0, 1.0],
"origin": [0.0, 0.0, 0.0],
"distance": 8.0,
"twist_angle": 30.0,
"guide_radius": 1.0
},
"inputs": ["node_for_profile_face"],
"output_count": 1
}
```
Replay reconstructs the continuous auxiliary-spine rotation law from the
recorded parameters and invokes `twisted_sweep_rsolid(...)`. No sampled loft
sections are stored or inferred.
## Helical sweep macro lowering
Source:
+34 -12
View File
@@ -2,21 +2,24 @@
## Overview
`TaggedMixin` is the internal tag and metadata storage mixin used by `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`. It owns the shared `_tags`, `_metadata`, and `_runtime` stores for topology wrappers.
`TaggedMixin` is the internal semantic binding and metadata mixin used by topology wrappers. Canonical tag ownership is source-preserving `TagBinding` data. `_tags` is only an effective-scope compatibility cache; user code must not treat it as writable truth.
User code should not call member tag mutators. The public tag API is functional:
- `apply_tag(shape, tag)` attaches one normalized tag.
- `list_tags(shape)` returns tags in deterministic sorted order.
- `apply_tag_rselection(scope, targets, tag, ...)` returns an independent semantic view and exposes explicit propagation policies.
- `list_tags(shape, scope=...)` returns tags in deterministic sorted order.
- `explain_tag(shape, tag, scope=...)` preserves binding and producer evidence.
- `select_faces_by_tag(...)`, `select_edges_by_tag(...)`, and QL predicates such as `ql.tag("role.*")` provide selection/query helpers.
## Tagging Mental Model
- Tags are normalized lowercase dot-separated semantic tokens.
- Examples: `role.mounting_surface`, `anchor.datum.primary`, `group.fasteners`, `face.top`, `edge.boundary`, `wire.outer`, `solid.boolean.cut`.
- `apply_tag(shape, tag)` does not expose propagation controls.
- The standard policy propagates `role.*`, `anchor.*`, `group.*`, and a few legacy bare semantic tags downward.
- Topology-specific tags such as `face.*`, `edge.*`, `wire.*`, `vertex.*`, and `solid.*` stay local.
- New user assignments default to local topology propagation regardless of prefix.
- Downward inheritance is explicit and computed dynamically; bindings are not copied into every child.
- `effective` means local plus inherited and does not include lineage.
- `lineage` requires complete topology history and only follows derivations allowed by the binding policy.
- Numeric dimensions, measurements, and rich descriptive payloads belong in metadata, not tags.
- Geometry builders store structured geometry facts under `metadata["geo"]`.
@@ -34,16 +37,29 @@ top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(fa
print(len(top_faces))
```
## Propagation Example
## Explicit Propagation Example
```python
import simplecadapi as scad
body = scad.make_box_rsolid(10, 10, 2)
scad.apply_tag(body, "role.mounting_plate")
body = scad.make_box_rsolid(width=10, height=10, depth=2)
tagged = scad.apply_tag_rselection(
scope=body,
targets=[body],
tag="role.mounting_plate",
topology_propagation=scad.TopologyPropagation.DOWNWARD,
)
face_hits = [face for face in body.get_faces() if "role.mounting_plate" in scad.list_tags(face)]
edge_hits = [edge for edge in body.get_edges() if "role.mounting_plate" in scad.list_tags(edge)]
face_hits = scad.select_faces_by_tag(
solid=tagged,
tag="role.mounting_plate",
scope=scad.TagScope.INHERITED,
)
edge_hits = scad.select_edges_by_tag(
shape=tagged,
tag="role.mounting_plate",
scope=scad.TagScope.INHERITED,
)
print(len(face_hits), len(edge_hits))
```
@@ -55,7 +71,13 @@ Primitives and modeling operations may attach normalized tags automatically:
- Primitive tags such as `geom.primitive.box`, `geom.primitive.cylinder`, and `geom.primitive.sphere`.
- Face tags from `auto_tag_faces(...)`, such as `face.top`, `face.bottom`, `face.side`, and `face.surface`.
- Wire tags such as `wire.outer` and `wire.inner`.
- Operation/tracking tags such as `solid.boolean.cut`, `op.cut.modified`, or `op.extrude.generated`.
- Operation-level categorical tags such as `solid.boolean.cut` may remain local annotations.
Operation events and source roles are not tags. Proven `preserved`, `modified`,
or `generated` events and `body`/`tool` origins live in typed
`metadata["track"]`. Query them with `ql.operation_event(...)` and
`ql.origin_role(...)`. Missing correspondence remains `coverage="partial"` and
`status="unknown"`; it is never promoted to `generated` by default.
## Metadata Methods
@@ -84,7 +106,7 @@ scad.apply_tag(body, "role.mounting_plate")
body.auto_tag_faces("box")
top_faces = Q.select(body.get_faces()).where(Q.tag("face.top")).all()
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*")).all()
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*", scope="effective")).all()
print(len(top_faces), len(role_faces))
```
+11
View File
@@ -0,0 +1,11 @@
# Engineering Guides
- [STEP BREP 逆向工程方法](step-brep-reverse-engineering.md):从 STEP 检查、解析几何指纹、特征树推断、候选迭代,到几何点集、BREP 拓扑、参数历史和模型回放的完整工作流。
对应的可运行模块和案例:
```text
src/simplecadapi/inverse_engineer/brep/
examples/out/xzby_reverse/README.md
examples/out/xzby_reverse/reverse_model.py
```
@@ -0,0 +1,49 @@
# STEP BREP 逆向工程方法
该方法论是 SimpleCAD skill 的任务专用参考,规范版本位于:
```text
skills/simplecadapi/references/inverse_engineer/brep-reverse-engineering.md
```
正式检查工具位于:
```text
src/simplecadapi/inverse_engineer/brep/
```
Python API
```python
from simplecadapi.inverse_engineer import brep
report = brep.inspect_step(path="part.step")
comparison = brep.compare_steps(target_path="target.step", candidate_path="candidate.step")
brep.render_step_views(step_path="candidate.step", output_path="candidate-views.png")
brep.compare_step_slices(
target_path="target.step",
candidate_path="candidate.step",
output_path="slice-overlay.png",
)
```
渲染和图片截面功能需要可选依赖:
```bash
uv sync --extra inverse-engineer
```
CLI
```bash
uv run simplecad-brep inspect part.step -o part-report.json
uv run simplecad-brep compare target.step candidate.step -o comparison.json
uv run simplecad-brep render candidate.step candidate-views.png
uv run simplecad-brep slices target.step candidate.step slice-overlay.png
```
具体逆向案例见:
```text
examples/out/xzby_reverse/README.md
```
+15 -2
View File
@@ -4,12 +4,12 @@ This index includes generated docs for standard part factory functions. Use thes
## Import Surfaces
- Recommended package-level module export: `import simplecadapi as scad`, then call functions through submodules such as `scad.std.gear.<function>(...)` and `scad.std.bearing.<function>(...)`.
- Recommended package-level module export: `import simplecadapi as scad`, then call functions through submodules such as `scad.std.gear.<function>(...)`, `scad.std.fastener.<function>(...)`, and `scad.std.bearing.<function>(...)`.
- Direct submodule import is also supported, for example `from simplecadapi.std.gear import make_spur_gear_rsolid` or `from simplecadapi.std.bearing import make_ball_bearing_rassembly`.
## Usage Guidance
- Prefer standard-library factories for standard bearings, gears, ring gears, and racks before hand-modeling profiles with core geometry APIs.
- Prefer standard-library factories for standard bearings, fasteners, spur gears, straight bevel gears, ring gears, and racks before hand-modeling profiles with core geometry APIs.
- Standard parts return normal SimpleCAD shapes or product assemblies, so they can be transformed, tagged, assembled, exported, or combined with core geometry operations.
- Switch to core geometry APIs only when the requested standard part needs substantial custom geometry beyond the factory parameters.
@@ -23,6 +23,19 @@ This index includes generated docs for standard part factory functions. Use thes
- [make_herringbone_gear_rsolid](make_herringbone_gear_rsolid.md) *(from std/gear.py)* `stdlib`
- [make_spur_gear_rsolid](make_spur_gear_rsolid.md) *(from std/gear.py)* `stdlib`
## Bevel Gears
- [make_straight_bevel_gear_rsolid](make_straight_bevel_gear_rsolid.md) *(from std/gear.py)* `stdlib`
## Roller Chain
- [make_roller_chain_sprocket_rsolid](make_roller_chain_sprocket_rsolid.md) *(from std/chain.py)* `stdlib`
## Fasteners
- [make_bolt_rsolid](make_bolt_rsolid.md) *(from std/fastener.py)* `stdlib`
- [make_nut_rsolid](make_nut_rsolid.md) *(from std/fastener.py)* `stdlib`
## Internal Ring Gears
- [make_helical_ring_gear_rsolid](make_helical_ring_gear_rsolid.md) *(from std/gear.py)* `stdlib`
@@ -0,0 +1,41 @@
# make_bolt_rsolid
## API Definition
```python
def make_bolt_rsolid(
diameter: float,
length: float,
head_style: str = "hex",
thread_style: str = "auto",
thread_detail: str = "modeled",
thread_form: str = "v",
thread_pitch: Optional[float] = None,
thread_depth: Optional[float] = None,
thread_length: Optional[float] = None,
head_width: Optional[float] = None,
head_height: Optional[float] = None,
drive_style: str = "none",
drive_size: Optional[float] = None,
drive_depth: Optional[float] = None,
underhead_fillet_radius: Optional[float] = None,
) -> Solid
```
*Source: std/fastener.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.fastener.make_bolt_rsolid(...)`; direct submodule import: `from simplecadapi.std.fastener import make_bolt_rsolid`
## Description
Create a parameterized bolt along `+Z`, with the head underside on `Z=0`. Head styles are `hex`, `square`, `cylindrical`, `button`, and `countersunk`. Drive styles are `none`, `slot`, `cross`, and `hex_socket`.
Thread styles are `auto`, `full`, `partial`, and `none`. `auto` selects full thread for `length <= 3 * diameter`; otherwise it uses the ISO-style piecewise partial-thread length. The default `thread_detail="modeled"` creates a visible replayable helical `v` or `trapezoidal` thread. Set `thread_detail="cosmetic"` explicitly for a smooth shank with thread intent only in metadata. For partial threads, `thread_length` is measured back from the tip at `Z=length`.
For metric coarse-series diameters, the factory derives the default pitch from the standard series and records `d2 = d - 0.6495P` and `d1 = d - 1.0825P` in metadata. Hex-head defaults use catalog-like `S` and `k` values where available. The default underhead fillet is `0.06d`; `underhead_fillet_radius` can override it. The metadata also reports a minimum recommended mating-hole chamfer equal to the fillet radius.
Modeled threads may rotate the helical seam to an equivalent kernel-stable phase. The selected phase is recorded as `thread_phase_degrees` in `std.fastener.bolt` metadata and does not change thread dimensions or handedness.
Default dimensions are useful parametric proportions, not a claim of compliance with a specific ISO, DIN, ASME, or supplier catalog. Set catalog dimensions explicitly for released hardware.
@@ -16,10 +16,9 @@ def make_helical_gear_rsolid(n_teeth: int, module: float, pressure_angle: float
Create an involute helical gear.
Non-zero helix angles are modeled as small-step ruled lofts through rotated
copies of one profile. The small angular step keeps closed-wire section
correspondence stable while ruled faces avoid smooth loft bulging in STEP
exports.
Non-zero helix angles use a continuous twisted sweep along the gear axis. An
auxiliary-spine rotation law preserves the profile while generating one
continuous side face per profile edge instead of one face per loft interval.
## Parameters
@@ -16,10 +16,8 @@ def make_herringbone_gear_rsolid(n_teeth: int, module: float, pressure_angle: fl
Create an involute herringbone (double-helical) gear.
Each half is modeled as a small-step ruled loft through rotated copies of
one profile, with a shared center section forming the herringbone ridge.
This keeps closed-wire section correspondence stable while avoiding smooth
loft bulging in STEP exports.
Each half uses a continuous twisted sweep with opposite handedness. The two
halves share the rotated center profile and are fused into one solid.
## Parameters
@@ -0,0 +1,35 @@
# make_nut_rsolid
## API Definition
```python
def make_nut_rsolid(
diameter: float,
width: float,
height: float,
nut_style: str = "hex",
hole_style: str = "through",
thread_detail: str = "modeled",
thread_form: str = "v",
thread_pitch: Optional[float] = None,
thread_depth: Optional[float] = None,
hole_depth: Optional[float] = None,
knurl_count: int = 24,
) -> Solid
```
*Source: std/fastener.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.fastener.make_nut_rsolid(...)`; direct submodule import: `from simplecadapi.std.fastener import make_nut_rsolid`
## Description
Create a parameterized nut along `+Z`, with its bottom face on `Z=0`. Nut styles are `hex`, `square`, `round`, and `knurled`. Hole styles are `through` and `blind`; a blind hole opens from the top face and uses `hole_depth` as its axial depth.
The default `thread_detail="modeled"` creates replayable internal `v` or `trapezoidal` helical teeth. Set `thread_detail="cosmetic"` explicitly for a smooth major-diameter hole with thread intent only in metadata. For metric coarse-series diameters, the factory derives the default pitch and records `d2 = d - 0.6495P` and `d1 = d - 1.0825P` in metadata. `knurl_count` controls the lobe count of the printable knurled approximation.
Modeled threads may rotate the helical seam to an equivalent kernel-stable phase. The selected phase is recorded as `thread_phase_degrees` in `std.fastener.nut` metadata and does not change thread dimensions or handedness.
Default thread proportions are useful for parametric models, not a substitute for catalog tolerances, thread class, lead-in, prevailing-torque features, or manufacturing checks.
@@ -0,0 +1,19 @@
# make_roller_chain_sprocket_rsolid
## API Definition
```python
def make_roller_chain_sprocket_rsolid(n_teeth: int, chain_pitch: float, roller_diameter: float, sprocket_thickness: float, *, bore_radius: float = 0.0, roller_clearance: float = 0.15) -> Solid
```
*Source: std/chain.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.chain.make_roller_chain_sprocket_rsolid(...)`; direct submodule import: `from simplecadapi.std.chain import make_roller_chain_sprocket_rsolid`
## Description
Create a roller-chain sprocket from tooth count, chain pitch, roller diameter, and tooth-plate thickness. The pitch radius follows the regular pitch polygon. Circular roller seats are cut at every pitch point and opened through the engineering outside-diameter envelope.
The result preserves assembly-level engagement dimensions. Manufacturing release still requires the selected chain standard's permitted tooth-form range, tooth width, hub, material, heat treatment, runout, and supplier checks.
@@ -0,0 +1,30 @@
# make_straight_bevel_gear_rsolid
## API Definition
```python
def make_straight_bevel_gear_rsolid(n_teeth: int, module: float, pitch_angle: float = 45.0, pressure_angle: float = 20.0, face_width: float = 8.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25, backlash: float = 0.0) -> Solid
```
*Source: std/gear.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.gear.make_straight_bevel_gear_rsolid(...)`; direct submodule import: `from simplecadapi.std.gear import make_straight_bevel_gear_rsolid`
## Description
Create a straight bevel gear with standard metric tooth proportions. The large-end transverse section uses an analytic involute profile. A similar small-end section is placed on the pitch cone and connected with ruled straight tooth surfaces.
The returned solid contains nominal tooth geometry. Releasing a mating pair still requires mounting-distance, contact-pattern, backlash, material, heat-treatment, and strength checks.
## Parameters
- `n_teeth`: Number of teeth, at least 3.
- `module`: Large-end transverse module in millimetres.
- `pitch_angle`: Pitch-cone angle in degrees, greater than 0 and less than 90.
- `pressure_angle`: Transverse pressure angle in degrees.
- `face_width`: Tooth face width along the pitch-cone generator in millimetres; must be smaller than the outer pitch-cone distance.
- `addendum_factor`: Large-end tooth addendum divided by module.
- `clearance_factor`: Root clearance beyond the addendum divided by module.
- `backlash`: Large-end circumferential tooth-thickness reduction at the pitch circle in millimetres.
+5
View File
@@ -4,3 +4,8 @@ out/
*.json
*.FCStd
*.FCBak
*.fcstd
*.fcbak
*.scene.zip
*.zip
*.log
@@ -1,31 +0,0 @@
"""Basic shape-first modeling with the functional API.
Run from the repository root with:
uv run python examples/01_basic_modeling.py
"""
from pathlib import Path
import simplecadapi as scad
OUT = Path("examples/out")
OUT.mkdir(parents=True, exist_ok=True)
base = scad.make_box_rsolid(60.0, 36.0, 8.0, bottom_face_center=(0.0, 0.0, 0.0))
hole = scad.make_cylinder_rsolid(5.0, 14.0, bottom_face_center=(0.0, 0.0, -3.0))
slot = scad.make_box_rsolid(18.0, 8.0, 14.0, bottom_face_center=(14.0, 0.0, -3.0))
part = scad.cut_rsolid(base, hole, slot)
boss = scad.make_cylinder_rsolid(8.0, 7.0, bottom_face_center=(-18.0, 0.0, 8.0))
part = scad.union_rsolid(part, boss)
part.auto_tag_faces("box")
print("volume", round(part.get_volume(), 3))
print("faces", len(part.get_faces()))
print("edges", len(part.get_edges()))
scad.export_step(part, str(OUT / "basic_modeling.step"))
scad.export_stl(part, str(OUT / "basic_modeling.stl"))
print("wrote", OUT / "basic_modeling.step")

Some files were not shown because too many files have changed in this diff Show More