feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距
接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。 支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
@@ -10,96 +10,104 @@
|
||||
- 主入口是 `python main.py`;无参数启动会快速进入空场景,不默认读取 STEP。常用测试模型是 `assets/models/geom_extract.step`;立方体测试模型是 `assets/models/cube_10mm.step`。
|
||||
- 左侧操作面板当前优先保留最小建模链路:STEP 文件、选择模式 / 按 ID 选择、当前选中对象、编辑、参数化建模和导出模型;部分辅助面板暂时收起,后续需要时再放回。
|
||||
- 局部编辑会先做计划、风险提示、预览和后台执行;失败时会尽量回滚,成功后进入撤销/重做历史。
|
||||
- 后续过渡到 `SCDM-first` 后端:能由 SpaceClaim/SCDM 识别和修改的特征,优先调用 SCDM 脚本接口;本软件不再重复造完整特征识别和直接建模内核,只负责 UI、缓存、脚本生成、校验、回滚和结果映射。
|
||||
- 当前开发顺序改为分阶段闭环:先集中完成 Face 修改能力并让工程师专项测试,再进入孔/槽,再进入凸台、圆角/倒角、Edge 和壳体等特征,最后扩展二级/三级关系;不要每类只做一点。
|
||||
- 当前统一走 `SCDM-first` 主线:能由 SpaceClaim/SCDM 识别和修改的特征,优先调用 SCDM 脚本接口;本软件不再重复造完整特征识别和直接建模内核,只负责 UI、缓存、脚本生成、校验、回滚、结果映射、参数导出和关系式管理。
|
||||
- 当前开发顺序按一条路线闭环:SCDM 识别与缓存 -> 能力字典和参数表 -> 参数化建模任务 -> 结果校验和 ID 续接 -> 按 Face、孔、槽、凸台、圆角/倒角、阵列、壳体逐项适配;本地 OCCT / Analysis Situs 只作为兜底和对照。
|
||||
- 新开 Codex 聊天框继续开发时,只需要 Codex 阅读 README 末尾的“Codex 项目记忆”;普通开发者可以忽略那一节。
|
||||
|
||||
## 软件整体架构
|
||||
|
||||
当前项目大致是这样分层的:
|
||||
当前项目大致是这样分层的。行数按当前 Git 跟踪的可读源码/脚本统计,排除 README、STEP 模型、图标、`.git`、`local`、`third_party` 和临时文件。
|
||||
|
||||
```text
|
||||
python-occt
|
||||
├── main.py
|
||||
python-occt(源码/脚本 84,740 行)
|
||||
├── main.py(7 行)
|
||||
│ └── 程序入口,支持普通启动、烟测、隔离子进程 worker 入口
|
||||
│
|
||||
├── step_editor/
|
||||
│ ├── app.py
|
||||
├── step_editor/(62,303 行)
|
||||
│ ├── app.py(1,976 行)
|
||||
│ │ └── 主窗口初始化、左侧操作面板、按钮和布局
|
||||
│ │
|
||||
│ ├── window_core.py
|
||||
│ ├── window_core.py(4,171 行)
|
||||
│ │ └── STEP 加载、VTK 视图、模型显示、拾取、高亮、坐标轴、FPS、显示刷新
|
||||
│ │
|
||||
│ ├── window_actions.py
|
||||
│ ├── window_actions.py(9,677 行)
|
||||
│ │ └── 导出、参数化建模动作、后台任务、隔离子进程、操作历史
|
||||
│ │
|
||||
│ ├── window_state.py
|
||||
│ ├── window_state.py(10,169 行)
|
||||
│ │ └── 选择状态、参数表状态、按钮启用/禁用、撤销/重做、历史定位
|
||||
│ │
|
||||
│ ├── model.py
|
||||
│ ├── model.py(5,713 行)
|
||||
│ │ └── StepModel 核心对象,保存 shape、Face、Edge、Solid、拓扑缓存和 mixin 组合
|
||||
│ │
|
||||
│ ├── features.py
|
||||
│ ├── features.py(3,591 行)
|
||||
│ │ └── 特征识别和编辑计划,比如孔、槽、凸台、圆角、壳体、解析曲面
|
||||
│ │
|
||||
│ ├── recognition_graph.py
|
||||
│ ├── recognition_graph.py(539 行)
|
||||
│ │ └── 内部 Face 几何图和通孔拆面识别缓存,记录平面/圆柱、邻接、共面、平行、垂直、同轴等基础关系
|
||||
│ │
|
||||
│ ├── asitus_bridge.py
|
||||
│ ├── asitus_bridge.py(308 行)
|
||||
│ │ └── Analysis Situs 孔组识别 CLI 桥接,把外部识别到的孔 Face 组映射回 Python/Qt Face 编号
|
||||
│ │
|
||||
│ ├── scdm_backend.py
|
||||
│ ├── scdm_backend.py(623 行)
|
||||
│ │ └── SCDM 后端发现和缓存底座,负责自动发现并缓存 SpaceClaim.exe,生成 /RunScript 命令并做最小烟测
|
||||
│ │
|
||||
│ ├── scdm_probe.py
|
||||
│ ├── scdm_probe.py(680 行)
|
||||
│ │ └── SCDM probe 任务生成和脚本生成,负责写 scdm_probe_job.json、临时 RunScript 脚本和 raw 识别输出约定
|
||||
│ │
|
||||
│ ├── scdm_edit_runner.py
|
||||
│ ├── scdm_edit_runner.py(1,145 行)
|
||||
│ │ └── SCDM 修改任务执行器,负责写 scdm_edit_job.json、临时修改脚本、result.step、result.json 和 error.json
|
||||
│ │
|
||||
│ ├── scdm_result_validator.py
|
||||
│ ├── scdm_result_validator.py(770 行)
|
||||
│ │ └── SCDM 结果校验和 ID 续接工具,负责输出文件检查、目标值回测、旧对象到新对象的唯一匹配和关系式 ID 重写
|
||||
│ │
|
||||
│ ├── scdm_schema.py / scdm_capabilities.py / scdm_feature_mapper.py / scdm_property_specs.py
|
||||
│ ├── scdm_schema.py / scdm_capabilities.py / scdm_feature_mapper.py / scdm_property_specs.py(1,821 行)
|
||||
│ │ └── SCDM raw 结果、产品能力字典、scdm_feature_cache.json 映射和参数表 spec 转换层,避免把 SCDM 原始技术对象直接暴露给 UI
|
||||
│ │
|
||||
│ ├── operations.py
|
||||
│ ├── operations.py(14,604 行)
|
||||
│ │ └── 真正的几何编辑实现,比如拉伸/切除、孔径、孔深、边长、圆角、倒角
|
||||
│ │
|
||||
│ ├── geometry_utils.py
|
||||
│ ├── geometry_utils.py(1,427 行)
|
||||
│ │ └── 通用 OCCT 几何工具、布尔结果处理、B-Rep 校验、拓扑辅助
|
||||
│ │
|
||||
│ ├── polydata.py
|
||||
│ ├── polydata.py(425 行)
|
||||
│ │ └── 把 OCCT Shape / Face / Edge 转成 VTK polydata,用于显示和拾取
|
||||
│ │
|
||||
│ ├── step_io.py
|
||||
│ ├── step_io.py(160 行)
|
||||
│ │ └── STEP/XCAF 读取、STEP 导出、内部 BREP 临时交换
|
||||
│ │
|
||||
│ ├── export.py
|
||||
│ ├── export.py(179 行)
|
||||
│ │ └── 导出完整模型、零件、Solid、Face、Edge,以及质量检查
|
||||
│ │
|
||||
│ ├── isolated_edit_worker.py
|
||||
│ ├── isolated_edit_worker.py(238 行)
|
||||
│ │ └── 高风险几何编辑的隔离子进程入口
|
||||
│ │
|
||||
│ ├── workers.py
|
||||
│ ├── workers.py(51 行)
|
||||
│ │ └── Qt 后台任务 worker,避免主界面被计算堵死
|
||||
│ │
|
||||
│ ├── ui_helpers.py / widgets.py / info_panel.py
|
||||
│ ├── ui_helpers.py / widgets.py / info_panel.py(1,682 行)
|
||||
│ │ └── UI 辅助函数、自定义控件、属性信息显示
|
||||
│ │
|
||||
│ └── recognition_priority.py
|
||||
│ └── recognition_priority.py(238 行)
|
||||
│ └── 特征识别优先级,让常用、稳定、语义清楚的特征排前面
|
||||
│
|
||||
├── scripts/
|
||||
├── scripts/(21,988 行)
|
||||
│ └── 各类专项验证脚本和一级编辑总回归入口
|
||||
│
|
||||
├── tools/asitus_probe/(386 行)
|
||||
│ └── Analysis Situs probe 的 C++ 源码和 CMake 配置
|
||||
│
|
||||
├── environment.yml / .gitignore(56 行)
|
||||
│ └── Conda 环境和 Git 忽略规则
|
||||
│
|
||||
└── assets/models/
|
||||
└── 仓库自带测试 STEP 模型
|
||||
```
|
||||
|
||||
## SCDM 后端过渡路线
|
||||
## SCDM-first 主路线
|
||||
|
||||
本阶段目标:把 SpaceClaim/SCDM 接成优先几何后端。SCDM 负责识别和执行它能稳定处理的直接建模能力;本软件负责显示、选择、能力映射、任务生成、异步执行、结果校验、回滚、ID 续接和参数导出。SCDM 不接管本软件 UI,也不把 SCDM 原始界面文字直接展示给客户。
|
||||
本阶段开始,README 里过去分散写的 SCDM 后端、开源辅助识别、参数化编辑和关系式内容统一收敛成一条 SCDM-first 路线,不再并行推进多套方案。
|
||||
|
||||
统一主线是:SCDM 负责识别和执行它能稳定处理的直接建模能力;本软件负责显示、选择、能力映射、参数表、关系式、任务生成、异步执行、结果校验、回滚、ID 续接和参数导出。Analysis Situs 和本地 OCCT 识别不再作为主路线和 SCDM 抢“谁说了算”,只作为兜底、对照验证和轻量几何关系辅助。SCDM 不接管本软件 UI,也不把 SCDM 原始界面文字直接展示给客户。
|
||||
|
||||
交付判断不是“调用了 SCDM”,而是下面这条闭环能稳定跑通:
|
||||
|
||||
@@ -107,103 +115,153 @@ python-occt
|
||||
导入 STEP
|
||||
-> 自动找到可用的 SpaceClaim.exe,并缓存路径、来源、版本和验证结果
|
||||
-> 本软件显示模型并建立当前 Face / Edge / Solid 基础索引
|
||||
-> 生成 scdm_probe_job.json,说明要让 SCDM 扫描哪个 STEP、输出到哪里、使用哪个适配器
|
||||
-> SCDM probe 打开 STEP,读取结构化几何对象、可执行命令候选、失败/限制原因
|
||||
-> 写出 scdm_raw_features.json,保留 SCDM 原始结构化结果,不依赖 UI 显示名称
|
||||
-> 本软件把 raw 结果映射到能力字典,生成 scdm_feature_cache.json
|
||||
-> 用户点击对象时,参数表只显示 cache 中已产品化、可执行、可校验的参数
|
||||
-> 用户修改目标值后,生成 scdm_edit_job.json 和对应 SCDM 修改脚本
|
||||
-> SpaceClaim.exe /RunScript 在后台隔离执行,成功输出 result.step,失败输出 error.json
|
||||
-> 本软件用 OCCT 重新读 result.step,做 B-Rep 校验、目标值回测和非预期变形检查
|
||||
-> 校验通过后替换当前模型,重新跑 probe 刷新 scdm_feature_cache.json
|
||||
-> 本软件把“请识别这个 STEP”的任务写给 SCDM,并用 /RunScript 启动 SpaceClaim
|
||||
-> SCDM 在后台打开 STEP,识别可编辑对象、参数、命令候选和限制原因,并把结果写回给本软件
|
||||
-> 本软件把 SCDM 结果翻译成能力字典和中文参数,并缓存起来,后续点击对象直接查缓存
|
||||
-> 用户点击对象时,参数表只显示已经产品化、可执行、可校验的参数
|
||||
-> 用户修改目标值后,本软件把“改哪个对象、改成多少、失败怎么回滚”的修改任务写给 SCDM
|
||||
-> SpaceClaim.exe /RunScript 在后台执行修改,成功写出新的 STEP,失败写出错误原因
|
||||
-> 本软件用 OCCT 重新读取新的 STEP,做 B-Rep 校验、目标值回测和非预期变形检查
|
||||
-> 校验通过后替换当前模型,重新让 SCDM 识别并刷新缓存
|
||||
-> 用新缓存更新 Face / Edge / 特征 ID 映射和关系式引用
|
||||
```
|
||||
|
||||
核心实现拆分如下:
|
||||
### 本软件与 SCDM 如何交互
|
||||
|
||||
本软件不是把 SCDM 界面嵌进来,也不是让客户写脚本。客户看到的仍然是本软件:导入 STEP、点击 Face/Feature、修改“偏移、孔径、位置、阵列间距”等中文参数,然后点击“参数化建模”。SCDM 只是在后台作为几何编辑后端运行。
|
||||
|
||||
两边的沟通方式是“文件中转 + 命令启动”,不是网络接口,也不是共享内存。`RunScript` 不是我们生成的文件名,而是 SpaceClaim 的启动参数;本软件真正生成的是临时脚本文件,然后用类似下面的命令把脚本交给 SCDM 执行:
|
||||
|
||||
```powershell
|
||||
SpaceClaim.exe /RunScript=local\scdm\<模型名>_<指纹>\scdm_probe.py
|
||||
SpaceClaim.exe /RunScript=local\scdm\<模型名>_<指纹>\edit\scdm_edit.py
|
||||
```
|
||||
|
||||
这两个脚本文件由本软件自动生成。第一个脚本负责“识别这个 STEP 能改什么”,第二个脚本负责“按用户输入修改模型”。脚本旁边会有一份任务 JSON,里面写着要打开哪个 STEP、要识别或修改哪个对象、目标值是多少、结果写到哪里。
|
||||
|
||||
```text
|
||||
SCDM-first 实施路线
|
||||
├── S0. 后端发现与缓存
|
||||
│ ├── [x] 新建 step_editor/scdm_backend.py
|
||||
│ ├── [x] 读取 local/scdm_backend.json,缓存可用时直接复用 SpaceClaim.exe
|
||||
│ ├── [x] 缓存失效时自动发现:Windows 注册表 -> 环境变量 -> ANSYS 常见安装目录 -> PATH
|
||||
│ ├── [x] 自动发现失败时,后端返回需要用户配置;导入模型后的 SCDM probe 会自动弹出路径选择框,用户选择 SpaceClaim.exe 后写回缓存
|
||||
│ ├── [x] 找到后可运行最小 /RunScript 烟测,确认能启动、能写报告、许可证可用
|
||||
│ ├── [x] 写回 local/scdm_backend.json:path、source、version、verifiedAt、runScriptOk、licenseOk
|
||||
│ └── [x] 验收:scripts/verify_scdm_backend.py 覆盖缓存、发现、禁用开关和 /RunScript 命令;SCDM 不可用时主程序仍可走 OCCT 降级模式
|
||||
识别阶段:
|
||||
本软件写识别任务 JSON + 识别脚本 scdm_probe.py
|
||||
-> 用 SpaceClaim.exe /RunScript=scdm_probe.py 启动 SCDM
|
||||
-> SCDM 打开 STEP,并把“识别到什么、哪些参数可改、有哪些限制”写回 JSON
|
||||
-> 本软件读取结果,翻译成中文能力和参数表,并缓存起来
|
||||
|
||||
修改阶段:
|
||||
本软件写修改任务 JSON + 修改脚本 scdm_edit.py
|
||||
-> 用 SpaceClaim.exe /RunScript=scdm_edit.py 启动 SCDM
|
||||
-> SCDM 按任务修改模型,并输出新的 STEP 或失败原因
|
||||
-> 本软件读取新 STEP,校验通过才替换当前模型;失败就回滚
|
||||
```
|
||||
|
||||
这些中转文件都放在项目的 `local/scdm/` 工作目录里,只用于本软件和 SCDM 后台沟通、调试和失败回放;清掉后可以重新生成,不提交到 Git。以 `ICEPAK-NATURAL.stp` 为例,当前本机路径类似:
|
||||
|
||||
```text
|
||||
local/scdm/ICEPAK-NATURAL_19ff9d1ede49/
|
||||
├── scdm_probe.py # 本软件生成,喂给 SCDM 做识别
|
||||
├── scdm_probe_job.json # 本软件生成,告诉识别脚本要打开哪个 STEP、结果写哪
|
||||
├── scdm_raw_features.json # SCDM 写回,原始识别结果
|
||||
├── scdm_feature_cache.json# 本软件生成,翻译后的中文参数和能力缓存
|
||||
└── edit/
|
||||
├── scdm_edit.py # 本软件生成,喂给 SCDM 做修改
|
||||
├── scdm_edit_job.json # 本软件生成,告诉修改脚本改哪个对象、目标值是多少
|
||||
├── result.step # SCDM 修改成功后输出的新 STEP
|
||||
└── error.json # SCDM 修改失败时输出的错误原因
|
||||
```
|
||||
|
||||
模型文件变更后会重新计算指纹,目录名也会变,旧识别结果不会误用到新模型上。`导出参数` 生成的外部流程组件是另一条链路,不属于本软件和 SCDM 的后台通信。
|
||||
|
||||
这里说的“SCDM 能做”,不是指 SCDM 界面里人工能点的所有按钮,而是指本软件能通过脚本拿到下面四类证据:
|
||||
|
||||
1. SCDM 返回了结构化对象或可定位几何,比如 Face、Hole、Slot、Boss、Round、Chamfer、Pattern、Shell。
|
||||
2. SCDM 返回或脚本环境确认了可用命令,比如 `OffsetFaces`、`Move`、`Fill`、`Delete`、`Chamfer`、`ConstantRound`。
|
||||
3. 当前对象有可用的当前值或定位信息,比如直径、位置、半径、Face locator、实例中心。
|
||||
4. 修改后能用新 STEP、新 cache、目标值回测或“目标特征消失”证明结果是对的。
|
||||
|
||||
不满足这四点的内容,只能进入诊断、日志或“待适配能力”,不能直接展示成客户可修改参数。
|
||||
|
||||
SCDM 内部如何处理相邻面、圆角链、二级/三级拓扑传播,交给 SCDM。本软件不再把手写一级、二级、三级传播当成新增能力主线,只负责把 SCDM 能力适配成可见参数、可执行任务和可校验结果;SCDM 做不到或当前脚本拿不到证据的能力,先不开放。
|
||||
|
||||
状态标记:
|
||||
|
||||
- `[x]` 已适配:已有能力字典、参数表/命令入口、edit job、runner 或 UI gate、结果校验和回归测试。
|
||||
- `[~]` 部分适配:已有识别、cache、脚本入口或假 runner,但真实 STEP 样例、命令细节或结果守门还要继续补。
|
||||
- `[ ]` 待适配:SCDM 可能能做,但本软件还没有完成结构化识别、命令脚本和结果校验闭环。
|
||||
|
||||
核心路线只保留下面这一棵树。前半段是 SCDM 后端闭环,后半段是按客户高频和 SCDM 可验证能力开放参数:
|
||||
|
||||
```text
|
||||
SCDM-first 主路线
|
||||
├── 0. 后端发现与可用性
|
||||
│ ├── [x] [自动发现 SpaceClaim.exe -> 缓存路径、来源、版本和验证结果]
|
||||
│ ├── [x] [发现顺序 -> 缓存、注册表、环境变量、ANSYS 常见目录、PATH]
|
||||
│ ├── [x] [找不到 -> 导入模型后弹出路径选择,用户选中后写回缓存]
|
||||
│ └── [x] [SCDM 不可用 -> 软件仍可查看、导出并使用本地兜底能力]
|
||||
│
|
||||
├── S1. Probe 任务与原始识别输出
|
||||
│ ├── [x] 新建 step_editor/scdm_probe.py,负责生成 scdm_probe_job.json 和临时 SCDM probe 脚本
|
||||
│ ├── [x] Probe 输入:STEP 路径、单位、模型指纹、输出目录、SCDM 版本、扫描范围
|
||||
│ ├── [~] Probe 动作:当前脚本已防御式遍历 Body / Face / Edge,Edge raw 已输出长度、起终点、圆弧半径和相邻 Face 摘要,并会先尝试 `StandardHoles.Find` / `GetHoleFaces`,失败或为空再回退到圆柱碎片聚合;圆柱面会尝试 `RoundInfo.Create` 生成圆角诊断摘要,Chamfer / Fill 的深度 API 识别仍待继续补
|
||||
│ ├── [x] Probe 输出:scdm_raw_features.json,包含 geometry、topologyHint、backendCommandCandidates、rawLimitations、availableCommands、Face 邻接、Edge 几何摘要和 featureInventory
|
||||
│ ├── [x] 不依赖 SCDM UI 文案;只记录 API 类型、几何属性、命令对象、参数字段和返回状态
|
||||
│ └── [~] 验收:scripts/verify_scdm_probe_pipeline.py 已覆盖 job/script/raw->cache 假数据闭环、`StandardHoles.Find`、`RoundInfo` 脚本入口和 availableCommands 映射;本机 SCDM v222 已验证 ICEPAK 可打开并遍历 13 Body / 158 Face / 396 Edge,且 396 条 Edge 均可回传长度、起终点和相邻 Face 数,脚本环境可见 `StandardHoles` / `OffsetFaces` / `Move` / `Fill` / `Delete` / `Chamfer` / `ConstantRound` / `RoundInfo`;该样例的标准孔和圆角 API 识别结果均为 0,仍需继续拿更多样例适配
|
||||
├── 1. SCDM 识别与缓存
|
||||
│ ├── [x] [scdm_probe_job.json -> /RunScript 扫描 STEP]
|
||||
│ ├── [x] [scdm_raw_features.json -> 保存 SCDM 结构化对象、几何属性、命令候选和限制原因]
|
||||
│ ├── [x] [scdm_feature_cache.json -> 映射为本软件能力字典和中文参数]
|
||||
│ ├── [x] [cache 命中 -> 点击对象只查缓存,不重复启动 SCDM]
|
||||
│ ├── [~] [probe 深度 -> Face / Edge / Body 已稳定遍历,Hole / Round API 真实样例继续补]
|
||||
│ └── [x] [不能产品化的 raw 结果 -> 只进诊断,不进客户参数表]
|
||||
│
|
||||
├── S2. 统一 Schema 与能力字典
|
||||
│ ├── [x] 新建 step_editor/scdm_schema.py,定义 raw feature、normalized feature、capability、edit job 的字段
|
||||
│ ├── [x] 新建 step_editor/scdm_capabilities.py,定义产品能力 key、中文名、单位、输入类型、默认建模意图、SCDM 命令、所需后端命令组和校验器
|
||||
│ ├── [x] 第一批能力:hole.diameter、hole.position、face.offset、feature.fill
|
||||
│ ├── [x] S7 后续能力先以 productized=false 进入能力字典和诊断缓存,不直接显示成客户可执行参数
|
||||
│ ├── [x] 未进入能力字典的 SCDM 结果只进入诊断和日志,状态为 discovered_not_productized
|
||||
│ ├── [x] 每个能力都必须声明 postCheck,例如目标孔径、目标轴心、目标偏移或目标特征消失
|
||||
│ └── [x] 验收:假 raw JSON 经过映射后,只产出已定义能力;未定义能力不会出现在产品化 cache
|
||||
├── 2. 能力字典、参数表和用户入口
|
||||
│ ├── [x] [capability key -> 中文名、单位、输入类型、建模意图、后端命令和 postCheck]
|
||||
│ ├── [x] [参数表 -> 只显示已产品化、可执行、可校验的参数]
|
||||
│ ├── [x] [不可改 -> UI 阶段直接说明是未实现、命令不可用、识别不完整还是风险过高]
|
||||
│ ├── [x] [参数化建模 -> 多个目标值统一提交,不再每行一个操作按钮]
|
||||
│ ├── [x] [导出参数 -> 勾选输入参数后生成组件目录 main.py 和参数 schema]
|
||||
│ └── [~] [关系式 -> 作为参数表上游输入,计算后回填目标值,已阻止自引用/重复目标/循环依赖]
|
||||
│
|
||||
├── S3. SCDM 缓存生成与失效策略
|
||||
│ ├── [x] 新建 step_editor/scdm_feature_mapper.py,把 scdm_raw_features.json 映射为 scdm_feature_cache.json
|
||||
│ ├── [x] cache 记录:modelFingerprint、backendVersion、objects、capabilities、geometrySignature、blockReason
|
||||
│ ├── [x] SCDM raw 没有 Python Face ID 时,用本软件当前 StepModel 的轻量几何签名补回 faceIds,支持米/mm 单位比例匹配
|
||||
│ ├── [x] cache 会额外保留 geometry_candidate_hints:当 SCDM 只给出平面/圆柱面/圆边/直边这些底层证据,而没有明确 slot/boss/round 对象时,先把 S7 候选记为“几何证据待分类”,不进入客户参数表
|
||||
│ ├── [x] 导入模型、重新加载、SCDM 参数化修改成功、撤销和重做后都会标记 cache 失效并后台刷新
|
||||
│ ├── [x] cache 可用时点击对象只查窗口/model 上的 scdm_feature_cache,不重新启动 SCDM
|
||||
│ ├── [x] cache 正在生成时,UI 状态栏显示“正在后台识别 SCDM 可修改参数”,不阻塞旋转查看模型
|
||||
│ └── [~] 验收:raw->cache 映射已由 scripts/verify_scdm_probe_pipeline.py 覆盖;window_core.py 已接后台预热,真实 SCDM 运行耗时和失败提示待样例机验证
|
||||
├── 3. SCDM 参数化建模执行
|
||||
│ ├── [x] [scdm_edit_job.json -> 记录模型、对象签名、能力、目标值、输出路径和超时]
|
||||
│ ├── [x] [/RunScript 后台执行 -> 成功写 result.step/result.json,失败写 error.json]
|
||||
│ ├── [x] [face.offset / hole.diameter / hole.position -> ICEPAK 真实 SCDM 回测通过]
|
||||
│ ├── [~] [slot.width / slot.depth / slot.position / boss.diameter / boss.height / boss.position -> runner、UI gate 和目标校验已接,真实样例待补]
|
||||
│ └── [~] [round.radius / chamfer.distance / feature.fill / feature.delete_round_or_chamfer -> 脚本和假 runner 已接,真实样例待补]
|
||||
│
|
||||
├── S4. 参数表接入
|
||||
│ ├── [x] 修改 window_state.py 的参数表数据源:存在 SCDM cache 时优先转成参数表 spec,现有 OCCT/Analysis Situs 能力作为兜底;对大模型多内孔平面位置调整这类本地已验证快路径,参数表会优先走 OCCT 专用一级边界重建,避免 SCDM 直接建模长时间计算
|
||||
│ ├── [x] 选中对象后,用当前 Face / Edge 索引、逻辑 ID、整孔 Face 组和 SCDM->Python faceIds 映射匹配 cache 中的 geometrySignature
|
||||
│ ├── [~] 匹配成功且 capability 已接入执行器时展示能力字典里的中文参数、当前值、建模意图、目标值和输入参数勾选框;参数表只展示 enabled 能力,未开放能力只进状态提示/诊断;当前按能力开放 `face.offset`、`hole.diameter`、`hole.position`、`slot.position`、`boss.position`
|
||||
│ ├── [x] 匹配失败或能力未开放时给出短原因:后台识别中、SCDM 未识别、能力未产品化、脚本命令不可用、后端失败;当前状态栏和下方诊断信息都会显示 SCDM 后端状态、当前选择状态、可执行能力和未开放原因
|
||||
│ ├── [x] 保留现有关系式、参数导出和统一“参数化建模”按钮,不新增一套 SCDM 专用 UI;数值型 SCDM 能力改目标值后执行,命令型能力在选中该行后由同一个按钮执行
|
||||
│ └── [~] 验收:scripts/verify_scdm_probe_pipeline.py 已验证 Face / Hole 参数 spec 转换、能力级启用开关和圆柱碎片组到本地 Face ID 的回填;scripts/verify_property_card_editor_ui.py 已覆盖 SCDM 状态摘要和选择诊断同步;真实 UI 长流程回归待继续
|
||||
├── 4. 结果校验、回滚和 ID 续接
|
||||
│ ├── [x] [OCCT 回读 result.step -> 检查输出文件、B-Rep 和基础模型规模]
|
||||
│ ├── [~] [目标回测 -> 已覆盖数值目标、位置目标、目标特征消失和未编辑对象漂移]
|
||||
│ ├── [~] [ID 续接 -> 简单唯一匹配已接,歧义/拆分/合并样例继续补]
|
||||
│ ├── [~] [关系式刷新 -> 修改后按新 cache 重写 Face / Edge / 特征 ID]
|
||||
│ └── [x] [失败 -> 回滚原模型并给出明确原因]
|
||||
│
|
||||
├── S5. 修改任务执行闭环
|
||||
│ ├── [x] 新建 step_editor/scdm_edit_runner.py,生成 scdm_edit_job.json 和临时 SCDM 修改脚本
|
||||
│ ├── [x] edit job 记录:模型路径、对象签名、capabilityKey、目标值、输出 STEP、超时、回滚文件
|
||||
│ ├── [~] 第一批执行:face.offset、孔径、孔位置已在本机 SCDM v222 + ICEPAK 样例真机验证;S7.2 的槽位置和 S7.3 的凸台位置已接入 `Move` 脚本、edit job、cache 映射和 UI gate,真实槽/凸台样例回测待补;命令型能力的统一按钮承载已补,填孔/删除小特征已补 `Fill.Execute` / `Delete.Execute` 脚本适配但仍待真实 STEP 验证后再开放
|
||||
│ ├── [~] SpaceClaim.exe /RunScript 已接入 UI 后台任务,成功后加载 result.step,重新触发 SCDM probe,并在 cache 刷新后再通知关系式/批量执行继续;校验通过后会写入撤销历史,撤销/重做会触发 SCDM cache 失效和后台刷新
|
||||
│ ├── [x] 成功写 result.step / result.json;失败写 error.json,包含 SCDM 命令、对象、参数和失败原因
|
||||
│ └── [~] 验收:scripts/verify_scdm_edit_runner.py 已覆盖 job/script/result/error、缺失输出、空 STEP、`Fill.Execute` / `Delete.Execute` 脚本路径和假 runner 闭环;真实 SCDM 已验证 `OffsetFaces.Execute`、`Move.Translate`、`DocumentSave.Execute`,UI 撤销历史和撤销/重做 cache 失效已接入,真实长流程回归待继续
|
||||
├── 5. 当前已开放或正在开放的 SCDM 能力
|
||||
│ ├── [x] [Face 偏移 -> face.offset / OffsetFaces]
|
||||
│ ├── [x] [孔直径 -> hole.diameter / StandardHoles 或同轴 OffsetFaces]
|
||||
│ ├── [x] [孔位置 -> hole.position / Move]
|
||||
│ ├── [x] [填孔/删除小特征 -> feature.fill / Fill 或 Delete]
|
||||
│ ├── [~] [槽宽 -> slot.width / OffsetFaces,真实 STEP 回测待补]
|
||||
│ ├── [~] [槽深 -> slot.depth / Move 或 OffsetFaces,只开放 SCDM 返回槽深、槽底面和深度方向证据的槽]
|
||||
│ ├── [x] [槽位置 -> slot.position / Move]
|
||||
│ ├── [~] [凸台直径 -> boss.diameter / OffsetFaces,真实 STEP 回测待补]
|
||||
│ ├── [~] [凸台高度 -> boss.height / Move 或 OffsetFaces,真实 STEP 回测待补]
|
||||
│ ├── [x] [凸台位置 -> boss.position / Move]
|
||||
│ ├── [~] [圆角半径 -> round.radius / ConstantRound,只开放 SCDM 明确返回等半径证据的圆角]
|
||||
│ ├── [~] [倒角距离 -> chamfer.distance / Chamfer,只开放 SCDM 明确返回等距倒角证据的倒角]
|
||||
│ ├── [~] [删除圆角/倒角 -> feature.delete_round_or_chamfer / Fill 或 Delete,真实 STEP 回测待补]
|
||||
│ ├── [x] [阵列间距 -> pattern.spacing / Move,整体阵列保持中心不变并等距重排;安全范围由支撑面动态计算,不针对 Face92 写死]
|
||||
│ └── [~] [局部间距 -> pattern.segment_spacing / Move,按“FaceA-FaceB 间距”或“零件A-零件B 间距”修改相邻段;已支持固定前项移动后侧、固定后项移动前侧、两侧均分保持中心]
|
||||
│
|
||||
├── S6. 结果校验、ID 续接和关系式刷新
|
||||
│ ├── [~] 用 OCCT 读取 result.step,校验 B-Rep 有效性、Solid 数量、目标参数、关键特征是否仍存在;当前 UI 已拒绝缺失/空输出 STEP,并在重新 probe 后调用 OCCT B-Rep 检查、目标值回测、未编辑对象漂移检查和 raw summary 全局规模漂移检查,圆角链/孔面数量等更专门几何质量规则待继续
|
||||
│ ├── [x] 修改前后保存 geometrySignature,用新 cache 做旧对象到新对象的唯一匹配
|
||||
│ ├── [~] 唯一匹配成功时更新 Face / Edge / 特征 ID,并同步更新关系式文本;SCDM 修改后的 cache 刷新阶段已接入关系式 ID 重写,歧义场景仍需更多真实样例回归
|
||||
│ ├── [x] 找不到或匹配多个时,返回 none / multiple 状态,供 UI 将关系式标记为失效并提示用户重新选择对象
|
||||
│ ├── [~] 对 SCDM 返回成功但几何变形的结果必须回滚;当前已能在 cache 层拦截其它已识别对象丢失、漂移、歧义匹配,以及 Face/Edge/Object 总量异常大幅变化,并由 UI 恢复编辑前快照;圆角/孔面数量等更专门质量规则待补
|
||||
│ └── [~] 验收:scripts/verify_scdm_result_validator.py 已覆盖输出文件、目标值、raw summary 全局规模漂移、未编辑对象漂移、ID 映射、关系式重写和歧义匹配;ICEPAK 真实 SCDM 已回读验证 face.offset、孔径和孔位置输出 STEP
|
||||
├── 6. 下一批只按 SCDM 能力适配
|
||||
│ ├── [ ] [阵列实例位置 -> pattern.instance_position]
|
||||
│ └── [~] [壳体厚度 -> shell.thickness,薄壁候选配对已进 cache,真实命令和 STEP 回测待补]
|
||||
│
|
||||
├── S7. 能力扩展顺序
|
||||
│ ├── [~] 第二批:slot.position 已接入能力字典、参数表、`move_slot` 执行脚本和 fake runner 回归,真实槽 STEP 回测待补;slot.width、slot.depth 仍只进入 planned_not_productized 诊断,真实 SCDM 命令和样例回测待补
|
||||
│ ├── [~] 第三批:boss.position 已接入能力字典、参数表、`move_boss` 执行脚本和 fake runner 回归,真实凸台 STEP 回测待补;boss.height、boss.diameter 仍只进入 planned_not_productized 诊断,真实 SCDM 命令和样例回测待补
|
||||
│ ├── [~] 第四批:round.radius、chamfer.distance、feature.delete_round_or_chamfer 已进入能力字典和 planned_not_productized 诊断;probe 已补 `RoundInfo` 摘要入口,真实 SCDM 命令和样例回测待补
|
||||
│ ├── [~] 第五批:pattern.spacing、pattern.instance_position、shell.thickness 已进入能力字典和 planned_not_productized 诊断;重复圆柱孔中心已能派生 linear_pattern 候选并写入 derived_feature_candidates,真实 SCDM 命令和样例回测待补
|
||||
│ ├── [x] 内部能力进度报告:`scdm_status.summarize_scdm_capability_progress` 会统计能力字典、执行器开放名单、当前 cache 已识别能力、后端命令阻止项、planned_not_productized、discovered_not_productized、geometry_candidate_hints、derived_feature_candidates,以及 SCDM probe 的对象/曲面/命令候选分布,供开发诊断和内部日志解释当前进度;客户 UI 只展示能识别/不能识别、能修改/不能修改
|
||||
│ └── [~] 每新增一个 capability,都必须同时补 probe 映射、edit runner、postCheck、UI 假 cache 测试和真实 STEP 样例;当前已补能力字典、probe 映射、Face 邻接、Edge raw 几何摘要、featureInventory、geometry_candidate_hints、假 cache 测试和能力进度报告,slot.position、boss.position 已补 edit runner 协议,其它 S7 能力的 edit runner 与真实 STEP 样例待补
|
||||
│
|
||||
└── S8. 打包与交付策略
|
||||
├── [ ] 不把商业 SCDM 打包进本软件,只检测客户本机安装和许可证
|
||||
├── [~] 打包产物带默认发现逻辑、自动路径选择和 SCDM 不可用说明;当前“软件进度”已改为紧凑按钮,点击后弹出 Markdown 风格能力清单,按“已能修改、已能识别、暂不能修改、暂不能稳定识别”组织;SCDM 找不到时再自动弹出 SpaceClaim.exe 路径选择
|
||||
├── [~] SCDM-only 能力在后端命令不可用时禁用并显示原因;当前已根据 availableCommands 阻止缺少 `OffsetFaces` / `Move` / `Fill` / `Delete` 等命令的能力,隐藏策略待打包态细化
|
||||
├── [x] 日志保留本次使用的后端:SCDM / OCCT / Analysis Situs,方便给客户解释结果来源;当前 SCDM probe/edit job、result.json、SCDM 操作历史、普通 OCCT 操作历史和左侧状态摘要都会记录后端来源,普通 OCCT 历史会额外写 execution=Qt worker/isolated subprocess,以及 recognition=internal StepModel 或 Analysis Situs + internal StepModel
|
||||
└── [ ] 验收:无 SCDM 机器可启动和查看 STEP;有 SCDM 机器可完成第一批 SCDM 修改闭环
|
||||
└── 7. 交付与兜底边界
|
||||
├── [x] [软件进度 -> 只展示能识别、不能识别、能修改、不能修改]
|
||||
├── [x] [日志 -> 记录本次使用 SCDM / OCCT / Analysis Situs 哪个后端]
|
||||
├── [x] [Analysis Situs -> 只做辅助定位、兜底识别和开源对照]
|
||||
├── [x] [本地 OCCT -> 只保留已验证兜底能力,不再作为新主线扩展]
|
||||
├── [ ] [商业 SCDM -> 不打包进本软件,只检测客户本机安装和许可证]
|
||||
└── [ ] [SCDM 之外的新能力 -> 等 SCDM 能力适配完再评估]
|
||||
```
|
||||
|
||||
短期开发只做第一批能力,不追求把 SCDM 识别到的所有候选都立即开放。`scdm_raw_features.json` 保留 SCDM 发现的全部结构化候选,供后续扩展;`scdm_feature_cache.json` 只保存已经映射到能力字典、能执行、能校验、能解释失败原因的产品化能力。`geometry_candidate_hints` 只表示“有几何证据值得继续分类”,不能当作可编辑参数展示给客户。`软件进度` 弹窗只展示客户关心的能力边界:哪些能识别、哪些不能稳定识别、哪些能修改、哪些暂不能修改;cache 数量、probe 证据、Runner 开放数等后台细节留在日志和开发诊断里。这样既能利用 SCDM 的专业识别和直接建模能力,也不会把未验证的内部候选暴露给客户。
|
||||
|
||||
## Analysis Situs 接入状态
|
||||
### Analysis Situs 辅助定位(非主线后端)
|
||||
|
||||
一句话状态:还没有全量接完;当前已经完成“孔组识别桥接 + AAG 轻量关系摘要 + 几何关系摘要”这一步,能把 Analysis Situs 识别出的拆面圆柱孔组用于整孔高亮、参数表和一级关系计划,也能把外部 AAG 的 Face、邻接、角度类型、共面、同轴、平行、垂直和相切摘要缓存到 `StepModel`,并作为 `recognition_graph.py` 的 `external_*` 关系证据;但还没有把它的完整 AAG/特征分析能力接成通用识别引擎。
|
||||
一句话状态:Analysis Situs 不再作为当前主识别路线继续接入;它保留为 SCDM-first 路线里的辅助定位、兜底识别和开源对照验证。当前已经完成“孔组识别桥接 + AAG 轻量关系摘要 + 几何关系摘要”,能把拆面圆柱孔组用于整孔高亮、参数表兜底和一级关系计划,也能把外部 AAG 的 Face、邻接、角度类型、共面、同轴、平行、垂直和相切摘要缓存到 `StepModel`,并作为 `recognition_graph.py` 的 `external_*` 关系证据。
|
||||
|
||||
当前已完成:
|
||||
|
||||
@@ -225,19 +283,18 @@ SCDM-first 实施路线
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\build_asitus_probe.ps1
|
||||
```
|
||||
|
||||
当前未完成:
|
||||
当前不再作为主线推进的内容:
|
||||
|
||||
- `[ ]` 还没有把 Analysis Situs 的完整 AAG 关系图作为主识别数据源;现在已经消费孔组、AAG 邻接/角度摘要和基础几何关系摘要,并用它们增强孔、槽、凸台、圆角候选的置信度和排序,但编辑计划的主判断仍以本项目现有 `StepModel` / `recognition_graph.py` 为准。
|
||||
- `[ ]` 还没有用 Analysis Situs 直接识别倒角、壳体、阵列孔或装配约束;槽、凸台和圆角当前只是辅助识别提示,不等同于外部特征引擎直接给出的可编辑特征。
|
||||
- `[ ]` 还没有让 Analysis Situs 的共面、同轴、平行、垂直、相切关系直接驱动一级/二级编辑计划;目前它们只作为外部证据增强识别图和置信度,真正能不能改仍由现有 `StepModel`、`recognition_graph.py`、编辑计划和结果守门共同决定。
|
||||
- `[ ]` 不再把 Analysis Situs 的完整 AAG 关系图接成主识别数据源;主识别和主修改优先走 SCDM,Analysis Situs 只增强孔、槽、凸台、圆角候选的置信度和排序。
|
||||
- `[ ]` 不再优先用 Analysis Situs 直接识别倒角、壳体、阵列孔或装配约束;这些高频能力按 SCDM capability 字典逐项产品化。
|
||||
- `[ ]` 不再让 Analysis Situs 的共面、同轴、平行、垂直、相切关系直接驱动一级/二级编辑计划;它们只作为外部证据,真正能不能改由 SCDM cache、能力字典、编辑计划和结果守门共同决定。
|
||||
- `[ ]` 还没有把 Analysis Situs CLI 编译产物纳入打包流程;本地没有 `recognize_holes.exe` 时,程序只走内部识别,不会报错退出。
|
||||
- `[ ]` 还没有做 pybind11 直连;当前是 Python 调外部 CLI,优点是隔离稳定,缺点是启动和 JSON 交换有额外开销。
|
||||
- `[ ]` 暂不做 pybind11 直连;当前 Python 调外部 CLI 的方式更适合作为兜底工具,避免把开源 C++ 分析库直接塞进主 GUI 进程。
|
||||
|
||||
下一步接入顺序:
|
||||
后续只在两种情况下继续动 Analysis Situs:
|
||||
|
||||
1. 把 `tools/asitus_probe` 的构建步骤写入 Windows 打包流程,让交付包能自带可用的 `recognize_holes.exe` 或明确跳过外部识别。
|
||||
2. 继续扩展倒角、壳体、阵列孔等候选的辅助识别提示,仍然只参与“识别和排序”,不直接绕过一级编辑守门。
|
||||
3. 最后再评估是否需要 pybind11 直连;在稳定性没证明之前,CLI 隔离比直接把 C++ 库塞进主进程更安全。
|
||||
1. SCDM 不可用,需要无商业后端兜底识别孔组或轻量关系。
|
||||
2. 需要开源结果和 SCDM probe 结果做对照,帮助判断 SCDM 识别是否漏掉明显几何关系。
|
||||
|
||||
## 怎么运行
|
||||
|
||||
@@ -294,7 +351,7 @@ python main.py assets\models\cube_10mm.step
|
||||
python scripts\generate_cube_step.py
|
||||
```
|
||||
|
||||
10. 验证几何修改基线。当前推进和测试优先级按用户常用操作排序:先 Face 面编辑,再孔、槽、凸台、圆角/倒角、壳体和 Edge,最后再扩二级/三级关系。完整一级编辑回归可以用一个入口串起来:
|
||||
10. 验证几何修改基线。当前主线按 SCDM capability 适配推进;下面这个入口仍保留历史 OCCT/一级编辑回归,用来证明本地兜底能力、UI 和文档守门没有退化:
|
||||
|
||||
```powershell
|
||||
python scripts\verify_first_level_edit_suites.py
|
||||
@@ -391,194 +448,6 @@ git diff --check
|
||||
|
||||
需要注意:STEP 文件通常是 B-Rep 几何数据,不是带完整建模历史的参数化 CAD 原文件。所以“随便选一条边改长度”不一定能稳定实现,实际会转化成更可靠的操作,比如移动某个面、重切某个孔、调整某个圆角等。
|
||||
|
||||
## 参数化编辑路线图
|
||||
|
||||
排序原则:`用户最常用优先 > B-Rep 上稳定可实现 > 参数语义清楚`。这张路线图是后续实现顺序和验收拆分的主线;UI 可以只显示短版进度,但开发和测试按下面这棵树推进。
|
||||
|
||||
状态标记:
|
||||
|
||||
- `[x]` 已实现:已经接入 UI/后端/回归测试,适用于当前明确支持的模型范围。
|
||||
- `[~]` 部分实现/进行中:已有识别、计划或部分模型编辑能力,但真实 STEP 上还需要继续补守门、补失败解释或补局部重建。
|
||||
- `[ ]` 未实现:作为后续目标保留,当前不应在 UI 里伪装成可稳定修改。
|
||||
|
||||
验收口径:
|
||||
|
||||
- `[x]` 不是“所有 CAD 形态都能改”,而是“当前声明支持的模型范围已经有 UI/后端/回归测试闭环”。
|
||||
- R1 Face 是当前主线的第一阶段收口对象;孔、槽、凸台、圆角、壳体和 Edge 已有各自一级基线,但仍按模型工程师给出的真实 STEP 失败项继续补边界。
|
||||
- R8 的二级/三级传播、跨特征约束和特征组联动不算在 0~7 完成度里,不能拿它反向判定 R1 Face 没完成。
|
||||
|
||||
```text
|
||||
STEP/B-Rep 参数化编辑主线
|
||||
├── 0. 先让用户知道“能不能改”
|
||||
│ ├── [x] [选中对象 -> 识别建模形式]
|
||||
│ │ └── Face / 孔 / 槽 / 凸台 / Edge / 圆角 / 倒角候选都要先映射成用户能理解的 CAD 入口。
|
||||
│ ├── [x] [识别结果 -> 显示可修改项]
|
||||
│ │ └── 特征参数表只放当前真正可执行的 [参数 -> 操作],面积这类结果变量放诊断信息。
|
||||
│ ├── [x] [不能修改 -> 立即说明原因]
|
||||
│ │ └── 所有常见特征入口的 blocked plan 会在执行前统一归类成非法输入 / 暂未实现 / 风险过高 / 识别不足,避免等几十秒后才失败。
|
||||
│ ├── [x] [高风险修改 -> 后台隔离执行]
|
||||
│ │ └── OCCT 布尔、圆角、局部重建等危险计算走子进程,失败回滚,主界面和原模型保持稳定。
|
||||
│ └── [x] [路线图 -> 验收脚本守门]
|
||||
│ └── quick 回归覆盖属性表、一级事实、关联探测、各阶段入口、README 口径一致性,并守住 0~7 不混入二级/三级传播任务。
|
||||
│
|
||||
├── 1. 平面 Face,第一条主线
|
||||
│ ├── [x] [偏移 -> 推拉平面 / 拉伸切除]
|
||||
│ │ └── 当前 Face 移动或加料/切削,边、点和共享边一级相邻面跟随重建,复杂端盖走隔离和回滚。
|
||||
│ ├── [x] [偏移 + 保持关系 -> 受限拉伸/切除]
|
||||
│ │ └── 偏移行新增“保持关系”建模意图:执行前要求一级相邻 Face 都能验证为平面平行/垂直关系,执行后反查关系;非平面、斜交或复杂多边界慢计划会提前阻止并有回归守门。
|
||||
│ ├── [x] [面内长度 -> 改长度]
|
||||
│ │ └── 近矩形平面 Face 沿长度方向变化,一级相邻面跟随,面积不作为驱动参数。
|
||||
│ ├── [x] [面内宽度 -> 改宽度]
|
||||
│ │ └── 近矩形平面 Face 沿宽度方向变化,一级相邻面跟随。
|
||||
│ ├── [~] [中心位置 -> 后端保留,界面暂不开放]
|
||||
│ │ └── 中心移动已有后端验证,但模型工程师判断当前客户价值不高,特征参数表暂不显示。
|
||||
│ ├── [x] [壳体厚度 -> 改薄壁厚度]
|
||||
│ │ └── 平面相对面可做局部拉伸/切除或厚度方向缩放,并做 Face 结果校验。
|
||||
│ └── [x] [复杂多边界端盖 -> 边界侧壁重建]
|
||||
│ └── 大 STEP 多内孔平面端盖可走 boundary-shell-rebuild,浅范围内外移动保持内孔线圈和单 Solid;接近切穿或需要孔底/槽底更深关系判断时快速阻止。
|
||||
│
|
||||
├── 2. 孔,第二条主线
|
||||
│ ├── [x] [直径/半径 -> 改孔径]
|
||||
│ │ └── 通孔/盲孔优先同轴重切孔壁,结果必须仍能识别目标圆柱孔。
|
||||
│ ├── [x] [深度 -> 改盲孔深度]
|
||||
│ │ └── 加深切削、变浅补料,目标底面和盲孔语义必须能回读。
|
||||
│ ├── [x] [中心位置 -> 移动孔]
|
||||
│ │ └── 先补旧孔再切新孔,移动的是整个孔特征,不是一片圆柱面。
|
||||
│ ├── [x] [孔本体 -> 封堵/删除孔]
|
||||
│ │ └── 孔壁消失,孔口补面有效,结果保持单 Solid。
|
||||
│ ├── [x] [锥孔/沉孔 -> 局部重切或提前阻止]
|
||||
│ │ └── 简单圆锥解析重建,嵌入式锥孔/沉孔优先局部重切;复杂浅锥、螺纹孔和组合孔提前解释为受限能力。
|
||||
│ ├── [x] [孔编辑范围 -> 单个稳定孔特征]
|
||||
│ │ └── 单孔、锥孔、沉孔的一级关系修改已经作为孔阶段基线。
|
||||
│ ├── [~] [Ctrl 多选完整孔 -> 批量孔径 / 位置偏移]
|
||||
│ │ └── 第一版支持多个完整圆柱孔统一改孔径或按同一偏移移动;等距、阵列、同尺寸联动关系式仍放到第 8 阶段。
|
||||
│ └── [~] [关系式 -> FaceID.参数 = 表达式]
|
||||
│ └── 第一版先把关系式作为“目标值生成器”:用户输入 `Face87.直径 = Face85.直径` 或 `Face87.位置 = Face85.位置 + (0, 0, -3.5)`,程序先计算并回填当前参数表目标值,再复用现有一级编辑执行;跨对象批量队列、等距/阵列/同尺寸联动和持久约束求解后续再扩展。
|
||||
│
|
||||
├── 3. 槽 / 长圆孔,从孔扩展到组合切除特征
|
||||
│ ├── [x] [槽宽 -> 改槽宽]
|
||||
│ │ └── 两侧壁和圆弧端同步更新,结果仍能识别为槽/半孔。
|
||||
│ ├── [x] [槽深 -> 改盲槽深度]
|
||||
│ │ └── 槽底移动,槽口边界有效,不把母体切坏。
|
||||
│ ├── [x] [矩形槽/口袋长宽/深度 -> 局部重建矩形切除]
|
||||
│ │ └── 规则矩形槽/口袋可改长宽和深度;中心移动后端保留但参数表暂不开放。
|
||||
│ ├── [x] [弧长/弧角/开口角 -> 改圆弧槽段]
|
||||
│ │ └── 保持圆柱槽语义,重切后能回读目标弧长或角度。
|
||||
│ ├── [x] [总长/中心距 -> 改长圆槽长度]
|
||||
│ │ └── 自动或手动配对两个圆弧端,槽宽保持不变。
|
||||
│ ├── [x] [中心位置 -> 移动槽]
|
||||
│ │ └── 半圆槽和长圆槽都按特征整体补旧槽、切新槽。
|
||||
│ ├── [x] [复杂/交叉槽 -> 识别并快速阻止]
|
||||
│ │ └── 多个同半径槽端但没有稳定共享侧壁配对时,会标成复杂槽/多槽组,不在扫描列表和参数表里暴露槽宽、槽深、弧长、弧角或轴心入口。
|
||||
│ └── [ ] [复杂草图槽/跨复杂面槽 -> 稳定修改]
|
||||
│ └── 非圆柱槽、跨复杂面的槽暂不承诺稳定编辑;多槽组关联放到第 8 阶段。
|
||||
│
|
||||
├── 4. 凸台 / Boss,从切除特征扩展到加料特征
|
||||
│ ├── [x] [直径/半径 -> 改圆柱凸台直径]
|
||||
│ │ └── 移除旧凸台包络后重建,底部融合必须有效。
|
||||
│ ├── [x] [高度 -> 改凸台高度]
|
||||
│ │ └── 顶面移动,侧壁延伸或裁剪,目标高度可回读。
|
||||
│ ├── [x] [中心位置 -> 移动圆柱凸台]
|
||||
│ │ └── 旧凸台先移除,新凸台按目标轴心补回,不只移动侧壁。
|
||||
│ ├── [x] [矩形凸台/口袋高度/深度 -> 端面推拉]
|
||||
│ │ └── 规则矩形凸台顶面和规则矩形口袋底面可按高度/深度推拉,修改后会回读矩形拉伸尺寸。
|
||||
│ ├── [x] [矩形凸台/口袋长度/宽度 -> 局部重建矩形包络]
|
||||
│ │ └── 规则矩形凸台/口袋可先移除或补回旧包络,再按目标长宽重建;当前不在一次编辑中交换长宽方向。
|
||||
│ ├── [~] [矩形凸台/口袋中心位置 -> 后端保留,界面暂不开放]
|
||||
│ │ └── 中心移动后端仍有计划和回归覆盖,但客户参数表暂时只开放长宽、高度/深度这类更常用尺寸。
|
||||
│ ├── [x] [多台阶矩形凸台顶层规则台阶 -> 独立编辑]
|
||||
│ │ └── 选中多台阶凸台最上层规则矩形台阶的顶面时,可按独立矩形凸台修改长宽和高度。
|
||||
│ └── [x] [复杂多台阶凸台整体 -> 识别并快速阻止]
|
||||
│ └── 多台阶中间过渡面会标记为受限,扫描列表、参数表和计划阶段都不暴露伪可改入口;整组联动、异形凸台和复杂融合边界仍未作为稳定能力。
|
||||
│
|
||||
├── 5. 圆角 / 倒角,从主形体扩展到边修饰
|
||||
│ ├── [x] [Edge -> 添加圆角]
|
||||
│ │ └── 简单直线 Edge 可调用 OCCT 倒圆,失败时回滚。
|
||||
│ ├── [x] [Edge -> 添加倒角]
|
||||
│ │ └── 支持对称倒角、不等距倒角和距离+角度倒角的简单边场景。
|
||||
│ ├── [x] [距离 -> 改已有简单等距倒角]
|
||||
│ │ └── 选中简单倒角斜面时按短边和支撑面估算倒角距离,先移除旧倒角面,再在恢复出的锐边上重新倒角。
|
||||
│ ├── [x] [半径 -> 改已有简单圆角]
|
||||
│ │ └── 当前走 defeature + refillet,目标半径和结果邻域必须反查通过。
|
||||
│ ├── [x] [弧长 -> 调整已有圆角]
|
||||
│ │ └── 按当前圆弧角把目标弧长换算成目标半径,再走 defeature + refillet,结果弧长必须反查通过。
|
||||
│ ├── [x] [简单等半径圆角链 -> 整链半径/弧长修改]
|
||||
│ │ └── 2 到 4 个共享边连通的同半径圆角 Face 可一起 defeature,再对恢复出的多条锐边重新倒圆;弧长会按当前圆弧角换算成目标半径,结果至少反查到同数量目标半径/弧长圆角面。
|
||||
│ ├── [x] [变半径 / 复杂圆角链 -> 识别并快速阻止]
|
||||
│ │ └── 相邻不同半径圆角 Face、角部 blend 或超过 4 个面的同半径长链会阻止,不暴露为单个可改圆角参数。
|
||||
│ └── [ ] [复杂圆角链 / 变半径圆角 -> 通用稳定重建]
|
||||
│ └── 大链式 blend、变半径圆角和复杂角部补面同步重建仍未完成,暂不承诺修改。
|
||||
│
|
||||
├── 6. Edge 一级编辑,补齐底层直接改边能力
|
||||
│ ├── [x] [边长 -> 改直线 Edge 长度]
|
||||
│ │ └── 支持只改当前边、移动端面、相邻圆柱和缩放所属对象等显式建模意图。
|
||||
│ ├── [x] [起点/终点 -> 移动直线 Edge]
|
||||
│ │ └── 简单全平面模型可移动端点并重建相邻面;中心移动后端保留但参数表暂不开放。
|
||||
│ ├── [x] [圆形 Edge 半径/直径 -> 代理到相邻圆柱]
|
||||
│ │ └── 圆边优先复用孔/槽/凸台直径或轴心修改,而不是裸改曲线。
|
||||
│ ├── [x] [椭圆 Edge 主/小半径 -> 单轴缩放]
|
||||
│ │ └── 简单椭圆边可沿主轴或小轴做局部仿射,并校验采样半径。
|
||||
│ ├── [x] [移动端面 -> 简单盒体保持垂直边长语义]
|
||||
│ │ └── 直线 Edge 可把长度变化转成端面推拉,端面和同向边跟随,计划和结果都会反查端面/侧面垂直关系。
|
||||
│ ├── [x] [固定起点/终点/中心 -> Edge 长度锚点]
|
||||
│ │ └── 修改直线 Edge 长度时可选择固定起点、固定终点或固定中心,结果会按锚点反查。
|
||||
│ ├── [x] [平行/垂直 -> 端面推拉一级关系守门]
|
||||
│ │ └── 移动端面策略会确认直接相邻平面和移动端面可验证,执行前给出平面约束摘要,执行后反查垂直/平行关系。
|
||||
│ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别]
|
||||
│ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要;Face 事实图还会明确同域/共面碎片和一级同轴圆柱关系,供 UI 说明、计划守门和后续约束求解复用。
|
||||
│ ├── [~] [平行/垂直 -> 通用可选约束]
|
||||
│ │ └── Face 偏移可走受限拉伸/切除,Face 面内长度/宽度的“保持关系”会转成所属对象单向缩放,Edge 长度可走端面推拉;这些路径都会要求一级平面关系可验证并做结果反查。跨多面、跨特征的通用约束求解仍需继续泛化。
|
||||
│ ├── [x] [复杂曲线 Edge -> 可编辑入口守门]
|
||||
│ │ └── B-spline / Bezier / 其它复杂曲线不再在扫描列表或参数表里伪装成稳定“改长度”入口。
|
||||
│ └── [ ] [任意曲线 Edge -> 通用约束编辑]
|
||||
│ └── B-spline 边、复杂曲线边和多约束边不作为当前稳定目标。
|
||||
│
|
||||
├── 7. 壳体 / 解析曲面,补齐高价值但边界更窄的能力
|
||||
│ ├── [x] [壳体厚度 -> 局部拉伸/切除或缩放]
|
||||
│ │ └── 简单薄板和可识别相对平面已纳入 Face/壳体套件。
|
||||
│ ├── [x] [圆锥参考半径/半角 -> 简单圆锥解析重建]
|
||||
│ │ └── 嵌入式锥孔优先局部重切,复杂拔模面提前阻止。
|
||||
│ ├── [x] [球面半径 -> 缩放特征]
|
||||
│ │ └── 修改后仍需识别为目标球面。
|
||||
│ ├── [x] [环面主/小半径 -> 缩放特征]
|
||||
│ │ └── 修改后仍需识别为目标环面。
|
||||
│ └── [x] [完整抽壳/开口面编辑 -> 先识别开口薄壁上下文并明确限制]
|
||||
│ └── 开口薄壁盒/槽腔类区域会标出“完整抽壳/开口面编辑”受限;可编辑对象扫描会优先露出真实薄壁的壳体厚度入口,当前仍只开放局部壳体厚度、平面推拉或整体缩放,不做通用抽壳历史恢复。
|
||||
│
|
||||
└── 8. 二级 / 三级关系,从“局部能改”走向“关系能保持”
|
||||
├── [ ] [Face 二级传播 -> 孔底/槽底/台阶联动]
|
||||
│ └── 当前只自动处理一级共享边邻域,二级/三级传播放到本阶段。
|
||||
├── [x] [一级影响范围 -> 明确显示]
|
||||
│ └── 当前 Face/孔/槽/Edge 已能显示或记录一级边界、顶点和直接相邻 Face 事实。
|
||||
├── [~] [关联探测 -> 找相邻特征]
|
||||
│ └── 已能在有限共享边拓扑范围内找关联孔、槽、凸台等,并合并部分关联尺寸到参数表。
|
||||
├── [~] [共面/同域碎片 -> 同步处理]
|
||||
│ └── 同域合并已用于计划、扫描和历史定位;作为用户可选“共面同步修改”仍需完善。
|
||||
├── [~] [同轴圆柱 -> 保持同轴]
|
||||
│ └── 孔、槽、凸台和圆柱端盖已在部分路径保持同轴;跨特征同轴约束还不是通用能力。
|
||||
├── [ ] [平行/垂直平面 -> 保持方向关系]
|
||||
│ └── 用户选择保持平行/垂直时,相关面方向需要作为约束参与重建。
|
||||
├── [ ] [相切关系 -> 保持连续]
|
||||
│ └── 圆角、圆柱过渡和相切支撑面的连续性需要在传播计划里显式表达。
|
||||
├── [ ] [重复特征组 -> 同组联动]
|
||||
│ └── 孔组、阵列孔、多槽组和多个同尺寸凸台要能选择只改当前一个,或改同组全部。
|
||||
└── [ ] [传播冲突 -> 用户选择范围]
|
||||
└── 当二级/三级关系互相冲突时,列出冲突原因,允许只执行一级或扩展传播,不强行硬改。
|
||||
```
|
||||
|
||||
## 关系式实现流程
|
||||
|
||||
当前关系式先按“可见公式 + 目标值生成器”推进,不把第一版伪装成完整 CAD 约束求解器。
|
||||
|
||||
1. `[~]` 公式输入与显示:左侧 `特征参数` 下方增加 `关系式` 区域,只提供 `添加公式` 和已有公式列表。公式面向用户显示为 `Face87.直径 = Face85.直径`、`Face87.位置 = Face85.位置 + (0, 0, -3.5)` 这类通用 `对象ID.参数` 形式,不使用“孔1/孔2”这类临时命名。
|
||||
2. `[~]` 公式补全:输入 `Face87.` 后,按当前选中对象、当前多选对象和可见参数表提供参数候选;第一版优先覆盖 `直径`、`半径`、`位置`、`位置X/Y/Z`、`偏移`、`面内长度`、`面内宽度`、`长度` 等已经能稳定显示或计算的参数。
|
||||
3. `[~]` 公式计算:点击 `参数化建模` 时,先计算启用的关系式,把结果回填到当前可见参数表目标值;随后继续走现有单参数或多参数批量建模执行链路。第一版不直接跨多个未显示对象开新编辑任务。
|
||||
4. `[~]` 简单 Face ID 追踪:公式保存时记录引用 Face 的逻辑 ID 和轻量几何签名;编辑成功后,如果旧 `Face87` 在新模型中能唯一映射到新 `Face90`,就自动把公式显示更新为 `Face90.参数`。如果找不到、找到多个、Face 被拆分/合并或参数不再存在,则把公式标为失效,提示用户重新选择。
|
||||
5. `[ ]` 跨对象批量执行:后续把多个公式目标拆成执行队列,例如先算 `Face87.直径`、`Face92.直径`,再按对象逐个切换选择并执行已有建模动作;失败时停在明确对象和原因。
|
||||
6. `[ ]` 循环依赖和冲突处理:后续检查 `A = B`、`B = A`、同一目标被多个公式覆盖、同一参数既手工改又被公式驱动等冲突,给出可读提示。
|
||||
7. `[ ]` 持久化:后续把公式、对象签名、目标参数、建模意图和失效状态保存到项目配置或组件配置里,重新打开模型后能尝试恢复。
|
||||
8. `[ ]` 复杂拓扑追踪:后续再处理 `原 Face -> 多个新 Face`、`多个原 Face -> 一个新 Face`、特征删除、阵列孔重编号、装配 occurrence 等情况。
|
||||
|
||||
## 几何修改语义原则
|
||||
|
||||
所有可修改参数都要先明确“这次修改到底代表什么”,不能只因为程序能算出一个结果就直接放行。这个原则适用于 Face、Edge、孔、槽、凸台、圆角、Solid 和整件特征,不是 Edge 专属。比如修改 Face 可能代表 Face 拉伸/切除、局部补料、局部切削、平移所属对象或整体缩放;修改孔/槽可能代表重切侧壁、移动轴心、调整端面深度或重新配对长圆槽端部;修改圆角可能代表移除旧圆角后重建;修改 Edge 可能代表只动这条边、移动相关端面,或者高风险地缩放所属几何。
|
||||
@@ -637,7 +506,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
|
||||
- 能导出选中的零件。
|
||||
- 能撤销/重做编辑。
|
||||
- 先实现少量可控编辑操作,用于验证后续特征编辑路线。
|
||||
- 当前开发顺序固定按上面的“参数化编辑路线图”推进:先把交互规则和 Face 打稳,再进入孔、槽、凸台、圆角/倒角、Edge、壳体/解析曲面,最后扩展二级/三级关系;每一类都要先做到可操作、可解释、可测试,再进入下一类,避免每类只做一点导致测试分散。
|
||||
- 当前开发顺序固定按上面的 `SCDM-first 主路线` 推进:先打通 SCDM 识别、能力字典、参数表、执行、校验和 ID 续接闭环,再按 SCDM 已能结构化表达的 Face、孔、槽、凸台、圆角/倒角、阵列、壳体能力逐项适配。历史 OCCT/一级编辑套件继续作为兜底回归,不再作为新增能力主线。
|
||||
|
||||
## 能改哪些,一分钟版
|
||||
|
||||
@@ -684,11 +553,11 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
|
||||
- 使用 VTK 显示模型,并通过 PySide6/Qt 承载桌面界面。
|
||||
- 左侧操作面板按功能模块分组显示,并使用四边同色的轻量彩色边框和标题层级区分文件、模型树、选择、编辑、导出、显示、测量、候选、历史和属性信息。
|
||||
- 支持选择模式:
|
||||
- 零件
|
||||
- Part
|
||||
- Solid
|
||||
- Face
|
||||
- Edge
|
||||
- 特征
|
||||
- Feature
|
||||
- 鼠标悬停对象会以红色预高亮,真正选中后会以黄色高亮。
|
||||
- 普通 Face / 特征点选优先走轻量选择:只高亮命中的对象并显示基础参数,避免旋转、悬停、点选时触发同域面、端盖、底面等重计算;大模型会关闭鼠标悬停拾取高亮,只保留点击选择;真正执行编辑计划、扫描候选或历史定位时才做更深识别。
|
||||
- 大模型的 VTK 显示网格带有预算保护:即使编辑后请求较细的显示 deflection,也不会对上千个 Face 的 STEP 无限制生成百万级圆柱三角面;B-Rep 几何编辑和结果校验仍使用真实拓扑,显示网格只负责渲染和拾取。只显示少量 Face 或隔离选中对象时仍可局部细化这些 Face,不会触发全模型超细重网格。
|
||||
@@ -701,7 +570,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
|
||||
- Face / 特征的 `复制 ID` 也优先复制 `逻辑 Face ID`,属性区会同时显示 `逻辑选择 ID` 和 `当前拓扑 Face ID`;这样编辑后拓扑编号变化时,用户仍能用原来的逻辑 ID 回到同一个面区域。
|
||||
- `鼠标选择模式` 下拉框关闭时会把鼠标滚轮交还给左侧滚动面板,避免鼠标经过时误切换模式;打开下拉列表后仍可用滚轮滚动选项。
|
||||
- `按 ID 选择` 会在输入框和 `选择` 按钮之间同步显示当前鼠标选择模式。
|
||||
- `特征` 模式现在会把点到的Face解释为几何特征候选:
|
||||
- `Feature` 模式现在会把点到的Face解释为几何特征候选:
|
||||
- 平面会识别为可拉伸/切除平面候选。
|
||||
- 平面会尝试查找同一Solid内投影重叠的相对平面,用于估算壳体局部区域厚度。
|
||||
- 圆柱面会识别为圆柱孔候选、槽/半孔候选、圆角/倒圆候选、凸台/外圆候选或未明确圆柱特征。
|
||||
@@ -994,7 +863,7 @@ vertices: 3262
|
||||
|
||||
## 下一步要实现什么
|
||||
|
||||
当前已经有按阶段跑通的一级编辑基线,但这还不等于 CAD 级完成。后续按上面的“参数化编辑路线图”继续推进:`[x]` 项保持回归和真实 STEP 验证,`[~]` 项优先补守门、补失败解释、补更多模型样例,`[ ]` 项不能提前在 UI 里伪装成稳定能力。每一类先用小模型和默认大模型保持回归通过,再拿模型工程师的真实 STEP 失败项补识别、补守门、补局部重建策略。
|
||||
当前已经有按阶段跑通的本地一级编辑基线,但这还不等于 CAD 级完成。后续按上面的 `SCDM-first 主路线` 继续推进:`[x]` 项保持回归和真实 STEP 验证,`[~]` 项优先补 SCDM API 细节、守门、失败解释和更多真实模型样例,`[ ]` 项不能提前在 UI 里伪装成稳定能力。新增能力优先看 SCDM 能否返回结构化对象、当前值、可用命令和可校验结果;本地 OCCT 只作为兜底和已验证快路径保留。
|
||||
|
||||
当前整体验证基线:
|
||||
|
||||
@@ -1008,19 +877,19 @@ vertices: 3262
|
||||
- 2026-08-18,新增 `python scripts\verify_scdm_backend.py`、`python scripts\verify_scdm_status.py`、`python scripts\verify_scdm_probe_pipeline.py`、`python scripts\verify_scdm_edit_runner.py` 和 `python scripts\verify_scdm_result_validator.py`,覆盖 SCDM 后端发现、缓存、禁用开关、/RunScript 命令生成、左侧状态摘要、probe job/script 生成、raw->cache 映射、edit job/script/result/error 执行协议,以及结果文件检查、目标值回测、ID 映射和关系式重写;已接入 `--quick`。
|
||||
- 2026-08-18,本机 `D:\softwaresInstallDir\ANSYS Inc\v222\SCDM\SpaceClaim.exe` 已通过真实 `/RunScript` smoke;对 `assets/models/ICEPAK-NATURAL.stp` 的真实 probe 可打开模型并输出 13 个 Body、158 个 Face、396 条 Edge,396 条 Edge 均可回传长度、起终点和相邻 Face 数,其中 160 条圆弧/圆边可回传半径和圆心。当前 `StandardHoles.GetHoleFaces` 对该 STEP 返回 0 个标准孔面,因此已改为按同中心/同轴/同半径聚合圆柱碎片;真实 SCDM 已验证 face.offset、孔径和孔位置,输出 STEP 均可被 SCDM probe 回读。
|
||||
|
||||
Face 阶段的当前验收口径(R1 已收口):
|
||||
Face 阶段的当前验收口径(本地 OCCT 兜底基线,R1 已收口):
|
||||
|
||||
- 已验收:平面 Face 的 `偏移`,稳定矩形/简单全平面 Face 的 `面内长度`、`面内宽度`,以及壳体厚度、圆柱端盖/圆柱侧面高度这类挂在 Face 入口上的高频编辑;支持 `局部重建`、`拉伸/切除`、`移动特征`、`缩放特征` 和受限的 `保持关系`。`中心` 移动后端保留,但特征参数表暂不开放。
|
||||
- 已验收:Face 的 `一级关系` 定义为选中 Face 本身、必要的同域/共面碎片 Face、这些 Face 的边界 Edge/Vertex,以及与该区域共享边的直接相邻 Face。只共享顶点的对象、相邻 Face 再连出去的 Face 都不作为 R1 自动传播范围。
|
||||
- 已验收:选中平面 Face 时,程序会生成 `一级关系` 事实包,记录当前 Face 区域、同域/共面碎片、一级平行/垂直关系、一级同轴圆柱事实、边界 Edge/Vertex、共享边相邻 Face,并明确二级、三级关系暂不自动传播;这些事实用于计划、校验和诊断,不再作为特征参数行显示。
|
||||
- 可用但受限:带内孔平面、曲面 Solid 上的平面、自由曲面、非矩形面、复杂端盖、接近切穿的内切等场景会按明确守门执行;能稳定改就走隔离/回滚,不能稳定改就提前 blocked 并说明原因。
|
||||
- 未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建、孔底/槽底/台阶等二级/三级传播、跨特征约束求解和特征组联动;这些属于 R8,不算 R1 未完成项。
|
||||
- 未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建、手写孔底/槽底/台阶等二级/三级传播、跨特征约束求解和特征组联动;这些不再作为 SCDM-first 近期主线,不算 Face 兜底基线未完成项。
|
||||
- `python scripts\verify_first_level_edit_suites.py --stage face` 必须通过,集中运行 R1 Face 专项套件和 Face isolated worker。
|
||||
- `python scripts\verify_face_edit_suite.py` 必须通过,集中覆盖面内长度/面内宽度、中心、偏移、保持关系、壳体厚度、拉伸/切除、局部变形禁用、目标值保护、属性回读、逻辑 Face ID 保持,以及 UI 里标题/复制 ID/按 ID 选择是否优先使用逻辑 Face ID。
|
||||
- `python scripts\verify_property_editor_specs.py` 必须通过,确保平面 Face 的特征参数表使用 `面内长度`、`面内宽度`、`偏移` 这些用户可理解的名称,暂时隐藏 `中心`,且不把面积暴露成可编辑驱动参数,并递归检查提示文字里不再出现容易误解的旧词。
|
||||
- `python scripts\verify_face_first_level_topology.py` 必须通过,确保普通正方体 Face 的一级关系能识别 4 条边界 Edge、4 个边界 Vertex、4 个共享边相邻 Face,并证明局部移动后目标 Face 的边界 Vertex 与共享边 Edge 已到新位置、一级侧面跟随重建、二级底面不跟随移动。
|
||||
|
||||
孔/槽阶段的当前验收口径(R2/R3 已收口):
|
||||
孔/槽阶段的当前验收口径(本地 OCCT 兜底基线,R2/R3 已收口):
|
||||
|
||||
- 已验收:圆柱孔/盲孔的 `直径`、`半径`、`轴心`、`盲孔深度` 和 `封堵/删除孔`。孔径按同轴重切孔壁执行,轴心移动会先补旧孔再切新孔,盲孔深度会按加深切削或变浅补料处理,结果必须仍能回读目标孔特征。
|
||||
- 已验收:槽/半孔/长圆槽的 `槽宽`、`槽深`、`圆弧长度`、`圆弧角度`、`开口角度`、`轴心`、`总长度` 和 `中心距`。普通半圆槽按局部圆柱槽重切,长圆槽按两端圆弧配对后重建,修改后必须仍能识别为目标槽或半孔。
|
||||
@@ -1028,18 +897,18 @@ Face 阶段的当前验收口径(R1 已收口):
|
||||
- 已验收:孔/槽局部布尔和重建会走隔离 worker,修改后会校验 B-Rep、单 Solid、目标几何回读和逻辑 Face ID 保持;失败时回滚,不破坏原模型。
|
||||
- 可用但受限:当前只承诺单个稳定孔、单个盲孔、单个半圆槽和可稳定配对的长圆槽;锥孔/沉孔只处理简单解析或局部重切路径,复杂浅锥、螺纹孔、组合孔、底面识别不可靠的盲孔会提前阻止。
|
||||
- 可用但受限:复杂槽、交叉槽、多槽端配对不唯一时,只做识别和快速阻止,不在参数表暴露槽宽、槽深、弧长、弧角或轴心这些伪可改入口。
|
||||
- 未实现/不承诺:孔组、阵列孔、同尺寸孔联动、多槽组联动、螺纹孔语义编辑、任意草图槽、跨复杂面槽、孔底/槽底带动台阶的二级/三级传播;这些属于 R8 或后续阶段,不算 R2/R3 未完成项。
|
||||
- 未实现/不承诺:孔组、阵列孔、同尺寸孔联动、多槽组联动、螺纹孔语义编辑、任意草图槽、跨复杂面槽、手写孔底/槽底带动台阶的二级/三级传播;这些不再作为 SCDM-first 近期主线,不算孔/槽兜底基线未完成项。
|
||||
- `python scripts\verify_first_level_edit_suites.py --stage hole-slot` 必须通过,集中运行 R2/R3 孔槽专项套件。
|
||||
- `python scripts\verify_hole_slot_edit_suite.py` 必须通过,覆盖孔/槽一级圆柱拓扑、通孔/盲孔局部重建、半圆槽/长圆槽局部重建、隔离 worker、逻辑 Face ID 保持、识别摘要、一级事实图和属性表分组。
|
||||
- `python scripts\verify_cylindrical_first_level_topology.py` 必须通过,确保孔/槽类圆柱特征会暴露侧壁、边界 Edge/Vertex、直接相邻 Face、底面/开口面/槽边界面,并明确二级、三级关系暂不自动传播。
|
||||
- `python scripts\verify_hole_slot_isolated_edit.py` 必须通过,确保孔径、孔径缩放特征、孔轴心、孔封堵、盲孔深度、槽宽、槽宽缩放特征、槽深、弧长、弧角、槽轴心、长圆槽总长度和中心距都能通过隔离执行通道验证。
|
||||
|
||||
0~7 其它阶段的当前基线:
|
||||
本地 OCCT 兜底能力的当前基线:
|
||||
|
||||
- R2/R3 孔槽已经拆成独立验收口径;后续只在真实 STEP 失败项上补识别、补守门、补局部重建,不把孔组、阵列孔、多槽组联动提前混进 0~7。
|
||||
- R2/R3 孔槽已经拆成独立验收口径;后续只在真实 STEP 失败项上补识别、补守门、补局部重建。孔组、阵列孔、多槽组联动是否开放,改由上面的 SCDM capability 路线判断。
|
||||
- `python scripts\verify_first_level_edit_suites.py --stage edge` 覆盖 Edge 一级拓扑、长度建模意图、坐标修改、圆角/倒角、椭圆 Edge 和 isolated worker。任意曲线 Edge 通用约束编辑仍未开放。
|
||||
- `python scripts\verify_first_level_edit_suites.py --stage boss --stage round-chamfer --stage shell --stage analytic` 覆盖凸台、圆角/倒角、壳体厚度和解析曲面当前可用范围。复杂链式特征、通用抽壳历史恢复和跨特征传播仍不承诺。
|
||||
- R2/R3 孔槽收口后,下一步优先把 Edge、凸台、圆角/倒角、壳体和解析曲面也按“已验收 / 可用但受限 / 未实现”继续收口。
|
||||
- 这些阶段后续只作为本地兜底能力维护;新增客户入口优先从 SCDM capability 路线开放。
|
||||
|
||||
## 怎么使用
|
||||
|
||||
@@ -1056,11 +925,11 @@ Face 阶段的当前验收口径(R1 已收口):
|
||||
|
||||
左侧 `选择模式` 用来切换鼠标选择模式:
|
||||
|
||||
- `零件`:选择整个零件。
|
||||
- `Part`:选择整个零件。
|
||||
- `Solid`:选择实体。
|
||||
- `Face`:选择面。
|
||||
- `Edge`:选择边。
|
||||
- `特征`:按几何特征候选方式选择。平面会识别为可拉伸/切除平面候选;圆柱面会进一步识别为圆柱孔、槽/半孔、圆角/倒圆、凸台/外圆等候选。
|
||||
- `Feature`:按几何特征候选方式选择。平面会识别为可拉伸/切除平面候选;圆柱面会进一步识别为圆柱孔、槽/半孔、圆角/倒圆、凸台/外圆等候选。
|
||||
圆柱面候选会额外显示侧壁 Face、端面 Face、疑似底面 Face、开口端相邻 Face 和边界 Edge。
|
||||
- 鼠标悬停时,当前模式下将要选中的对象会显示为红色;真正选中后会显示为黄色。
|
||||
- 如果当前模式是 `Edge`,即使鼠标点到的是面,程序也会从这个面的边界里选取距离鼠标拾取点最近的Edge。
|
||||
@@ -1204,7 +1073,7 @@ Face 阶段的当前验收口径(R1 已收口):
|
||||
- 它会按目标直径或半径比例均匀缩放所属特征或 Solid,适合“整个所属对象一起放大/缩小”的意图。
|
||||
- 它不适合只想改孔壁的场景,因为高度、厚度和同一对象上的其它尺寸会一起变化。
|
||||
- `槽/半孔宽度` + `调整槽/半孔宽度`:
|
||||
- 先切换到 `特征` 选择模式并选择一个槽/半孔候选 Face,或从圆柱面候选表中选择局部圆柱形 `hole/groove candidate`。
|
||||
- 先切换到 `Feature` 选择模式并选择一个槽/半孔候选 Face,或从圆柱面候选表中选择局部圆柱形 `hole/groove candidate`。
|
||||
- 选中符合条件的槽/半孔后,`槽/半孔宽度` 会自动填入一个比当前槽宽估算略大的建议值。
|
||||
- 输入目标槽宽后点击 `调整槽/半孔宽度`。
|
||||
- 程序会用当前圆弧角度把目标槽宽换算成目标圆柱直径,再复用圆柱孔/槽调整流程。
|
||||
@@ -1325,7 +1194,7 @@ Face 阶段的当前验收口径(R1 已收口):
|
||||
- 这是“给直线边添加新圆角”,不是修改已有圆角面。
|
||||
- 由于 STEP 不带建模历史,某些边会被 OCCT 判断为不适合倒圆;失败时程序会提示并尝试恢复编辑前状态。
|
||||
- `圆角半径` / `圆角弧长` 的 `重建圆角`:
|
||||
- 先切换到 `特征` 选择模式并选择一个已有圆角/倒圆候选 Face,或从 `可编辑对象` 中点击 `修改已有圆角半径` 行进入。
|
||||
- 先切换到 `Feature` 选择模式并选择一个已有圆角/倒圆候选 Face,或从 `可编辑对象` 中点击 `修改已有圆角半径` 行进入。
|
||||
- 在当前选中对象表里修改 `圆角半径` 或 `圆角弧长`,并把 `建模意图` 设为 `重建圆角`。
|
||||
- 输入目标值后点击 `参数化建模`。
|
||||
- 程序会先检查该Face是否为 `round/fillet candidate`、是否识别到至少两个支撑Face、目标半径是否有效,以及当前零件是否为单Solid。
|
||||
@@ -1414,7 +1283,7 @@ Face 阶段的当前验收口径(R1 已收口):
|
||||
- `导出选中零件`:导出当前选中的零件。
|
||||
- `导出选中Solid`:先选中一个Solid,再导出该Solid。
|
||||
- `导出选中面区域`:先切换到 `Face` 选择模式并选中一个Face,再导出该Face;如果当前选择已经带有特征区域信息,则会导出这片已知区域。
|
||||
- `导出选中特征区域`:先切换到 `特征` 选择模式并选中特征,再导出当前特征高亮的Face集合。
|
||||
- `导出选中特征区域`:先切换到 `Feature` 选择模式并选中特征,再导出当前特征高亮的Face集合。
|
||||
- `导出选中Edge`:先切换到 `Edge` 选择模式并选中一个Edge,再导出该Edge。
|
||||
- `导出参数`:在 `特征参数` 表的 `输入参数` 列勾选需要外部驱动的尺寸行后,点击按钮会在项目根目录写出 `data.json`,并在 `nodes/` 下生成一个 FlowEditor 风格的参数化组件 `main.py`。组件脚本会把勾选的输入参数直接嵌入 `INPUT_PARAMETERS`,通过 `PARAMETERS / NODE_INFO` 暴露给节点设计器,并提供 `execute(inputs, params, context)` 调用入口;组件目录里不会额外生成 `data.json` 或 `step_edit_config.json`。执行后返回 `output_step`,指向修改后的 STEP 文件。
|
||||
- `检查导出质量`:检查当前选中对象;如果没有选中对象,就检查当前完整模型。
|
||||
@@ -1483,6 +1352,7 @@ pythonocc-step-editor/
|
||||
verify_hole_resize.py # 临时生成通孔/盲孔 STEP 并验证孔径/孔轴心/盲孔深度修改
|
||||
verify_property_editor_specs.py # 验证属性表不会把通用 Face 编辑混入孔/槽/凸台/圆角/解析曲面特征
|
||||
verify_property_card_editor_ui.py # 验证参数表 UI 能构建、检测目标值修改并展开完整参数
|
||||
verify_relation_formula_rules.py # 验证关系式自引用、重复目标和循环依赖会在添加阶段阻止
|
||||
verify_parametric_component_export.py # 验证导出参数会生成嵌入参数列表的组件 main.py,组件目录不额外写 data.json
|
||||
verify_scdm_backend.py # 验证 SCDM 后端发现、缓存、禁用开关和 /RunScript 烟测命令生成
|
||||
verify_scdm_status.py # 验证 SCDM 左侧状态摘要、后端来源、识别缓存计数和 OCCT/Analysis Situs 兜底说明
|
||||
@@ -1605,7 +1475,7 @@ git diff --check
|
||||
- `assets/screenshots/`:调试截图和问题截图,默认忽略,不提交。
|
||||
- `scripts/generate_cube_step.py`:生成 `assets/models/cube_10mm.step`。
|
||||
- `scripts/verify_first_level_acceptance_docs.py`:验证 README 里声明的一级验收口径、阶段命令和脚本清单仍然能对上 `verify_first_level_edit_suites.py`,避免目标文档和实际验证入口脱节。
|
||||
- `scripts/verify_first_level_edit_suites.py`:一级编辑总验证入口。默认按 Face、孔/槽、Edge、凸台、圆角/倒角、壳体和解析曲面阶段串起现有专项套件;`--quick` 跑烟测、属性表、参数表 UI、SCDM 后端发现底座、SCDM 状态摘要、SCDM probe/cache 假数据管线、SCDM edit job/runner 协议、SCDM result validator、ICEPAK 真实 STEP 同域圆柱孔、一级事实图、关联探测、显示网格预算和验收文档一致性;`--stage edge` 这类参数可只跑某个阶段。
|
||||
- `scripts/verify_first_level_edit_suites.py`:一级编辑总验证入口。默认按 Face、孔/槽、Edge、凸台、圆角/倒角、壳体和解析曲面阶段串起现有专项套件;`--quick` 跑烟测、属性表、参数表 UI、关系式规则、SCDM 后端发现底座、SCDM 状态摘要、SCDM probe/cache 假数据管线、SCDM edit job/runner 协议、SCDM result validator、ICEPAK 真实 STEP 同域圆柱孔、一级事实图、关联探测、显示网格预算和验收文档一致性;`--stage edge` 这类参数可只跑某个阶段。
|
||||
- `scripts/verify_scdm_backend.py`:纯 Python 检查 SCDM 后端发现和缓存逻辑,不依赖 OCC;覆盖环境变量、常见安装目录、`local/scdm_backend.json`、禁用开关和 `/RunScript` 命令生成。
|
||||
- `scripts/verify_scdm_status.py`:纯 Python 检查 SCDM 运行状态摘要,不依赖 OCC;覆盖未配置、已配置、后台识别中、识别成功、识别失败、cache 失效和缓存来源显示。
|
||||
- `scripts/verify_scdm_probe_pipeline.py`:纯 Python 检查 SCDM probe job、RunScript 脚本生成、raw 识别结果到产品化 cache 的映射;覆盖第一批 `hole.diameter`、`hole.position` 和 `face.offset` 能力。
|
||||
|
||||
@@ -47,27 +47,30 @@ def _verify_readme_mentions(readme: str) -> None:
|
||||
"当前整体验证基线",
|
||||
"不等于 CAD 级完成",
|
||||
"Face 阶段的当前验收口径",
|
||||
"参数化编辑路线图",
|
||||
"用户最常用优先 > B-Rep 上稳定可实现 > 参数语义清楚",
|
||||
"`[x]` 已实现",
|
||||
"`[~]` 部分实现/进行中",
|
||||
"`[ ]` 未实现",
|
||||
"`[x]` 不是“所有 CAD 形态都能改”",
|
||||
"R1 Face 是当前主线的第一阶段收口对象",
|
||||
"R8 的二级/三级传播、跨特征约束和特征组联动不算在 0~7 完成度里",
|
||||
"STEP/B-Rep 参数化编辑主线",
|
||||
"[不能修改 -> 立即说明原因]",
|
||||
"[一级影响范围 -> 明确显示]",
|
||||
"Face 阶段的当前验收口径(R1 已收口)",
|
||||
"SCDM-first 主路线",
|
||||
"核心路线只保留下面这一棵树",
|
||||
"SCDM 内部如何处理相邻面、圆角链、二级/三级拓扑传播,交给 SCDM",
|
||||
"本软件不再把手写一级、二级、三级传播当成新增能力主线",
|
||||
"`[x]` 已适配",
|
||||
"`[~]` 部分适配",
|
||||
"`[ ]` 待适配",
|
||||
"[scdm_probe_job.json -> /RunScript 扫描 STEP]",
|
||||
"[scdm_feature_cache.json -> 映射为本软件能力字典和中文参数]",
|
||||
"[参数化建模 -> 多个目标值统一提交,不再每行一个操作按钮]",
|
||||
"[Face 偏移 -> face.offset / OffsetFaces]",
|
||||
"[槽宽 -> slot.width / OffsetFaces",
|
||||
"[槽深 -> slot.depth / Move 或 OffsetFaces",
|
||||
"[本地 OCCT -> 只保留已验证兜底能力,不再作为新主线扩展]",
|
||||
"Face 阶段的当前验收口径(本地 OCCT 兜底基线,R1 已收口)",
|
||||
"已验收:平面 Face 的 `偏移`,稳定矩形/简单全平面 Face 的 `面内长度`、`面内宽度`",
|
||||
"未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建",
|
||||
"孔/槽阶段的当前验收口径(R2/R3 已收口)",
|
||||
"孔/槽阶段的当前验收口径(本地 OCCT 兜底基线,R2/R3 已收口)",
|
||||
"已验收:圆柱孔/盲孔的 `直径`、`半径`、`轴心`、`盲孔深度`",
|
||||
"已验收:槽/半孔/长圆槽的 `槽宽`、`槽深`、`圆弧长度`",
|
||||
"未实现/不承诺:孔组、阵列孔、同尺寸孔联动、多槽组联动",
|
||||
"2026-08-11,在 `pyocc` 环境下已通过 `python scripts\\verify_first_level_edit_suites.py --stage hole-slot`",
|
||||
"覆盖 R2/R3 孔槽专项套件、隔离执行、逻辑 Face ID 保持和孔槽阶段收口口径",
|
||||
"0~7 其它阶段的当前基线",
|
||||
"本地 OCCT 兜底能力的当前基线",
|
||||
"verify_first_level_edit_suites.py --quick",
|
||||
"verify_first_level_edit_suites.py --stage face",
|
||||
"verify_first_level_edit_suites.py --stage hole-slot",
|
||||
@@ -92,42 +95,56 @@ def _verify_readme_mentions(readme: str) -> None:
|
||||
|
||||
|
||||
def _verify_roadmap_scope(readme: str) -> None:
|
||||
start_marker = "STEP/B-Rep 参数化编辑主线"
|
||||
end_marker = "└── 8. 二级 / 三级关系"
|
||||
start_marker = "SCDM-first 主路线"
|
||||
end_marker = "└── 7. 交付与兜底边界"
|
||||
start = readme.find(start_marker)
|
||||
end = readme.find(end_marker)
|
||||
_assert(start >= 0 and end > start, "README roadmap should contain a 0~7 active scope before stage 8")
|
||||
_assert(start >= 0 and end > start, "README should contain a single SCDM-first roadmap before fallback boundary")
|
||||
active_scope = readme[start:end]
|
||||
deferred_scope = readme[end:]
|
||||
|
||||
required_active_fragments = (
|
||||
"├── 0. 先让用户知道“能不能改”",
|
||||
"│ ├── [x] [不能修改 -> 立即说明原因]",
|
||||
"│ └── [x] [路线图 -> 验收脚本守门]",
|
||||
"├── 1. 平面 Face,第一条主线",
|
||||
"├── 2. 孔,第二条主线",
|
||||
"├── 3. 槽 / 长圆孔,从孔扩展到组合切除特征",
|
||||
"├── 4. 凸台 / Boss,从切除特征扩展到加料特征",
|
||||
"├── 5. 圆角 / 倒角,从主形体扩展到边修饰",
|
||||
"├── 6. Edge 一级编辑,补齐底层直接改边能力",
|
||||
"├── 7. 壳体 / 解析曲面,补齐高价值但边界更窄的能力",
|
||||
"├── 0. 后端发现与可用性",
|
||||
"│ ├── [x] [自动发现 SpaceClaim.exe -> 缓存路径、来源、版本和验证结果]",
|
||||
"├── 1. SCDM 识别与缓存",
|
||||
"│ ├── [x] [scdm_probe_job.json -> /RunScript 扫描 STEP]",
|
||||
"│ ├── [x] [scdm_feature_cache.json -> 映射为本软件能力字典和中文参数]",
|
||||
"├── 2. 能力字典、参数表和用户入口",
|
||||
"├── 3. SCDM 参数化建模执行",
|
||||
"├── 4. 结果校验、回滚和 ID 续接",
|
||||
"├── 5. 当前已开放或正在开放的 SCDM 能力",
|
||||
"[阵列间距 -> pattern.spacing / Move,整体阵列保持中心不变并等距重排;安全范围由支撑面动态计算,不针对 Face92 写死]",
|
||||
"[局部间距 -> pattern.segment_spacing / Move,按“FaceA-FaceB 间距”或“零件A-零件B 间距”修改相邻段;已支持固定前项移动后侧、固定后项移动前侧、两侧均分保持中心]",
|
||||
"├── 6. 下一批只按 SCDM 能力适配",
|
||||
"[阵列实例位置 -> pattern.instance_position]",
|
||||
"[壳体厚度 -> shell.thickness,薄壁候选配对已进 cache",
|
||||
)
|
||||
for fragment in required_active_fragments:
|
||||
_assert(fragment in active_scope, f"README active roadmap missing: {fragment}")
|
||||
|
||||
forbidden_active_fragments = (
|
||||
"SCDM-first 统一实施路线",
|
||||
"SCDM-first Capability 适配路线图",
|
||||
"SCDM-first Capability 适配路线",
|
||||
"├── 6. Edge 一级编辑,补齐底层直接改边能力",
|
||||
"└── 8. 二级 / 三级关系",
|
||||
"[Face 二级传播",
|
||||
"[孔组 ->",
|
||||
"多槽组 ->",
|
||||
"二级传播 ->",
|
||||
"三级传播 ->",
|
||||
"二级 / 三级关系",
|
||||
)
|
||||
for fragment in forbidden_active_fragments:
|
||||
_assert(fragment not in active_scope, f"README 0~7 roadmap should defer this to stage 8: {fragment}")
|
||||
_assert(fragment not in active_scope, f"README SCDM-first roadmap should not keep old route scope: {fragment}")
|
||||
|
||||
_assert("[Face 二级传播 -> 孔底/槽底/台阶联动]" in deferred_scope, "README should keep Face deeper propagation in stage 8")
|
||||
_assert("0~7 不混入二级/三级传播任务" in active_scope, "README should document the active roadmap guard")
|
||||
_assert("[Analysis Situs -> 只做辅助定位、兜底识别和开源对照]" in deferred_scope, "README should keep Analysis Situs as auxiliary boundary")
|
||||
_assert("[本地 OCCT -> 只保留已验证兜底能力,不再作为新主线扩展]" in deferred_scope, "README should keep OCCT as fallback boundary")
|
||||
_assert("[SCDM 之外的新能力 -> 等 SCDM 能力适配完再评估]" in deferred_scope, "README should defer non-SCDM expansion")
|
||||
|
||||
forbidden_global_fragments = (
|
||||
"SCDM-first 统一实施路线",
|
||||
"SCDM-first Capability 适配路线图",
|
||||
"SCDM-first Capability 适配路线",
|
||||
)
|
||||
for fragment in forbidden_global_fragments:
|
||||
_assert(fragment not in readme, f"README should keep only one SCDM-first route, found old title: {fragment}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -70,6 +70,7 @@ QUICK_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("Smoke test", ("main.py", "--smoke-test")),
|
||||
("Property editor specs", ("verify_property_editor_specs.py",)),
|
||||
("Property table editor UI", ("verify_property_card_editor_ui.py",)),
|
||||
("Relation formula rules", ("verify_relation_formula_rules.py",)),
|
||||
("Parametric component export", ("verify_parametric_component_export.py",)),
|
||||
("SCDM backend discovery", ("verify_scdm_backend.py",)),
|
||||
("SCDM runtime status", ("verify_scdm_status.py",)),
|
||||
|
||||
@@ -49,6 +49,7 @@ from step_editor.window_state import (
|
||||
RELATION_FORMULA_OBJECT_COMPLETION_LIMIT,
|
||||
WindowStateMixin,
|
||||
)
|
||||
from step_editor.ui_helpers import _selection_mode_label, _selection_mode_value
|
||||
|
||||
|
||||
class _StatusBar:
|
||||
@@ -86,7 +87,23 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "empty"
|
||||
self.scdm_feature_cache_message = ""
|
||||
self.scdm_edit_runner_ready = {"face.offset", "hole.diameter", "hole.position", "slot.position", "boss.position"}
|
||||
self.scdm_edit_runner_ready = {
|
||||
"face.offset",
|
||||
"hole.diameter",
|
||||
"hole.position",
|
||||
"feature.fill",
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"slot.position",
|
||||
"boss.diameter",
|
||||
"boss.height",
|
||||
"boss.position",
|
||||
"round.radius",
|
||||
"chamfer.distance",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"pattern.spacing",
|
||||
"pattern.segment_spacing",
|
||||
}
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.object_edit_box = self
|
||||
@@ -1560,6 +1577,65 @@ def _assert_scdm_selection_diagnostics() -> None:
|
||||
_assert("偏移" in str(info.get("scdm_selection_enabled_capabilities")), f"SCDM enabled capability diagnostic missing: {info}")
|
||||
|
||||
|
||||
def _assert_solid_selection_does_not_expand_face_scdm_specs() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_edit_runner_ready = {"face.offset"}
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": "face:0",
|
||||
"objectType": "face",
|
||||
"geometrySignature": {"faceIds": [0], "surfaceType": "plane", "planeOffset": 0.0},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "face.offset",
|
||||
"displayName": "偏移",
|
||||
"currentValue": 0.0,
|
||||
"valueKind": "number",
|
||||
"defaultIntent": "推拉平面",
|
||||
"backendOperation": "pull_face_offset",
|
||||
"postCheck": "target_face_offset",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"objectId": "face:1",
|
||||
"objectType": "face",
|
||||
"geometrySignature": {"faceIds": [1], "surfaceType": "plane", "planeOffset": -5.0},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "face.offset",
|
||||
"displayName": "偏移",
|
||||
"currentValue": -5.0,
|
||||
"valueKind": "number",
|
||||
"defaultIntent": "推拉平面",
|
||||
"backendOperation": "pull_face_offset",
|
||||
"postCheck": "target_face_offset",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
probe.selected_kind = "face"
|
||||
probe.selected_face_id = 0
|
||||
face_specs = probe._scdm_property_specs_for_selection()
|
||||
_assert(
|
||||
len([item for item in face_specs if item.get("scdm_capability_key") == "face.offset"]) == 1,
|
||||
f"Face selection should show the selected Face SCDM offset only: {face_specs}",
|
||||
)
|
||||
|
||||
probe.selected_kind = "solid"
|
||||
probe.selected_face_id = None
|
||||
probe.selected_solid_id = 0
|
||||
probe.model = SimpleNamespace(face_solid_ids=[0, 0])
|
||||
solid_specs = probe._scdm_property_specs_for_selection()
|
||||
_assert(
|
||||
solid_specs == [],
|
||||
f"Solid selection should not expand every child Face SCDM offset into the parameter table: {solid_specs}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_operation_record_backend_sources() -> None:
|
||||
class _OperationRecordProbe(WindowActionMixin):
|
||||
pass
|
||||
@@ -1718,6 +1794,10 @@ def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
_assert("scdm_local_face_signatures" in window_body, "window SCDM preload should use the lightweight model signature API")
|
||||
_assert("quick_face_info" not in window_body, "window SCDM preload must not call quick_face_info for every Face")
|
||||
loaded_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_apply_loaded_model_result")
|
||||
_assert(
|
||||
"_restore_scdm_feature_cache_from_disk()" in loaded_body,
|
||||
"STEP load should restore a matching SCDM disk cache before launching a new probe",
|
||||
)
|
||||
_assert(
|
||||
"_defer_large_model_recognition_preloads" in loaded_body,
|
||||
"large model loads should defer full external recognition preloads by default",
|
||||
@@ -1727,6 +1807,10 @@ def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
"SCDM edit-result reloads should still be able to force cache refresh for validation",
|
||||
)
|
||||
preload_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_start_scdm_probe_preload")
|
||||
_assert(
|
||||
"_current_scdm_feature_cache_matches_loaded_step()" in preload_body,
|
||||
"SCDM preload should skip relaunching SpaceClaim when a current cache is already installed",
|
||||
)
|
||||
_assert(
|
||||
"local_face_signatures = self._scdm_local_face_signatures()" not in preload_body,
|
||||
"SCDM preload must not build all local Face signatures on the UI thread",
|
||||
@@ -1866,6 +1950,53 @@ def _assert_large_planar_offset_prefers_local_backend() -> None:
|
||||
_assert("SCDM" in str(probe.scdm_selection_status_message), "backend preference should explain that SCDM was bypassed")
|
||||
|
||||
|
||||
def _assert_scdm_target_values_use_backend_units() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
offset = probe._scdm_property_target_value(
|
||||
{"scdm_capability_key": "face.offset", "value_type": "number", "scdm_unit_scale": 0.001},
|
||||
"2.5",
|
||||
)
|
||||
_assert(abs(float(offset) - 0.0025) <= 1.0e-12, f"SCDM numeric targets should be converted back to backend units: {offset}")
|
||||
position = probe._scdm_property_target_value(
|
||||
{"scdm_capability_key": "hole.position", "value_type": "vector3", "scdm_unit_scale": 0.001},
|
||||
"(1, 2, 3)",
|
||||
)
|
||||
_assert(position == [0.001, 0.002, 0.003], f"SCDM vector targets should be converted back to backend units: {position}")
|
||||
spacing_spec = {"label": "阵列间距", "value_type": "positive", "max_value": 1.3571428571428572}
|
||||
_assert(
|
||||
not probe._property_target_validation_error(spacing_spec, "1.1"),
|
||||
"pattern spacing targets within the support face range should be accepted",
|
||||
)
|
||||
spacing_error = probe._property_target_validation_error(spacing_spec, "2")
|
||||
_assert(
|
||||
"阵列间距" in spacing_error and "1.35714" in spacing_error,
|
||||
f"pattern spacing beyond the support face range should be blocked in the UI: {spacing_error}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_selection_mode_labels_are_english() -> None:
|
||||
expected = {
|
||||
"Feature": "Feature",
|
||||
"Face": "Face",
|
||||
"Edge": "Edge",
|
||||
"Solid": "Solid",
|
||||
"Part": "Part",
|
||||
}
|
||||
for value, label in expected.items():
|
||||
_assert(_selection_mode_label(value) == label, f"selection mode {value} should display as English: {_selection_mode_label(value)}")
|
||||
_assert(_selection_mode_value(label) == value, f"selection mode label {label} should resolve to {value}")
|
||||
for legacy, value in {
|
||||
"智能特征": "Feature",
|
||||
"特征": "Feature",
|
||||
"面": "Face",
|
||||
"边": "Edge",
|
||||
"实体": "Solid",
|
||||
"零件": "Part",
|
||||
"装配零件": "Part",
|
||||
}.items():
|
||||
_assert(_selection_mode_value(legacy) == value, f"legacy selection label {legacy} should resolve to {value}")
|
||||
|
||||
|
||||
def _assert_background_load_uses_worker() -> None:
|
||||
body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_load_step_background_or_sync")
|
||||
_assert("LoadWorker(action)" in body, "background STEP load should run through LoadWorker")
|
||||
@@ -1891,6 +2022,8 @@ def main() -> int:
|
||||
not top_level_label_probe.shown_labels,
|
||||
f"property labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
|
||||
)
|
||||
_assert_scdm_target_values_use_backend_units()
|
||||
_assert_selection_mode_labels_are_english()
|
||||
_assert_property_table_editor(probe)
|
||||
_assert(probe.current_capability_button.text() == "软件进度", "software progress should be a compact button")
|
||||
_assert("当前支持" in probe.current_capability_button.toolTip(), "software progress button tooltip did not show supported areas")
|
||||
@@ -1952,6 +2085,7 @@ def main() -> int:
|
||||
_assert_relation_formula_ids_follow_model_remap()
|
||||
_assert_mouse_selection_guards()
|
||||
_assert_scdm_selection_diagnostics()
|
||||
_assert_solid_selection_does_not_expand_face_scdm_specs()
|
||||
_assert_operation_record_backend_sources()
|
||||
_assert_scdm_auto_prompt()
|
||||
_assert_property_ui_reroute_guards()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.relation_formulas import ( # noqa: E402
|
||||
RelationFormulaError,
|
||||
parse_relation_formula,
|
||||
validate_relation_formula_graph,
|
||||
)
|
||||
|
||||
|
||||
def _parse_many(texts: list[str]):
|
||||
return [parse_relation_formula(text) for text in texts]
|
||||
|
||||
|
||||
def _assert_ok(texts: list[str]) -> None:
|
||||
validate_relation_formula_graph(_parse_many(texts))
|
||||
|
||||
|
||||
def _assert_fails(texts: list[str], expected_fragment: str) -> None:
|
||||
try:
|
||||
validate_relation_formula_graph(_parse_many(texts))
|
||||
except RelationFormulaError as exc:
|
||||
message = str(exc)
|
||||
if expected_fragment not in message:
|
||||
raise AssertionError(f"expected {expected_fragment!r} in error message, got {message!r}") from exc
|
||||
return
|
||||
raise AssertionError(f"expected relation formulas to fail: {texts!r}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_assert_ok(["Face87.直径 = Face87.半径 + 0.1"])
|
||||
_assert_ok(["Face87.位置 = Face85.位置 + (0, 0, -3.5)"])
|
||||
_assert_ok(["Face85.直径 = Face87.半径", "Face11.直径 = Face85.半径"])
|
||||
|
||||
_assert_fails(["Face87.直径 = Face87.直径 + 0.1"], "不能引用自身")
|
||||
_assert_fails(["Face85.直径 = Face87.半径", "Face85.直径 = Face11.半径"], "同一目标参数")
|
||||
_assert_fails(["Face85.直径 = Face87.直径", "Face87.直径 = Face85.直径"], "循环依赖")
|
||||
_assert_fails(
|
||||
[
|
||||
"Face1.位置 = Face2.位置",
|
||||
"Face2.位置 = Face3.位置",
|
||||
"Face3.位置 = Face1.位置",
|
||||
],
|
||||
"循环依赖",
|
||||
)
|
||||
|
||||
print("relation formula rules ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -168,19 +168,40 @@ def main() -> int:
|
||||
"change_hole_diameter",
|
||||
"move_hole_axis",
|
||||
"move_slot",
|
||||
"change_slot_width",
|
||||
"change_slot_depth",
|
||||
"move_boss",
|
||||
"change_boss_height",
|
||||
"change_boss_diameter",
|
||||
"change_round_radius",
|
||||
"change_chamfer_distance",
|
||||
"change_pattern_spacing",
|
||||
"change_pattern_segment_spacing",
|
||||
"segment_before",
|
||||
"segment_split",
|
||||
"pull_face_offset",
|
||||
"fill_feature",
|
||||
"delete_round_or_chamfer",
|
||||
"StandardHoles.ModifyHoleRadius",
|
||||
"ConstantRound.ModifyRadius",
|
||||
"Chamfer.ModifyDistance",
|
||||
"OffsetFaces.Execute",
|
||||
"Move.Translate",
|
||||
"MoveOptions",
|
||||
"ConstantRoundOptions",
|
||||
"ChamferOptions",
|
||||
"OffsetFaceOptions",
|
||||
"FillOptions",
|
||||
"FillMode",
|
||||
"Delete.Execute",
|
||||
"DocumentSave",
|
||||
"scdmFaceLocators",
|
||||
"heightFaceLocators",
|
||||
"depthFaceLocators",
|
||||
"diameterFaceLocators",
|
||||
"patternInstances",
|
||||
"bodyLocators",
|
||||
"instanceKind",
|
||||
"result.json",
|
||||
"error.json",
|
||||
):
|
||||
@@ -197,6 +218,10 @@ def main() -> int:
|
||||
"faceOrdinals": [30, 31, 32],
|
||||
"globalFaceOrdinal": 40,
|
||||
"globalFaceOrdinals": [40, 41, 42],
|
||||
"width": 2.0,
|
||||
"depth": 1.5,
|
||||
"depthAxis": [0.0, 0.0, -1.0],
|
||||
"depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
}
|
||||
@@ -214,6 +239,37 @@ def main() -> int:
|
||||
_assert(slot_job.get("target", {}).get("backendOperation") == "move_slot", f"slot.position should route to move_slot: {slot_job}")
|
||||
_assert(slot_job.get("target", {}).get("value") == [1.0, 2.0, 5.0], f"slot.position target should be vector3: {slot_job}")
|
||||
_assert(slot_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [30, 31, 32], f"slot faces should be preserved: {slot_job}")
|
||||
slot_width_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-width-prepared",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
target_value="2.5",
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
)
|
||||
_assert(slot_width_prepared.get("ok") is True, f"slot.width should be productized and prepare an edit job: {slot_width_prepared}")
|
||||
slot_width_job = read_json(slot_width_prepared["job_path"])
|
||||
_assert(slot_width_job.get("target", {}).get("backendOperation") == "change_slot_width", f"slot.width should route to change_slot_width: {slot_width_job}")
|
||||
_assert(slot_width_job.get("target", {}).get("value") == 2.5, f"slot.width target should be numeric: {slot_width_job}")
|
||||
_assert(slot_width_job.get("object", {}).get("geometrySignature", {}).get("width") == 2.0, f"slot width signature should be preserved: {slot_width_job}")
|
||||
slot_depth_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-depth-prepared",
|
||||
backend=backend,
|
||||
capability_key="slot.depth",
|
||||
target_value="2.0",
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
)
|
||||
_assert(slot_depth_prepared.get("ok") is True, f"slot.depth should be productized and prepare an edit job: {slot_depth_prepared}")
|
||||
slot_depth_job = read_json(slot_depth_prepared["job_path"])
|
||||
_assert(slot_depth_job.get("target", {}).get("backendOperation") == "change_slot_depth", f"slot.depth should route to change_slot_depth: {slot_depth_job}")
|
||||
_assert(slot_depth_job.get("target", {}).get("value") == 2.0, f"slot.depth target should be numeric: {slot_depth_job}")
|
||||
slot_depth_signature = slot_depth_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(slot_depth_signature.get("depth") == 1.5, f"slot depth signature should be preserved: {slot_depth_job}")
|
||||
_assert(slot_depth_signature.get("depthFaceLocators"), f"slot depth bottom face locator should be preserved: {slot_depth_job}")
|
||||
_assert(slot_depth_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_depth_job}")
|
||||
|
||||
boss_signature = {
|
||||
"objectType": "cylindrical_boss",
|
||||
@@ -223,6 +279,10 @@ def main() -> int:
|
||||
"faceOrdinals": [50, 51, 52],
|
||||
"globalFaceOrdinal": 70,
|
||||
"globalFaceOrdinals": [70, 71, 72],
|
||||
"height": 4.0,
|
||||
"diameter": 3.0,
|
||||
"heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}],
|
||||
"diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
}
|
||||
@@ -240,12 +300,369 @@ def main() -> int:
|
||||
_assert(boss_job.get("target", {}).get("backendOperation") == "move_boss", f"boss.position should route to move_boss: {boss_job}")
|
||||
_assert(boss_job.get("target", {}).get("value") == [2.0, 0.0, 2.0], f"boss.position target should be vector3: {boss_job}")
|
||||
_assert(boss_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [50, 51, 52], f"boss faces should be preserved: {boss_job}")
|
||||
boss_height_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-height-prepared",
|
||||
backend=backend,
|
||||
capability_key="boss.height",
|
||||
target_value="5.5",
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
)
|
||||
_assert(boss_height_prepared.get("ok") is True, f"boss.height should be productized and prepare an edit job: {boss_height_prepared}")
|
||||
boss_height_job = read_json(boss_height_prepared["job_path"])
|
||||
_assert(boss_height_job.get("target", {}).get("backendOperation") == "change_boss_height", f"boss.height should route to change_boss_height: {boss_height_job}")
|
||||
_assert(boss_height_job.get("target", {}).get("value") == 5.5, f"boss.height target should be numeric: {boss_height_job}")
|
||||
boss_height_signature = boss_height_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(boss_height_signature.get("height") == 4.0, f"boss height signature should be preserved: {boss_height_job}")
|
||||
_assert(boss_height_signature.get("heightFaceLocators"), f"boss height top face locator should be preserved: {boss_height_job}")
|
||||
boss_diameter_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-diameter-prepared",
|
||||
backend=backend,
|
||||
capability_key="boss.diameter",
|
||||
target_value="4.5",
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
)
|
||||
_assert(boss_diameter_prepared.get("ok") is True, f"boss.diameter should be productized and prepare an edit job: {boss_diameter_prepared}")
|
||||
boss_diameter_job = read_json(boss_diameter_prepared["job_path"])
|
||||
_assert(boss_diameter_job.get("target", {}).get("backendOperation") == "change_boss_diameter", f"boss.diameter should route to change_boss_diameter: {boss_diameter_job}")
|
||||
_assert(boss_diameter_job.get("target", {}).get("value") == 4.5, f"boss.diameter target should be numeric: {boss_diameter_job}")
|
||||
boss_diameter_signature = boss_diameter_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(boss_diameter_signature.get("diameter") == 3.0, f"boss diameter signature should be preserved: {boss_diameter_job}")
|
||||
_assert(boss_diameter_signature.get("diameterFaceLocators"), f"boss diameter side face locator should be preserved: {boss_diameter_job}")
|
||||
|
||||
round_signature = {
|
||||
"objectType": "round",
|
||||
"faceIds": [60],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 60,
|
||||
"globalFaceOrdinal": 80,
|
||||
"center": [1.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"radius": 0.5,
|
||||
"roundType": "ConstantRound",
|
||||
"isConstantRound": True,
|
||||
}
|
||||
round_radius_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-radius-prepared",
|
||||
backend=backend,
|
||||
capability_key="round.radius",
|
||||
target_value="0.75",
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
)
|
||||
_assert(round_radius_prepared.get("ok") is True, f"round.radius should be productized and prepare an edit job: {round_radius_prepared}")
|
||||
round_radius_job = read_json(round_radius_prepared["job_path"])
|
||||
_assert(round_radius_job.get("target", {}).get("backendOperation") == "change_round_radius", f"round.radius should route to change_round_radius: {round_radius_job}")
|
||||
_assert(round_radius_job.get("target", {}).get("value") == 0.75, f"round.radius target should be numeric: {round_radius_job}")
|
||||
round_radius_signature = round_radius_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(round_radius_signature.get("isConstantRound") is True, f"round.radius should preserve constant round evidence: {round_radius_job}")
|
||||
_assert(round_radius_signature.get("radius") == 0.5, f"round.radius should preserve current radius: {round_radius_job}")
|
||||
round_delete_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-delete-prepared",
|
||||
backend=backend,
|
||||
capability_key="feature.delete_round_or_chamfer",
|
||||
target_value="",
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
)
|
||||
_assert(round_delete_prepared.get("ok") is True, f"round/chamfer delete should be productized and prepare an edit job: {round_delete_prepared}")
|
||||
round_delete_job = read_json(round_delete_prepared["job_path"])
|
||||
_assert(round_delete_job.get("target", {}).get("backendOperation") == "delete_round_or_chamfer", f"round delete should route to delete_round_or_chamfer: {round_delete_job}")
|
||||
_assert(round_delete_job.get("target", {}).get("value") is True, f"command target should be true: {round_delete_job}")
|
||||
|
||||
chamfer_signature = {
|
||||
"objectType": "chamfer",
|
||||
"faceIds": [63],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 63,
|
||||
"globalFaceOrdinal": 83,
|
||||
"distance": 0.8,
|
||||
"distance1": 0.8,
|
||||
"distance2": 0.8,
|
||||
"chamferType": "EqualDistanceChamfer",
|
||||
"isEqualDistanceChamfer": True,
|
||||
}
|
||||
chamfer_distance_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "chamfer-distance-prepared",
|
||||
backend=backend,
|
||||
capability_key="chamfer.distance",
|
||||
target_value="1.2",
|
||||
object_id="chamfer:63",
|
||||
object_signature=chamfer_signature,
|
||||
)
|
||||
_assert(chamfer_distance_prepared.get("ok") is True, f"chamfer.distance should be productized and prepare an edit job: {chamfer_distance_prepared}")
|
||||
chamfer_distance_job = read_json(chamfer_distance_prepared["job_path"])
|
||||
_assert(chamfer_distance_job.get("target", {}).get("backendOperation") == "change_chamfer_distance", f"chamfer.distance should route to change_chamfer_distance: {chamfer_distance_job}")
|
||||
_assert(chamfer_distance_job.get("target", {}).get("value") == 1.2, f"chamfer.distance target should be numeric: {chamfer_distance_job}")
|
||||
chamfer_job_signature = chamfer_distance_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(chamfer_job_signature.get("isEqualDistanceChamfer") is True, f"chamfer.distance should preserve equal-distance evidence: {chamfer_distance_job}")
|
||||
_assert(chamfer_job_signature.get("distance") == 0.8, f"chamfer.distance should preserve current distance: {chamfer_distance_job}")
|
||||
|
||||
pattern_signature = {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [85, 94, 87, 96, 89, 98],
|
||||
"bodyIndex": 0,
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
"patternInstances": [
|
||||
{
|
||||
"sourceObjectId": "hole:a",
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"faceIds": [85, 94],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85},
|
||||
{"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 94},
|
||||
],
|
||||
},
|
||||
{
|
||||
"sourceObjectId": "hole:b",
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"faceIds": [87, 96],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 22, "globalFaceOrdinal": 87},
|
||||
{"bodyIndex": 0, "faceOrdinal": 29, "globalFaceOrdinal": 96},
|
||||
],
|
||||
},
|
||||
{
|
||||
"sourceObjectId": "hole:c",
|
||||
"center": [10.0, 0.0, 0.0],
|
||||
"faceIds": [89, 98],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 89},
|
||||
{"bodyIndex": 0, "faceOrdinal": 39, "globalFaceOrdinal": 98},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
pattern_spacing_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value="7.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_signature,
|
||||
)
|
||||
_assert(pattern_spacing_prepared.get("ok") is True, f"pattern.spacing should be productized and prepare an edit job: {pattern_spacing_prepared}")
|
||||
pattern_spacing_job = read_json(pattern_spacing_prepared["job_path"])
|
||||
_assert(pattern_spacing_job.get("target", {}).get("backendOperation") == "change_pattern_spacing", f"pattern.spacing should route to change_pattern_spacing: {pattern_spacing_job}")
|
||||
_assert(pattern_spacing_job.get("target", {}).get("value") == 7.5, f"pattern.spacing target should be numeric: {pattern_spacing_job}")
|
||||
pattern_job_signature = pattern_spacing_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(pattern_job_signature.get("spacing") == 5.0, f"pattern spacing signature should be preserved: {pattern_spacing_job}")
|
||||
_assert(len(pattern_job_signature.get("patternInstances") or []) == 3, f"pattern instance locators should be preserved: {pattern_spacing_job}")
|
||||
pattern_segment_signature = dict(pattern_signature)
|
||||
pattern_segment_signature["segmentIndex"] = 1
|
||||
pattern_segment_signature["movingSide"] = "after"
|
||||
pattern_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_signature,
|
||||
)
|
||||
_assert(pattern_segment_prepared.get("ok") is True, f"pattern.segment_spacing should prepare an edit job: {pattern_segment_prepared}")
|
||||
pattern_segment_job = read_json(pattern_segment_prepared["job_path"])
|
||||
_assert(
|
||||
pattern_segment_job.get("target", {}).get("backendOperation") == "change_pattern_segment_spacing",
|
||||
f"pattern.segment_spacing should route to change_pattern_segment_spacing: {pattern_segment_job}",
|
||||
)
|
||||
_assert(pattern_segment_job.get("target", {}).get("value") == 6.5, f"pattern.segment_spacing target should be numeric: {pattern_segment_job}")
|
||||
_assert(
|
||||
pattern_segment_job.get("object", {}).get("geometrySignature", {}).get("segmentIndex") == 1,
|
||||
f"pattern.segment_spacing should preserve the selected adjacent segment: {pattern_segment_job}",
|
||||
)
|
||||
pattern_segment_before_signature = dict(pattern_segment_signature)
|
||||
pattern_segment_before_signature["movingSide"] = "before"
|
||||
pattern_segment_before_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-before-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_before_signature,
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_before_prepared.get("ok") is True
|
||||
and read_json(pattern_segment_before_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "before",
|
||||
f"pattern.segment_spacing should preserve fix-right/move-left semantics: {pattern_segment_before_prepared}",
|
||||
)
|
||||
pattern_segment_split_signature = dict(pattern_segment_signature)
|
||||
pattern_segment_split_signature["movingSide"] = "split"
|
||||
pattern_segment_split_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-split-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_split_signature,
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_split_prepared.get("ok") is True
|
||||
and read_json(pattern_segment_split_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "split",
|
||||
f"pattern.segment_spacing should preserve split/keep-center semantics: {pattern_segment_split_prepared}",
|
||||
)
|
||||
|
||||
body_pattern_signature = {
|
||||
"objectType": "linear_pattern",
|
||||
"patternKind": "body",
|
||||
"instanceKind": "body",
|
||||
"faceIds": [300, 301, 302],
|
||||
"bodyIndices": [20, 21, 22],
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 20.0, 0.0], [0.0, 25.0, 0.0], [0.0, 30.0, 0.0]],
|
||||
"patternInstances": [
|
||||
{"sourceObjectId": "body:20", "instanceKind": "body", "center": [0.0, 20.0, 0.0], "bodyIndex": 20, "bodyLocators": [{"bodyIndex": 20}], "faceIds": [300]},
|
||||
{"sourceObjectId": "body:21", "instanceKind": "body", "center": [0.0, 25.0, 0.0], "bodyIndex": 21, "bodyLocators": [{"bodyIndex": 21}], "faceIds": [301]},
|
||||
{"sourceObjectId": "body:22", "instanceKind": "body", "center": [0.0, 30.0, 0.0], "bodyIndex": 22, "bodyLocators": [{"bodyIndex": 22}], "faceIds": [302]},
|
||||
],
|
||||
}
|
||||
body_pattern_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "body-pattern-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value="8",
|
||||
object_id="pattern:parts-20-22",
|
||||
object_signature=body_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
body_pattern_prepared.get("ok") is False
|
||||
and body_pattern_prepared.get("reason") == "body-pattern-spacing-missing-component-locators",
|
||||
f"body pattern spacing should be blocked until component occurrence locators are available: {body_pattern_prepared}",
|
||||
)
|
||||
body_segment_pattern_signature = dict(body_pattern_signature)
|
||||
body_segment_pattern_signature["segmentIndex"] = 0
|
||||
body_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "body-pattern-segment-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6",
|
||||
object_id="pattern:parts-20-22",
|
||||
object_signature=body_segment_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
body_segment_prepared.get("ok") is False
|
||||
and body_segment_prepared.get("reason") == "body-pattern-spacing-missing-component-locators",
|
||||
f"body pattern local segment spacing should also require component occurrence locators: {body_segment_prepared}",
|
||||
)
|
||||
component_body_pattern_signature = dict(body_pattern_signature)
|
||||
component_body_pattern_signature["componentInstanceCount"] = 3
|
||||
component_body_pattern_signature["patternInstances"] = [
|
||||
{
|
||||
**dict(item),
|
||||
"componentLocators": [
|
||||
{
|
||||
"componentIndex": index,
|
||||
"componentPath": [index],
|
||||
"componentBodyIndex": 0,
|
||||
"bodyIndex": item.get("bodyIndex"),
|
||||
"componentName": f"Part {index + 1}",
|
||||
}
|
||||
],
|
||||
}
|
||||
for index, item in enumerate(body_pattern_signature["patternInstances"])
|
||||
]
|
||||
component_body_pattern_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "component-body-pattern-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value="8",
|
||||
object_id="pattern:components-20-22",
|
||||
object_signature=component_body_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
component_body_pattern_prepared.get("ok") is True,
|
||||
f"body pattern spacing should prepare when every member has a component occurrence locator: {component_body_pattern_prepared}",
|
||||
)
|
||||
component_body_pattern_job = read_json(component_body_pattern_prepared["job_path"])
|
||||
component_instances = component_body_pattern_job.get("object", {}).get("geometrySignature", {}).get("patternInstances") or []
|
||||
_assert(
|
||||
all(item.get("componentLocators") for item in component_instances if isinstance(item, dict)),
|
||||
f"component occurrence locators should be preserved in pattern.spacing job: {component_body_pattern_job}",
|
||||
)
|
||||
component_body_segment_signature = dict(component_body_pattern_signature)
|
||||
component_body_segment_signature["segmentIndex"] = 0
|
||||
component_body_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "component-body-pattern-segment-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6",
|
||||
object_id="pattern:components-20-22",
|
||||
object_signature=component_body_segment_signature,
|
||||
)
|
||||
_assert(
|
||||
component_body_segment_prepared.get("ok") is True,
|
||||
f"body pattern local segment spacing should prepare with component occurrence locators: {component_body_segment_prepared}",
|
||||
)
|
||||
blocked_pattern_signature = dict(pattern_signature)
|
||||
blocked_pattern_signature["supportPatternFit"] = {
|
||||
"supportFaceIds": [92],
|
||||
"localUnitScale": 0.001,
|
||||
"maxSpacing": 0.0012,
|
||||
"maxSpacingLocal": 1.2,
|
||||
"instanceCount": 3,
|
||||
}
|
||||
blocked_pattern_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "body-pattern-spacing-blocked",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value=0.002,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=blocked_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
blocked_pattern_prepared.get("ok") is False
|
||||
and blocked_pattern_prepared.get("reason") == "pattern-spacing-exceeds-support",
|
||||
f"pattern spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_pattern_prepared}",
|
||||
)
|
||||
blocked_segment_signature = dict(pattern_segment_signature)
|
||||
blocked_segment_signature["supportPatternFit"] = {
|
||||
"supportFaceIds": [92],
|
||||
"localUnitScale": 0.001,
|
||||
"maxSegmentSpacing": 0.0013,
|
||||
"maxSegmentSpacingLocal": 1.3,
|
||||
"instanceCount": 3,
|
||||
}
|
||||
blocked_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-blocked",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value=0.002,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=blocked_segment_signature,
|
||||
)
|
||||
_assert(
|
||||
blocked_segment_prepared.get("ok") is False
|
||||
and blocked_segment_prepared.get("reason") == "pattern-spacing-exceeds-support",
|
||||
f"pattern segment spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_segment_prepared}",
|
||||
)
|
||||
|
||||
planned = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "unsupported",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
capability_key="pattern.instance_position",
|
||||
target_value="1",
|
||||
object_signature=signature,
|
||||
)
|
||||
@@ -305,6 +722,30 @@ def main() -> int:
|
||||
)
|
||||
_assert(slot_success.get("ok") is True, f"fake slot.position edit should succeed: {slot_success}")
|
||||
_assert(slot_success.get("backend_operation") == "move_slot", f"slot.position should report move_slot: {slot_success}")
|
||||
slot_width_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-width-success",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
target_value=2.5,
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(slot_width_success.get("ok") is True, f"fake slot.width edit should succeed: {slot_width_success}")
|
||||
_assert(slot_width_success.get("backend_operation") == "change_slot_width", f"slot.width should report change_slot_width: {slot_width_success}")
|
||||
slot_depth_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-depth-success",
|
||||
backend=backend,
|
||||
capability_key="slot.depth",
|
||||
target_value=2.0,
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(slot_depth_success.get("ok") is True, f"fake slot.depth edit should succeed: {slot_depth_success}")
|
||||
_assert(slot_depth_success.get("backend_operation") == "change_slot_depth", f"slot.depth should report change_slot_depth: {slot_depth_success}")
|
||||
|
||||
boss_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
@@ -318,6 +759,97 @@ def main() -> int:
|
||||
)
|
||||
_assert(boss_success.get("ok") is True, f"fake boss.position edit should succeed: {boss_success}")
|
||||
_assert(boss_success.get("backend_operation") == "move_boss", f"boss.position should report move_boss: {boss_success}")
|
||||
boss_height_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-height-success",
|
||||
backend=backend,
|
||||
capability_key="boss.height",
|
||||
target_value=5.5,
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(boss_height_success.get("ok") is True, f"fake boss.height edit should succeed: {boss_height_success}")
|
||||
_assert(boss_height_success.get("backend_operation") == "change_boss_height", f"boss.height should report change_boss_height: {boss_height_success}")
|
||||
boss_diameter_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-diameter-success",
|
||||
backend=backend,
|
||||
capability_key="boss.diameter",
|
||||
target_value=4.5,
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(boss_diameter_success.get("ok") is True, f"fake boss.diameter edit should succeed: {boss_diameter_success}")
|
||||
_assert(boss_diameter_success.get("backend_operation") == "change_boss_diameter", f"boss.diameter should report change_boss_diameter: {boss_diameter_success}")
|
||||
|
||||
round_radius_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-radius-success",
|
||||
backend=backend,
|
||||
capability_key="round.radius",
|
||||
target_value=0.75,
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(round_radius_success.get("ok") is True, f"fake round.radius edit should succeed: {round_radius_success}")
|
||||
_assert(round_radius_success.get("backend_operation") == "change_round_radius", f"round.radius should report change_round_radius: {round_radius_success}")
|
||||
|
||||
round_delete_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-delete-success",
|
||||
backend=backend,
|
||||
capability_key="feature.delete_round_or_chamfer",
|
||||
target_value="",
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(round_delete_success.get("ok") is True, f"fake round/chamfer delete edit should succeed: {round_delete_success}")
|
||||
_assert(round_delete_success.get("backend_operation") == "delete_round_or_chamfer", f"round/chamfer delete should report delete_round_or_chamfer: {round_delete_success}")
|
||||
|
||||
chamfer_distance_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "chamfer-distance-success",
|
||||
backend=backend,
|
||||
capability_key="chamfer.distance",
|
||||
target_value=1.2,
|
||||
object_id="chamfer:63",
|
||||
object_signature=chamfer_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(chamfer_distance_success.get("ok") is True, f"fake chamfer.distance edit should succeed: {chamfer_distance_success}")
|
||||
_assert(chamfer_distance_success.get("backend_operation") == "change_chamfer_distance", f"chamfer.distance should report change_chamfer_distance: {chamfer_distance_success}")
|
||||
|
||||
pattern_spacing_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-spacing-success",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value=7.5,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(pattern_spacing_success.get("ok") is True, f"fake pattern.spacing edit should succeed: {pattern_spacing_success}")
|
||||
_assert(pattern_spacing_success.get("backend_operation") == "change_pattern_spacing", f"pattern.spacing should report change_pattern_spacing: {pattern_spacing_success}")
|
||||
pattern_segment_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-success",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value=6.5,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(pattern_segment_success.get("ok") is True, f"fake pattern.segment_spacing edit should succeed: {pattern_segment_success}")
|
||||
_assert(
|
||||
pattern_segment_success.get("backend_operation") == "change_pattern_segment_spacing",
|
||||
f"pattern.segment_spacing should report change_pattern_segment_spacing: {pattern_segment_success}",
|
||||
)
|
||||
|
||||
missing_output = run_scdm_edit_job(
|
||||
step_path,
|
||||
|
||||
@@ -95,11 +95,15 @@ def main() -> int:
|
||||
_assert("StandardHoles" in script and "FindStandardHoleOptions" in script and "getattr(standard_holes, 'Find'" in script, "probe script should try SCDM StandardHoles.Find before geometric fallback")
|
||||
_assert("availableCommands" in script and "ConstantRound" in script and "Chamfer" in script, "probe script should report SCDM command availability")
|
||||
_assert("RoundInfo" in script and "_round_info_from_face" in script and "change_round_radius" in script, "probe script should collect SCDM round diagnostics")
|
||||
_assert("ChamferInfo" in script and "_chamfer_info_from_face" in script and "change_chamfer_distance" in script, "probe script should collect SCDM chamfer diagnostics")
|
||||
_assert("SlotInfo" in script and "_slot_info_from_face" in script and "change_slot_depth" in script, "probe script should collect SCDM slot diagnostics")
|
||||
_assert("backendCommandCandidates" in script, "probe script should write raw command candidates")
|
||||
for token in ("_geometry_from_edge", "Length", "StartPoint", "EndPoint", "adjacentFaceOrdinals"):
|
||||
_assert(token in script, f"probe script should enrich Edge raw geometry with {token}")
|
||||
for token in ("_record_face_adjacency", "_face_adjacency_rows", "edgeGeometrySummary", "_final_edge_geometry_summary", "_feature_inventory"):
|
||||
_assert(token in script, f"probe script should summarize SCDM topology evidence with {token}")
|
||||
for token in ("_component_entries", "_component_body_locator_map", "componentInstances", "componentLocators"):
|
||||
_assert(token in script, f"probe script should preserve SCDM component occurrence evidence with {token}")
|
||||
generated = generate_scdm_probe_script(job_path)
|
||||
_assert("JOB_PATH =" in generated and job_path.name in generated, "generated probe script should embed the job path")
|
||||
|
||||
@@ -123,7 +127,10 @@ def main() -> int:
|
||||
{"name": "Move", "available": True},
|
||||
{"name": "Fill", "available": True},
|
||||
{"name": "Delete", "available": True},
|
||||
{"name": "Chamfer", "available": True},
|
||||
{"name": "ConstantRound", "available": True},
|
||||
{"name": "ChamferInfo", "available": True},
|
||||
{"name": "SlotInfo", "available": True},
|
||||
{"name": "SomeFutureCommand", "available": False},
|
||||
],
|
||||
"faceAdjacency": [
|
||||
@@ -144,18 +151,30 @@ def main() -> int:
|
||||
"maxEdgeLength": 8.0,
|
||||
},
|
||||
"featureInventory": {
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1},
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1, "chamfer": 1},
|
||||
"surfaceTypeCounts": {"plane": 1, "cylinder": 3},
|
||||
"curveTypeCounts": {"Line": 5, "Circle": 4},
|
||||
"operationCounts": {
|
||||
"pull_face_offset": 1,
|
||||
"change_hole_diameter": 1,
|
||||
"change_slot_width": 1,
|
||||
"change_slot_depth": 1,
|
||||
"move_slot": 1,
|
||||
"change_boss_height": 1,
|
||||
"move_boss": 1,
|
||||
"change_chamfer_distance": 1,
|
||||
},
|
||||
},
|
||||
"componentInstances": [
|
||||
{
|
||||
"backendId": "component:0",
|
||||
"componentIndex": 0,
|
||||
"componentPath": [0],
|
||||
"componentName": "零件 3",
|
||||
"contentBodyCount": 1,
|
||||
"placementTranslation": [0.0, 20.0, 0.0],
|
||||
}
|
||||
],
|
||||
},
|
||||
"summary": {"bodyCount": 1, "objectCount": 10, "faceCount": 6, "edgeCount": 1, "holeFaceCount": 0},
|
||||
"objects": [
|
||||
@@ -233,6 +252,34 @@ def main() -> int:
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:2/face:100",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [100], "bodyIndex": 2, "faceOrdinal": 100, "globalFaceOrdinal": 100},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:2/face:101",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 1.2],
|
||||
"normal": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [101], "bodyIndex": 2, "faceOrdinal": 101, "globalFaceOrdinal": 101},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:8/face:5",
|
||||
"objectType": "face",
|
||||
@@ -262,7 +309,17 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "round:1",
|
||||
"objectType": "round",
|
||||
"geometry": {"radius": 1.0},
|
||||
"geometry": {
|
||||
"radius": 1.0,
|
||||
"roundInfo": {
|
||||
"radius": 1.0,
|
||||
"diameter": 2.0,
|
||||
"isConstant": True,
|
||||
"isRound": True,
|
||||
"type": "ConstantRound",
|
||||
},
|
||||
},
|
||||
"topologyHint": {"faceIds": [60], "bodyIndex": 0, "faceOrdinal": 60, "globalFaceOrdinal": 60},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_round_radius", "enabled": True},
|
||||
{"operation": "delete_round_or_chamfer", "enabled": True},
|
||||
@@ -272,10 +329,16 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "slot:1",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30},
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {
|
||||
"faceIds": [30, 31, 32],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 30,
|
||||
"depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}],
|
||||
},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_slot_width", "enabled": True},
|
||||
{"operation": "change_slot_depth", "enabled": True},
|
||||
{"operation": "move_slot", "enabled": True, "parameterFields": {"center": [1.0, 2.0, 3.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
@@ -284,9 +347,16 @@ def main() -> int:
|
||||
"backendId": "boss:1",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [50, 51, 52], "bodyIndex": 0, "faceOrdinal": 50},
|
||||
"topologyHint": {
|
||||
"faceIds": [50, 51, 52],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 50,
|
||||
"heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}],
|
||||
"diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}],
|
||||
},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_boss_height", "enabled": True},
|
||||
{"operation": "change_boss_diameter", "enabled": True},
|
||||
{"operation": "move_boss", "enabled": True, "parameterFields": {"center": [0.0, 0.0, 2.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
@@ -294,7 +364,18 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "chamfer:1",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"distance": 0.8},
|
||||
"geometry": {
|
||||
"distance": 0.8,
|
||||
"chamferInfo": {
|
||||
"distance": 0.8,
|
||||
"distance1": 0.8,
|
||||
"distance2": 0.8,
|
||||
"isEqualDistance": True,
|
||||
"isChamfer": True,
|
||||
"type": "EqualDistanceChamfer",
|
||||
},
|
||||
},
|
||||
"topologyHint": {"faceIds": [63], "bodyIndex": 0, "faceOrdinal": 63, "globalFaceOrdinal": 63},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
@@ -305,6 +386,48 @@ def main() -> int:
|
||||
"backendCommandCandidates": [{"operation": "change_pattern_spacing", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 20,
|
||||
"faceIds": [300],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [300],
|
||||
"componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 21,
|
||||
"faceIds": [301],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [301],
|
||||
"componentLocators": [{"componentIndex": 1, "componentPath": [1], "componentBodyIndex": 0, "bodyIndex": 21}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 22,
|
||||
"faceIds": [302],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [302],
|
||||
"componentLocators": [{"componentIndex": 2, "componentPath": [2], "componentBodyIndex": 0, "bodyIndex": 22}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "shell:1",
|
||||
"objectType": "shell",
|
||||
@@ -342,7 +465,21 @@ def main() -> int:
|
||||
_assert(edge_signature.get("length") == 3.14, f"Edge length should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("startPoint") == [0.0, 0.0, 0.0], f"Edge start point should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("adjacentFaceOrdinals") == [3, 4], f"Edge adjacent faces should be preserved: {edge_signature}")
|
||||
component_signature = geometry_signature(
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0]},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 20,
|
||||
"bodyLocators": [{"bodyIndex": 20, "componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0}],
|
||||
"componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}],
|
||||
},
|
||||
}
|
||||
)
|
||||
_assert(component_signature.get("componentLocators"), f"component locators should be preserved in geometry signatures: {component_signature}")
|
||||
_assert(cache.get("modelFingerprint") == "abc123", f"model fingerprint should be copied: {cache}")
|
||||
_assert(int(cache.get("mapperRevision") or 0) >= 2, f"SCDM cache should carry the mapper revision for disk reuse: {cache}")
|
||||
_assert(cache.get("backendVersion") == "v222", f"backend version should be copied: {cache}")
|
||||
_assert({"hole.diameter", "hole.position"} <= _capability_keys(cache, "hole:1"), f"hole caps missing: {cache}")
|
||||
objects = cache.get("objects")
|
||||
@@ -375,24 +512,63 @@ def main() -> int:
|
||||
_assert({str(spec.get("scdm_capability_key")) for spec in face_specs} == {"face.offset"}, f"Face cache should map to offset only: {face_specs}")
|
||||
gated_face_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(9,), execution_ready={"face.offset"})
|
||||
_assert(gated_face_specs and all(spec.get("enabled") is True for spec in gated_face_specs), f"capability gate should enable verified face offset: {gated_face_specs}")
|
||||
_assert({"slot.position"} <= _capability_keys(cache, "slot:1"), f"slot.position should now be productized: {cache}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.position"})
|
||||
_assert({"slot.width", "slot.depth", "slot.position"} <= _capability_keys(cache, "slot:1"), f"slot width/depth/position should now be productized: {cache}")
|
||||
slot_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "slot:slot:1"), None)
|
||||
_assert(isinstance(slot_object, dict), f"slot object should be normalized: {cache}")
|
||||
slot_signature = slot_object.get("geometrySignature")
|
||||
_assert(isinstance(slot_signature, dict) and slot_signature.get("depthFaceLocators"), f"slot depth locator should be preserved: {slot_signature}")
|
||||
_assert(isinstance(slot_signature, dict) and slot_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_signature}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.width", "slot.depth", "slot.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.position"},
|
||||
f"slot cache should expose only productized slot.position: {slot_specs}",
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.width", "slot.depth", "slot.position"},
|
||||
f"slot cache should expose productized slot.width, slot.depth and slot.position: {slot_specs}",
|
||||
)
|
||||
_assert(slot_specs and slot_specs[0].get("enabled") is True, f"slot.position should enable when runner gate is ready: {slot_specs}")
|
||||
_assert(slot_specs and all(spec.get("enabled") is True for spec in slot_specs), f"slot width/depth/position should enable when runner gate is ready: {slot_specs}")
|
||||
slot_width_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.width"), None)
|
||||
_assert(slot_width_spec and slot_width_spec.get("value_type") == "positive", f"slot.width should use positive numeric input: {slot_specs}")
|
||||
slot_depth_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_spec and slot_depth_spec.get("value_type") == "positive", f"slot.depth should use positive numeric input: {slot_specs}")
|
||||
blocked_slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"face.offset"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot.position should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.position"} <= _capability_keys(cache, "boss:1"), f"boss.position should now be productized: {cache}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.position"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot capabilities should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.diameter", "boss.height", "boss.position"} <= _capability_keys(cache, "boss:1"), f"boss diameter/height/position should now be productized: {cache}")
|
||||
boss_object = next((item for item in objects if isinstance(item, dict) and item.get("objectType") == "cylindrical_boss"), None)
|
||||
_assert(isinstance(boss_object, dict), f"boss object should be normalized: {cache}")
|
||||
boss_signature = boss_object.get("geometrySignature")
|
||||
_assert(isinstance(boss_signature, dict) and boss_signature.get("heightFaceLocators"), f"boss height locator should be preserved: {boss_signature}")
|
||||
_assert(isinstance(boss_signature, dict) and boss_signature.get("diameterFaceLocators"), f"boss diameter locator should be preserved: {boss_signature}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.diameter", "boss.height", "boss.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.position"},
|
||||
f"boss cache should expose only productized boss.position: {boss_specs}",
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.diameter", "boss.height", "boss.position"},
|
||||
f"boss cache should expose productized boss.diameter, boss.height and boss.position: {boss_specs}",
|
||||
)
|
||||
_assert(boss_specs and boss_specs[0].get("enabled") is True, f"boss.position should enable when runner gate is ready: {boss_specs}")
|
||||
_assert(boss_specs and all(spec.get("enabled") is True for spec in boss_specs), f"boss diameter/height/position should enable when runner gate is ready: {boss_specs}")
|
||||
blocked_boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"slot.position"})
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss.position should honor runner gate: {blocked_boss_specs}")
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss capabilities should honor runner gate: {blocked_boss_specs}")
|
||||
_assert({"round.radius", "feature.delete_round_or_chamfer"} <= _capability_keys(cache, "round:1"), f"round radius/delete should now be productized: {cache}")
|
||||
round_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "round:round:1"), None)
|
||||
_assert(isinstance(round_object, dict), f"round object should be normalized: {cache}")
|
||||
round_signature = round_object.get("geometrySignature")
|
||||
_assert(isinstance(round_signature, dict) and round_signature.get("isConstantRound") is True, f"round constant evidence should be preserved: {round_signature}")
|
||||
round_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(60,), execution_ready={"round.radius", "feature.delete_round_or_chamfer"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in round_specs} == {"round.radius", "feature.delete_round_or_chamfer"},
|
||||
f"round cache should expose productized radius and delete command: {round_specs}",
|
||||
)
|
||||
round_radius_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
round_delete_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "feature.delete_round_or_chamfer"), None)
|
||||
_assert(round_radius_spec and round_radius_spec.get("value_type") == "positive" and round_radius_spec.get("enabled") is True, f"round.radius should be executable when runner gate is ready: {round_specs}")
|
||||
_assert(round_delete_spec and round_delete_spec.get("value_type") == "command" and round_delete_spec.get("enabled") is True, f"round delete command should be executable when runner gate is ready: {round_specs}")
|
||||
_assert({"chamfer.distance"} <= _capability_keys(cache, "chamfer:1"), f"chamfer distance should now be productized: {cache}")
|
||||
chamfer_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "chamfer:chamfer:1"), None)
|
||||
_assert(isinstance(chamfer_object, dict), f"chamfer object should be normalized: {cache}")
|
||||
chamfer_signature = chamfer_object.get("geometrySignature")
|
||||
_assert(isinstance(chamfer_signature, dict) and chamfer_signature.get("isEqualDistanceChamfer") is True, f"chamfer equal-distance evidence should be preserved: {chamfer_signature}")
|
||||
chamfer_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(63,), execution_ready={"chamfer.distance"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in chamfer_specs} == {"chamfer.distance"},
|
||||
f"chamfer cache should expose productized chamfer.distance: {chamfer_specs}",
|
||||
)
|
||||
_assert(chamfer_specs and chamfer_specs[0].get("value_type") == "positive" and chamfer_specs[0].get("enabled") is True, f"chamfer.distance should be executable when runner gate is ready: {chamfer_specs}")
|
||||
|
||||
raw_without_local_ids = {
|
||||
"schemaVersion": 1,
|
||||
@@ -452,6 +628,163 @@ def main() -> int:
|
||||
grouped_specs = property_specs_from_scdm_cache(enriched_group, selected_face_ids=(96,), execution_ready=True)
|
||||
grouped_keys = {str(spec.get("scdm_capability_key")) for spec in grouped_specs}
|
||||
_assert({"hole.diameter", "hole.position", "feature.fill"} <= grouped_keys, f"local Face IDs should attach to SCDM cylinder groups: {enriched_group}")
|
||||
body_pattern_without_face_ids = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
enriched_body_pattern = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(body_pattern_without_face_ids),
|
||||
[
|
||||
{"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane"},
|
||||
{"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane"},
|
||||
{"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane"},
|
||||
],
|
||||
)
|
||||
body_pattern_specs_from_ordinals = property_specs_from_scdm_cache(enriched_body_pattern, selected_face_ids=(301,), execution_ready={"pattern.spacing"})
|
||||
_assert(
|
||||
any(spec.get("scdm_capability_key") == "pattern.spacing" for spec in body_pattern_specs_from_ordinals),
|
||||
f"body pattern spacing should attach local Face IDs from SCDM ordinals: {enriched_body_pattern}",
|
||||
)
|
||||
unit_scaled_face_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:0/face:9",
|
||||
"objectType": "face",
|
||||
"geometry": {"surfaceType": "plane", "axis": [-1.0, 0.0, 0.0], "planeOffset": -0.00125},
|
||||
"topologyHint": {"bodyIndex": 0, "faceOrdinal": 9, "globalFaceOrdinal": 9},
|
||||
"backendCommandCandidates": [{"operation": "pull_face_offset", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
unit_scaled_face_cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(unit_scaled_face_raw),
|
||||
[
|
||||
{
|
||||
"faceId": 9,
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 9,
|
||||
"globalFaceOrdinal": 9,
|
||||
"surfaceType": "plane",
|
||||
"axis": [-1.0, 0.0, 0.0],
|
||||
"planeOffset": -1.25,
|
||||
}
|
||||
],
|
||||
)
|
||||
unit_scaled_face_signature = unit_scaled_face_cache["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
_assert(abs(float(unit_scaled_face_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"ordinal-matched Face should infer SCDM/local unit scale: {unit_scaled_face_signature}")
|
||||
unit_scaled_face_specs = property_specs_from_scdm_cache(unit_scaled_face_cache, selected_face_ids=(9,), execution_ready=True)
|
||||
_assert(
|
||||
unit_scaled_face_specs and unit_scaled_face_specs[0].get("current_text") == "-1.25",
|
||||
f"SCDM length values should display in local model units: {unit_scaled_face_specs}",
|
||||
)
|
||||
unit_scaled_body_pattern_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.020, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.025, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.030, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
unit_scaled_body_pattern_cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(unit_scaled_body_pattern_raw),
|
||||
[
|
||||
{"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane", "bboxMin": [-0.5, 19.0, 0.0], "bboxMax": [0.5, 21.0, 1.0]},
|
||||
{"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane", "bboxMin": [-0.5, 24.0, 0.0], "bboxMax": [0.5, 26.0, 1.0]},
|
||||
{"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane", "bboxMin": [-0.5, 29.0, 0.0], "bboxMax": [0.5, 31.0, 1.0]},
|
||||
{"faceId": 399, "bodyIndex": 99, "faceOrdinal": 0, "globalFaceOrdinal": 399, "surfaceType": "plane", "axis": [0.0, 0.0, 1.0], "planeOffset": 0.0, "bboxMin": [-1.0, 18.0, 0.0], "bboxMax": [1.0, 32.0, 0.0], "area": 28.0},
|
||||
],
|
||||
)
|
||||
unit_scaled_body_pattern = next(
|
||||
item for item in unit_scaled_body_pattern_cache["objects"] if item.get("objectType") == "linear_pattern" # type: ignore[index]
|
||||
)
|
||||
unit_scaled_body_signature = unit_scaled_body_pattern["geometrySignature"] # type: ignore[index]
|
||||
_assert(abs(float(unit_scaled_body_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"body pattern should infer unit scale from local body centers: {unit_scaled_body_signature}")
|
||||
_assert(unit_scaled_body_signature.get("supportFaceIds") == [399], f"body pattern should expose its support Face: {unit_scaled_body_signature}")
|
||||
fit = unit_scaled_body_signature.get("supportPatternFit")
|
||||
_assert(isinstance(fit, dict) and fit.get("supportFaceIds") == [399], f"body pattern should record support fit limits: {unit_scaled_body_signature}")
|
||||
_assert(abs(float(fit.get("maxSpacingLocal") or 0.0) - 6.0) <= 1.0e-9, f"support fit should limit centered spacing: {fit}")
|
||||
unit_scaled_body_specs = property_specs_from_scdm_cache(
|
||||
{"objects": [unit_scaled_body_pattern]},
|
||||
selected_face_ids=(399,),
|
||||
execution_ready={"pattern.spacing", "pattern.segment_spacing"},
|
||||
)
|
||||
_assert(
|
||||
unit_scaled_body_specs and unit_scaled_body_specs[0].get("current_text") == "5",
|
||||
f"support Face should expose pattern spacing in local units: {unit_scaled_body_specs}",
|
||||
)
|
||||
_assert(
|
||||
unit_scaled_body_specs[0].get("max_value") == 6.0,
|
||||
f"support Face pattern spacing should expose a local-unit hard limit: {unit_scaled_body_specs}",
|
||||
)
|
||||
local_segment_spec = next(
|
||||
(spec for spec in unit_scaled_body_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"),
|
||||
None,
|
||||
)
|
||||
_assert(
|
||||
isinstance(local_segment_spec, dict)
|
||||
and str(local_segment_spec.get("label") or "").startswith("Solid20-Solid21")
|
||||
and str(local_segment_spec.get("range_hint") or "").startswith("固定前项,移动后侧:")
|
||||
and "固定 Solid20" in str(local_segment_spec.get("range_hint") or "")
|
||||
and "平移 Solid21" in str(local_segment_spec.get("range_hint") or ""),
|
||||
f"local mapped pattern segment labels should use visible Solid IDs and explain motion semantics: {unit_scaled_body_specs}",
|
||||
)
|
||||
|
||||
missing_command_raw = {
|
||||
"schemaVersion": 1,
|
||||
@@ -479,6 +812,218 @@ def main() -> int:
|
||||
missing_command_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_command_raw), selected_face_ids=(12,), execution_ready=True)
|
||||
_assert(missing_command_specs and missing_command_specs[0].get("enabled") is False, f"missing SCDM command should disable capability: {missing_command_specs}")
|
||||
_assert("OffsetFaces" in str(missing_command_specs[0].get("disabled_tip") or ""), f"disabled reason should name the missing SCDM command: {missing_command_specs}")
|
||||
missing_current_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-center",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0},
|
||||
"topologyHint": {"faceIds": [44]},
|
||||
"backendCommandCandidates": [{"operation": "move_slot", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
missing_current_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_current_raw), selected_face_ids=(44,), execution_ready={"slot.width", "slot.position"})
|
||||
slot_position_missing_current = next((spec for spec in missing_current_specs if spec.get("scdm_capability_key") == "slot.position"), None)
|
||||
_assert(slot_position_missing_current and slot_position_missing_current.get("enabled") is False, f"missing current value should disable slot.position: {missing_current_specs}")
|
||||
_assert("没有返回可用于编辑的当前值" in str(slot_position_missing_current.get("disabled_tip") or ""), f"missing current value should be explained: {missing_current_specs}")
|
||||
slot_missing_depth_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-depth-locator",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [45], "bodyIndex": 0, "faceOrdinal": 45},
|
||||
"backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
slot_missing_depth_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(slot_missing_depth_locator_raw),
|
||||
selected_face_ids=(45,),
|
||||
execution_ready={"slot.depth"},
|
||||
)
|
||||
slot_depth_without_locator = next((spec for spec in slot_missing_depth_locator_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_without_locator and slot_depth_without_locator.get("enabled") is False, f"slot.depth should require a bottom face locator: {slot_missing_depth_locator_specs}")
|
||||
_assert("槽底面定位信息" in str(slot_depth_without_locator.get("disabled_tip") or ""), f"slot.depth missing locator should be explained: {slot_missing_depth_locator_specs}")
|
||||
slot_missing_depth_axis_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-depth-axis",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [46], "bodyIndex": 0, "faceOrdinal": 46, "depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 46}]},
|
||||
"backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
slot_missing_depth_axis_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(slot_missing_depth_axis_raw),
|
||||
selected_face_ids=(46,),
|
||||
execution_ready={"slot.depth"},
|
||||
)
|
||||
slot_depth_without_axis = next((spec for spec in slot_missing_depth_axis_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_without_axis and slot_depth_without_axis.get("enabled") is False, f"slot.depth should require a depth axis: {slot_missing_depth_axis_specs}")
|
||||
_assert("槽深方向" in str(slot_depth_without_axis.get("disabled_tip") or ""), f"slot.depth missing axis should be explained: {slot_missing_depth_axis_specs}")
|
||||
boss_missing_height_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "boss:missing-height-locator",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [55], "bodyIndex": 0, "faceOrdinal": 55},
|
||||
"backendCommandCandidates": [{"operation": "change_boss_height", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
boss_missing_height_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(boss_missing_height_locator_raw),
|
||||
selected_face_ids=(55,),
|
||||
execution_ready={"boss.height"},
|
||||
)
|
||||
boss_height_without_locator = next((spec for spec in boss_missing_height_locator_specs if spec.get("scdm_capability_key") == "boss.height"), None)
|
||||
_assert(boss_height_without_locator and boss_height_without_locator.get("enabled") is False, f"boss.height should require a top face locator: {boss_missing_height_locator_specs}")
|
||||
_assert("顶面定位信息" in str(boss_height_without_locator.get("disabled_tip") or ""), f"boss.height missing locator should be explained: {boss_missing_height_locator_specs}")
|
||||
boss_missing_diameter_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "boss:missing-diameter-locator",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [56], "bodyIndex": 0, "faceOrdinal": 56},
|
||||
"backendCommandCandidates": [{"operation": "change_boss_diameter", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
boss_missing_diameter_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(boss_missing_diameter_locator_raw),
|
||||
selected_face_ids=(56,),
|
||||
execution_ready={"boss.diameter"},
|
||||
)
|
||||
boss_diameter_without_locator = next((spec for spec in boss_missing_diameter_locator_specs if spec.get("scdm_capability_key") == "boss.diameter"), None)
|
||||
_assert(boss_diameter_without_locator and boss_diameter_without_locator.get("enabled") is False, f"boss.diameter should require a side face locator: {boss_missing_diameter_locator_specs}")
|
||||
_assert("侧壁定位信息" in str(boss_diameter_without_locator.get("disabled_tip") or ""), f"boss.diameter missing locator should be explained: {boss_missing_diameter_locator_specs}")
|
||||
round_missing_constant_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "round:missing-constant",
|
||||
"objectType": "round",
|
||||
"geometry": {"roundInfo": {"radius": 1.0, "isRound": True}},
|
||||
"topologyHint": {"faceIds": [61], "bodyIndex": 0, "faceOrdinal": 61},
|
||||
"backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
round_missing_constant_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(round_missing_constant_raw),
|
||||
selected_face_ids=(61,),
|
||||
execution_ready={"round.radius"},
|
||||
)
|
||||
round_without_constant = next((spec for spec in round_missing_constant_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
_assert(round_without_constant and round_without_constant.get("enabled") is False, f"round.radius should require constant-round evidence: {round_missing_constant_specs}")
|
||||
_assert("等半径圆角证据" in str(round_without_constant.get("disabled_tip") or ""), f"round.radius missing constant evidence should be explained: {round_missing_constant_specs}")
|
||||
round_missing_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "round:missing-locator",
|
||||
"objectType": "round",
|
||||
"geometry": {"roundInfo": {"radius": 1.0, "isConstant": True, "isRound": True}},
|
||||
"topologyHint": {"faceIds": [62]},
|
||||
"backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
round_missing_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(round_missing_locator_raw),
|
||||
selected_face_ids=(62,),
|
||||
execution_ready={"round.radius"},
|
||||
)
|
||||
round_without_locator = next((spec for spec in round_missing_locator_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
_assert(round_without_locator and round_without_locator.get("enabled") is False, f"round.radius should require SCDM face locator evidence: {round_missing_locator_specs}")
|
||||
_assert("可定位的圆角面" in str(round_without_locator.get("disabled_tip") or ""), f"round.radius missing locator should be explained: {round_missing_locator_specs}")
|
||||
chamfer_missing_equal_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "chamfer:missing-equal",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"chamferInfo": {"distance": 0.8, "isChamfer": True}},
|
||||
"topologyHint": {"faceIds": [64], "bodyIndex": 0, "faceOrdinal": 64},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
chamfer_missing_equal_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(chamfer_missing_equal_raw),
|
||||
selected_face_ids=(64,),
|
||||
execution_ready={"chamfer.distance"},
|
||||
)
|
||||
chamfer_without_equal = next((spec for spec in chamfer_missing_equal_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None)
|
||||
_assert(chamfer_without_equal and chamfer_without_equal.get("enabled") is False, f"chamfer.distance should require equal-distance evidence: {chamfer_missing_equal_specs}")
|
||||
_assert("等距倒角证据" in str(chamfer_without_equal.get("disabled_tip") or ""), f"chamfer.distance missing equal evidence should be explained: {chamfer_missing_equal_specs}")
|
||||
chamfer_missing_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "chamfer:missing-locator",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"chamferInfo": {"distance": 0.8, "isEqualDistance": True, "isChamfer": True}},
|
||||
"topologyHint": {"faceIds": [65]},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
chamfer_missing_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(chamfer_missing_locator_raw),
|
||||
selected_face_ids=(65,),
|
||||
execution_ready={"chamfer.distance"},
|
||||
)
|
||||
chamfer_without_locator = next((spec for spec in chamfer_missing_locator_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None)
|
||||
_assert(chamfer_without_locator and chamfer_without_locator.get("enabled") is False, f"chamfer.distance should require SCDM face locator evidence: {chamfer_missing_locator_specs}")
|
||||
_assert("可定位的倒角面" in str(chamfer_without_locator.get("disabled_tip") or ""), f"chamfer.distance missing locator should be explained: {chamfer_missing_locator_specs}")
|
||||
diagnostics = cache.get("diagnostics")
|
||||
_assert(isinstance(diagnostics, dict), f"cache diagnostics should be present: {cache}")
|
||||
raw_summary = diagnostics.get("raw_summary")
|
||||
@@ -499,11 +1044,18 @@ def main() -> int:
|
||||
and feature_inventory.get("operationCounts", {}).get("change_slot_width") == 1,
|
||||
f"SCDM feature inventory should be preserved: {cache}",
|
||||
)
|
||||
component_instances = diagnostics.get("component_instances")
|
||||
_assert(
|
||||
isinstance(component_instances, list)
|
||||
and component_instances
|
||||
and component_instances[0].get("componentPath") == [0],
|
||||
f"SCDM component occurrence diagnostics should be preserved: {cache}",
|
||||
)
|
||||
geometry_hints = diagnostics.get("geometry_candidate_hints")
|
||||
_assert(isinstance(geometry_hints, list) and geometry_hints, f"SCDM structural candidate hints should be produced: {cache}")
|
||||
hint_keys = {str(item.get("capabilityKey")) for item in geometry_hints if isinstance(item, dict)}
|
||||
_assert(
|
||||
{"slot.width", "boss.height", "round.radius", "chamfer.distance", "pattern.spacing", "shell.thickness"} <= hint_keys,
|
||||
{"pattern.instance_position", "shell.thickness"} <= hint_keys and "slot.depth" not in hint_keys and "pattern.spacing" not in hint_keys,
|
||||
f"S7 geometry hints should cover planned feature families: {hint_keys}",
|
||||
)
|
||||
derived_candidates = diagnostics.get("derived_feature_candidates")
|
||||
@@ -516,20 +1068,103 @@ def main() -> int:
|
||||
_assert(pattern_signature.get("instanceCount") == 3, f"linear pattern count should be preserved: {pattern_signature}")
|
||||
_assert(pattern_signature.get("axis") == [1.0, 0.0, 0.0], f"linear pattern axis should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("instanceCenters") or []) == 3, f"linear pattern centers should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("patternInstances") or []) == 3, f"linear pattern instance locators should be preserved: {pattern_signature}")
|
||||
_assert({"pattern.spacing"} <= _capability_keys(cache, "derived:linear_pattern"), f"derived linear pattern should expose productized spacing: {cache}")
|
||||
thin_wall_candidate = next((item for item in derived_candidates if isinstance(item, dict) and item.get("objectType") == "thin_wall"), None)
|
||||
_assert(isinstance(thin_wall_candidate, dict), f"paired planar faces should produce a thin-wall candidate: {derived_candidates}")
|
||||
thin_wall_signature = thin_wall_candidate.get("geometrySignature")
|
||||
_assert(isinstance(thin_wall_signature, dict), f"thin-wall candidate should keep a geometry signature: {thin_wall_candidate}")
|
||||
_assert(thin_wall_signature.get("thickness") == 1.2, f"thin-wall thickness should be preserved: {thin_wall_signature}")
|
||||
_assert(thin_wall_signature.get("thicknessAxis") == [0.0, 0.0, 1.0], f"thin-wall axis should be preserved: {thin_wall_signature}")
|
||||
_assert(len(thin_wall_signature.get("wallFaceLocators") or []) == 2, f"thin-wall face locators should be preserved: {thin_wall_signature}")
|
||||
body_pattern_candidate = next(
|
||||
(
|
||||
item
|
||||
for item in derived_candidates
|
||||
if isinstance(item, dict)
|
||||
and item.get("objectType") == "linear_pattern"
|
||||
and isinstance(item.get("geometrySignature"), dict)
|
||||
and item.get("geometrySignature", {}).get("patternKind") == "body"
|
||||
),
|
||||
None,
|
||||
)
|
||||
_assert(isinstance(body_pattern_candidate, dict), f"repeated bodies should produce a body linear-pattern candidate: {derived_candidates}")
|
||||
body_pattern_signature = body_pattern_candidate.get("geometrySignature")
|
||||
_assert(isinstance(body_pattern_signature, dict), f"body pattern should keep a geometry signature: {body_pattern_candidate}")
|
||||
_assert(body_pattern_signature.get("spacing") == 5.0, f"body pattern spacing should be preserved: {body_pattern_signature}")
|
||||
_assert(body_pattern_signature.get("axis") == [0.0, 1.0, 0.0], f"body pattern axis should be preserved: {body_pattern_signature}")
|
||||
_assert(body_pattern_signature.get("bodyIndices") == [20, 21, 22], f"body pattern body indices should be preserved: {body_pattern_signature}")
|
||||
body_instances = body_pattern_signature.get("patternInstances") or []
|
||||
_assert(len(body_instances) == 3 and all(item.get("instanceKind") == "body" for item in body_instances), f"body pattern instances should stay body-level: {body_pattern_signature}")
|
||||
_assert(all(item.get("componentLocators") for item in body_instances if isinstance(item, dict)), f"body pattern instances should preserve component occurrence locators: {body_pattern_signature}")
|
||||
body_pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(301,), execution_ready={"pattern.spacing", "pattern.segment_spacing"})
|
||||
body_pattern_spacing = [
|
||||
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"
|
||||
]
|
||||
_assert(body_pattern_spacing, f"selecting a body member face should keep body pattern spacing as a recognized capability: {body_pattern_specs}")
|
||||
_assert(
|
||||
body_pattern_spacing[0].get("enabled") is True,
|
||||
f"body pattern spacing should be executable when every member has a component occurrence locator: {body_pattern_spacing}",
|
||||
)
|
||||
body_segment_specs = [
|
||||
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"
|
||||
]
|
||||
_assert(len(body_segment_specs) == 2, f"three body pattern members should expose two local spacing segments: {body_pattern_specs}")
|
||||
_assert(
|
||||
"零件" in str(body_segment_specs[0].get("label") or "")
|
||||
and body_segment_specs[0].get("enabled") is True
|
||||
and body_segment_specs[0].get("scdm_backend_operation") == "change_pattern_segment_spacing",
|
||||
f"local body pattern spacing should be executable and name the adjacent members: {body_segment_specs}",
|
||||
)
|
||||
body_segment_modes = body_segment_specs[0].get("scope_modes")
|
||||
_assert(
|
||||
isinstance(body_segment_modes, dict)
|
||||
and {"fix_left_move_right", "fix_right_move_left", "split_keep_center", "move_single_right"} <= set(body_segment_modes),
|
||||
f"local pattern spacing should expose several explicit modeling intents: {body_segment_specs}",
|
||||
)
|
||||
_assert(
|
||||
body_segment_modes["fix_left_move_right"].get("enabled") is True
|
||||
and body_segment_modes["fix_right_move_left"].get("enabled") is True
|
||||
and body_segment_modes["split_keep_center"].get("enabled") is True
|
||||
and body_segment_modes["move_single_right"].get("enabled") is False,
|
||||
f"implemented and blocked local spacing intents should be explicit: {body_segment_modes}",
|
||||
)
|
||||
for mode in body_segment_modes.values():
|
||||
if not isinstance(mode, dict):
|
||||
continue
|
||||
label = str(mode.get("label") or "").strip()
|
||||
tip = str(mode.get("enabled_tip") or mode.get("disabled_tip") or mode.get("range_hint") or "").strip()
|
||||
_assert(label and tip.startswith(label), f"modeling-intent tooltip should start with its intent label: {body_segment_modes}")
|
||||
_assert(
|
||||
"固定前项" in str(body_segment_specs[0].get("scope_text") or "")
|
||||
and "移动后侧" in str(body_segment_specs[0].get("scope_text") or ""),
|
||||
f"local pattern spacing should expose motion semantics in the intent column: {body_segment_specs}",
|
||||
)
|
||||
body_segment_signature = body_segment_specs[0].get("scdm_geometry_signature")
|
||||
_assert(
|
||||
isinstance(body_segment_signature, dict)
|
||||
and body_segment_signature.get("segmentIndex") == 0
|
||||
and body_segment_signature.get("movingSide") == "after",
|
||||
f"local segment specs should carry segment execution metadata: {body_segment_specs}",
|
||||
)
|
||||
pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(201,), execution_ready={"pattern.spacing", "pattern.segment_spacing"})
|
||||
pattern_spacing_spec = next((spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"), None)
|
||||
_assert(pattern_spacing_spec and pattern_spacing_spec.get("enabled") is True, f"pattern.spacing should enable for selected pattern member: {pattern_specs}")
|
||||
_assert(pattern_spacing_spec.get("value_type") == "positive", f"pattern.spacing should use positive numeric input: {pattern_specs}")
|
||||
face_segment_specs = [
|
||||
spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"
|
||||
]
|
||||
_assert(len(face_segment_specs) == 2, f"three face pattern members should expose two local spacing segments: {pattern_specs}")
|
||||
_assert(
|
||||
str(face_segment_specs[0].get("label") or "").startswith("Face"),
|
||||
f"face pattern local spacing should name the adjacent Face members instead of raw ordinal numbers: {face_segment_specs}",
|
||||
)
|
||||
not_productized = diagnostics.get("discovered_not_productized")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"round candidate should stay diagnostic only: {cache}")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"non-productized candidates should stay diagnostic only: {cache}")
|
||||
planned = diagnostics.get("planned_not_productized")
|
||||
_assert(isinstance(planned, list), f"planned diagnostics should be present: {cache}")
|
||||
planned_keys = {str(item.get("capabilityKey")) for item in planned if isinstance(item, dict)}
|
||||
expected_planned = {
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"boss.height",
|
||||
"boss.diameter",
|
||||
"round.radius",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"chamfer.distance",
|
||||
"pattern.spacing",
|
||||
"pattern.instance_position",
|
||||
"shell.thickness",
|
||||
}
|
||||
|
||||
@@ -44,6 +44,29 @@ def _hole(object_id: str, face_id: int, *, diameter: float, center: tuple[float,
|
||||
}
|
||||
|
||||
|
||||
def _feature(
|
||||
object_id: str,
|
||||
object_type: str,
|
||||
face_id: int,
|
||||
*,
|
||||
center: tuple[float, float, float],
|
||||
capability_key: str,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"objectId": object_id,
|
||||
"objectType": object_type,
|
||||
"geometrySignature": {
|
||||
"objectType": object_type,
|
||||
"faceIds": [face_id],
|
||||
"center": list(center),
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"capabilities": [
|
||||
{"key": capability_key, "currentValue": list(center)},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _cache(*objects: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
@@ -128,6 +151,419 @@ def main() -> int:
|
||||
position_check = check_scdm_target(position_after["objects"][0], capability_key="hole.position", expected_target=[2.0, 1.0, 6.0])
|
||||
_assert(position_check.get("ok") is True, f"position target should pass: {position_check}")
|
||||
|
||||
before_slot = _cache(_feature("slot:30", "slot", 30, center=(1.0, 2.0, 3.0), capability_key="slot.position"))
|
||||
after_slot = _cache(_feature("slot:40", "slot", 40, center=(1.0, 2.0, 6.0), capability_key="slot.position"))
|
||||
slot_before_signature = before_slot["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
slot_match = match_scdm_object_by_signature(slot_before_signature, after_slot, capability_key="slot.position")
|
||||
_assert(slot_match.get("status") == "unique", f"moved slot should match without old center lock: {slot_match}")
|
||||
slot_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=slot_before_signature,
|
||||
before_cache=before_slot,
|
||||
after_cache=after_slot,
|
||||
capability_key="slot.position",
|
||||
expected_target=[1.0, 2.0, 6.0],
|
||||
edited_object_id="slot:30",
|
||||
)
|
||||
_assert(slot_ok.get("ok") is True, f"slot.position result should validate target center: {slot_ok}")
|
||||
_assert(slot_ok.get("targetCheck", {}).get("ok") is True, f"slot target should be checked: {slot_ok}")
|
||||
before_slot_width = _cache(
|
||||
{
|
||||
"objectId": "slot:30",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [30],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"width": 2.0,
|
||||
},
|
||||
"capabilities": [{"key": "slot.width", "currentValue": 2.0}],
|
||||
}
|
||||
)
|
||||
after_slot_width = _cache(
|
||||
{
|
||||
"objectId": "slot:40",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [40],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"width": 2.5,
|
||||
},
|
||||
"capabilities": [{"key": "slot.width", "currentValue": 2.5}],
|
||||
}
|
||||
)
|
||||
slot_width_before_signature = before_slot_width["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
slot_width_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=slot_width_before_signature,
|
||||
before_cache=before_slot_width,
|
||||
after_cache=after_slot_width,
|
||||
capability_key="slot.width",
|
||||
expected_target=2.5,
|
||||
edited_object_id="slot:30",
|
||||
)
|
||||
_assert(slot_width_ok.get("ok") is True, f"slot.width result should validate target width: {slot_width_ok}")
|
||||
slot_width_mismatch = check_scdm_target(after_slot_width["objects"][0], capability_key="slot.width", expected_target=2.1) # type: ignore[index]
|
||||
_assert(slot_width_mismatch.get("ok") is False and slot_width_mismatch.get("reason") == "target-mismatch", f"slot.width mismatch should fail: {slot_width_mismatch}")
|
||||
before_slot_depth = _cache(
|
||||
{
|
||||
"objectId": "slot:31",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [31],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"depth": 1.5,
|
||||
"depthAxis": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"capabilities": [{"key": "slot.depth", "currentValue": 1.5}],
|
||||
}
|
||||
)
|
||||
after_slot_depth = _cache(
|
||||
{
|
||||
"objectId": "slot:41",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [41],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"depth": 2.0,
|
||||
"depthAxis": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"capabilities": [{"key": "slot.depth", "currentValue": 2.0}],
|
||||
}
|
||||
)
|
||||
slot_depth_before_signature = before_slot_depth["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
slot_depth_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=slot_depth_before_signature,
|
||||
before_cache=before_slot_depth,
|
||||
after_cache=after_slot_depth,
|
||||
capability_key="slot.depth",
|
||||
expected_target=2.0,
|
||||
edited_object_id="slot:31",
|
||||
)
|
||||
_assert(slot_depth_ok.get("ok") is True, f"slot.depth result should validate target depth: {slot_depth_ok}")
|
||||
slot_depth_mismatch = check_scdm_target(after_slot_depth["objects"][0], capability_key="slot.depth", expected_target=1.5) # type: ignore[index]
|
||||
_assert(slot_depth_mismatch.get("ok") is False and slot_depth_mismatch.get("reason") == "target-mismatch", f"slot.depth mismatch should fail: {slot_depth_mismatch}")
|
||||
|
||||
boss_after = _feature("boss:50", "cylindrical_boss", 50, center=(3.0, 0.0, 2.0), capability_key="boss.position")
|
||||
boss_check = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[3.0, 0.0, 2.0])
|
||||
_assert(boss_check.get("ok") is True, f"boss.position target should pass: {boss_check}")
|
||||
boss_mismatch = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[4.0, 0.0, 2.0])
|
||||
_assert(boss_mismatch.get("ok") is False and boss_mismatch.get("reason") == "target-mismatch", f"boss.position mismatch should fail: {boss_mismatch}")
|
||||
before_boss_height = _cache(
|
||||
{
|
||||
"objectId": "boss:50",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [50, 51, 52],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 3.0,
|
||||
"height": 4.0,
|
||||
},
|
||||
"capabilities": [{"key": "boss.height", "currentValue": 4.0}],
|
||||
}
|
||||
)
|
||||
after_boss_height = _cache(
|
||||
{
|
||||
"objectId": "boss:55",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [55, 56, 57],
|
||||
"center": [0.0, 0.0, 2.75],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 3.0,
|
||||
"height": 5.5,
|
||||
},
|
||||
"capabilities": [{"key": "boss.height", "currentValue": 5.5}],
|
||||
}
|
||||
)
|
||||
boss_height_signature = before_boss_height["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
boss_height_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=boss_height_signature,
|
||||
before_cache=before_boss_height,
|
||||
after_cache=after_boss_height,
|
||||
capability_key="boss.height",
|
||||
expected_target=5.5,
|
||||
edited_object_id="boss:50",
|
||||
)
|
||||
_assert(boss_height_ok.get("ok") is True, f"boss.height result should validate target height: {boss_height_ok}")
|
||||
boss_height_mismatch = check_scdm_target(after_boss_height["objects"][0], capability_key="boss.height", expected_target=4.5) # type: ignore[index]
|
||||
_assert(boss_height_mismatch.get("ok") is False and boss_height_mismatch.get("reason") == "target-mismatch", f"boss.height mismatch should fail: {boss_height_mismatch}")
|
||||
before_boss_diameter = _cache(
|
||||
{
|
||||
"objectId": "boss:60",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [60, 61, 62],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 3.0,
|
||||
"height": 4.0,
|
||||
},
|
||||
"capabilities": [{"key": "boss.diameter", "currentValue": 3.0}],
|
||||
}
|
||||
)
|
||||
after_boss_diameter = _cache(
|
||||
{
|
||||
"objectId": "boss:63",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [63, 64, 65],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 4.5,
|
||||
"height": 4.0,
|
||||
},
|
||||
"capabilities": [{"key": "boss.diameter", "currentValue": 4.5}],
|
||||
}
|
||||
)
|
||||
boss_diameter_signature = before_boss_diameter["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
boss_diameter_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=boss_diameter_signature,
|
||||
before_cache=before_boss_diameter,
|
||||
after_cache=after_boss_diameter,
|
||||
capability_key="boss.diameter",
|
||||
expected_target=4.5,
|
||||
edited_object_id="boss:60",
|
||||
)
|
||||
_assert(boss_diameter_ok.get("ok") is True, f"boss.diameter result should validate target diameter: {boss_diameter_ok}")
|
||||
boss_diameter_mismatch = check_scdm_target(after_boss_diameter["objects"][0], capability_key="boss.diameter", expected_target=3.5) # type: ignore[index]
|
||||
_assert(boss_diameter_mismatch.get("ok") is False and boss_diameter_mismatch.get("reason") == "target-mismatch", f"boss.diameter mismatch should fail: {boss_diameter_mismatch}")
|
||||
|
||||
before_round_radius = _cache(
|
||||
{
|
||||
"objectId": "round:70",
|
||||
"objectType": "round",
|
||||
"geometrySignature": {
|
||||
"objectType": "round",
|
||||
"faceIds": [70],
|
||||
"center": [1.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"radius": 0.5,
|
||||
"isConstantRound": True,
|
||||
},
|
||||
"capabilities": [{"key": "round.radius", "currentValue": 0.5}],
|
||||
}
|
||||
)
|
||||
after_round_radius = _cache(
|
||||
{
|
||||
"objectId": "round:71",
|
||||
"objectType": "round",
|
||||
"geometrySignature": {
|
||||
"objectType": "round",
|
||||
"faceIds": [71],
|
||||
"center": [1.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"radius": 0.75,
|
||||
"isConstantRound": True,
|
||||
},
|
||||
"capabilities": [{"key": "round.radius", "currentValue": 0.75}],
|
||||
}
|
||||
)
|
||||
round_radius_signature = before_round_radius["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
round_radius_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=round_radius_signature,
|
||||
before_cache=before_round_radius,
|
||||
after_cache=after_round_radius,
|
||||
capability_key="round.radius",
|
||||
expected_target=0.75,
|
||||
edited_object_id="round:70",
|
||||
)
|
||||
_assert(round_radius_ok.get("ok") is True, f"round.radius result should validate target radius: {round_radius_ok}")
|
||||
round_radius_mismatch = check_scdm_target(after_round_radius["objects"][0], capability_key="round.radius", expected_target=0.5) # type: ignore[index]
|
||||
_assert(round_radius_mismatch.get("ok") is False and round_radius_mismatch.get("reason") == "target-mismatch", f"round.radius mismatch should fail: {round_radius_mismatch}")
|
||||
|
||||
before_chamfer_distance = _cache(
|
||||
{
|
||||
"objectId": "chamfer:80",
|
||||
"objectType": "chamfer",
|
||||
"geometrySignature": {
|
||||
"objectType": "chamfer",
|
||||
"faceIds": [80],
|
||||
"center": [2.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"distance": 0.8,
|
||||
"isEqualDistanceChamfer": True,
|
||||
},
|
||||
"capabilities": [{"key": "chamfer.distance", "currentValue": 0.8}],
|
||||
}
|
||||
)
|
||||
after_chamfer_distance = _cache(
|
||||
{
|
||||
"objectId": "chamfer:81",
|
||||
"objectType": "chamfer",
|
||||
"geometrySignature": {
|
||||
"objectType": "chamfer",
|
||||
"faceIds": [81],
|
||||
"center": [2.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"distance": 1.2,
|
||||
"isEqualDistanceChamfer": True,
|
||||
},
|
||||
"capabilities": [{"key": "chamfer.distance", "currentValue": 1.2}],
|
||||
}
|
||||
)
|
||||
chamfer_distance_signature = before_chamfer_distance["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
chamfer_distance_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=chamfer_distance_signature,
|
||||
before_cache=before_chamfer_distance,
|
||||
after_cache=after_chamfer_distance,
|
||||
capability_key="chamfer.distance",
|
||||
expected_target=1.2,
|
||||
edited_object_id="chamfer:80",
|
||||
)
|
||||
_assert(chamfer_distance_ok.get("ok") is True, f"chamfer.distance result should validate target distance: {chamfer_distance_ok}")
|
||||
chamfer_distance_mismatch = check_scdm_target(after_chamfer_distance["objects"][0], capability_key="chamfer.distance", expected_target=0.8) # type: ignore[index]
|
||||
_assert(chamfer_distance_mismatch.get("ok") is False and chamfer_distance_mismatch.get("reason") == "target-mismatch", f"chamfer.distance mismatch should fail: {chamfer_distance_mismatch}")
|
||||
|
||||
before_pattern_spacing = _cache(
|
||||
{
|
||||
"objectId": "pattern:holes",
|
||||
"objectType": "linear_pattern",
|
||||
"geometrySignature": {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [85, 87, 89],
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
},
|
||||
"capabilities": [{"key": "pattern.spacing", "currentValue": 5.0}],
|
||||
}
|
||||
)
|
||||
after_pattern_spacing = _cache(
|
||||
{
|
||||
"objectId": "pattern:holes-new",
|
||||
"objectType": "linear_pattern",
|
||||
"geometrySignature": {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [90, 91, 92],
|
||||
"center": [7.5, 0.0, 0.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 7.5,
|
||||
"pitch": 7.5,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 0.0, 0.0], [7.5, 0.0, 0.0], [15.0, 0.0, 0.0]],
|
||||
},
|
||||
"capabilities": [{"key": "pattern.spacing", "currentValue": 7.5}],
|
||||
}
|
||||
)
|
||||
pattern_spacing_before_signature = before_pattern_spacing["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
pattern_spacing_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=pattern_spacing_before_signature,
|
||||
before_cache=before_pattern_spacing,
|
||||
after_cache=after_pattern_spacing,
|
||||
capability_key="pattern.spacing",
|
||||
expected_target=7.5,
|
||||
edited_object_id="pattern:holes",
|
||||
)
|
||||
_assert(pattern_spacing_ok.get("ok") is True, f"pattern.spacing result should validate target spacing: {pattern_spacing_ok}")
|
||||
pattern_spacing_mismatch = check_scdm_target(after_pattern_spacing["objects"][0], capability_key="pattern.spacing", expected_target=5.0) # type: ignore[index]
|
||||
_assert(pattern_spacing_mismatch.get("ok") is False and pattern_spacing_mismatch.get("reason") == "target-mismatch", f"pattern.spacing mismatch should fail: {pattern_spacing_mismatch}")
|
||||
before_pattern_segment = _cache(
|
||||
{
|
||||
"objectId": "pattern:holes",
|
||||
"objectType": "linear_pattern",
|
||||
"geometrySignature": {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [85, 87, 89],
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"segmentIndex": 1,
|
||||
"movingSide": "after",
|
||||
"patternInstances": [
|
||||
{"sourceObjectId": "a", "center": [0.0, 0.0, 0.0], "faceIds": [85]},
|
||||
{"sourceObjectId": "b", "center": [5.0, 0.0, 0.0], "faceIds": [87]},
|
||||
{"sourceObjectId": "c", "center": [10.0, 0.0, 0.0], "faceIds": [89]},
|
||||
],
|
||||
},
|
||||
"capabilities": [{"key": "pattern.segment_spacing", "currentValue": 5.0}],
|
||||
},
|
||||
_hole("hole:unrelated", 999, diameter=1.0, center=(100.0, 0.0, 0.0)),
|
||||
)
|
||||
after_pattern_segment = _cache(_hole("hole:unrelated-new", 999, diameter=1.0, center=(100.0, 0.0, 0.0)))
|
||||
pattern_segment_ok = validate_scdm_edit_result(
|
||||
{
|
||||
"ok": True,
|
||||
"output_step": str(output_step),
|
||||
"result": {
|
||||
"applied": {
|
||||
"segmentSpacing": 6.5,
|
||||
"targetSpacing": 6.5,
|
||||
"segmentIndex": 1,
|
||||
"spacingMode": "segment_after",
|
||||
}
|
||||
},
|
||||
},
|
||||
before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index]
|
||||
before_cache=before_pattern_segment,
|
||||
after_cache=after_pattern_segment,
|
||||
capability_key="pattern.segment_spacing",
|
||||
expected_target=6.5,
|
||||
edited_object_id="pattern:holes",
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_ok.get("ok") is True
|
||||
and pattern_segment_ok.get("targetCheck", {}).get("spacingMode") == "segment_after",
|
||||
f"pattern.segment_spacing should validate from the applied edit result even when the old uniform pattern no longer matches: {pattern_segment_ok}",
|
||||
)
|
||||
pattern_segment_mismatch = validate_scdm_edit_result(
|
||||
{
|
||||
"ok": True,
|
||||
"output_step": str(output_step),
|
||||
"applied": {"segmentSpacing": 6.0, "segmentIndex": 1, "spacingMode": "segment_after"},
|
||||
},
|
||||
before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index]
|
||||
before_cache=before_pattern_segment,
|
||||
after_cache=after_pattern_segment,
|
||||
capability_key="pattern.segment_spacing",
|
||||
expected_target=6.5,
|
||||
edited_object_id="pattern:holes",
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_mismatch.get("ok") is False and pattern_segment_mismatch.get("reason") == "target-mismatch",
|
||||
f"pattern.segment_spacing mismatch should fail from the applied edit result: {pattern_segment_mismatch}",
|
||||
)
|
||||
shell_thickness_ok = check_scdm_target(
|
||||
{
|
||||
"objectType": "thin_wall",
|
||||
"geometrySignature": {"objectType": "thin_wall", "thickness": 1.6},
|
||||
"capabilities": [{"key": "shell.thickness", "currentValue": 1.6}],
|
||||
},
|
||||
capability_key="shell.thickness",
|
||||
expected_target=1.6,
|
||||
)
|
||||
_assert(shell_thickness_ok.get("ok") is True, f"shell.thickness result should validate target thickness: {shell_thickness_ok}")
|
||||
shell_thickness_mismatch = check_scdm_target(
|
||||
{
|
||||
"objectType": "thin_wall",
|
||||
"geometrySignature": {"objectType": "thin_wall", "thickness": 1.6},
|
||||
"capabilities": [{"key": "shell.thickness", "currentValue": 1.6}],
|
||||
},
|
||||
capability_key="shell.thickness",
|
||||
expected_target=2.0,
|
||||
)
|
||||
_assert(shell_thickness_mismatch.get("ok") is False and shell_thickness_mismatch.get("reason") == "target-mismatch", f"shell.thickness mismatch should fail: {shell_thickness_mismatch}")
|
||||
|
||||
summary_before = _cache_with_summary(
|
||||
{"bodyCount": 13, "objectCount": 554, "faceCount": 158, "edgeCount": 396},
|
||||
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
||||
@@ -154,6 +590,57 @@ def main() -> int:
|
||||
summary_fill = check_scdm_summary_delta(summary_before, summary_after_bad, capability_key="feature.fill")
|
||||
_assert(summary_fill.get("ok") is None and summary_fill.get("reason") == "skipped-command-feature", f"fill should skip summary count guard: {summary_fill}")
|
||||
|
||||
fill_before = _cache(
|
||||
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
||||
_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)),
|
||||
)
|
||||
fill_after = _cache(_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)))
|
||||
fill_signature = fill_before["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
fill_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=fill_signature,
|
||||
before_cache=fill_before,
|
||||
after_cache=fill_after,
|
||||
capability_key="feature.fill",
|
||||
edited_object_id="hole:85",
|
||||
)
|
||||
_assert(fill_ok.get("ok") is True, f"feature.fill should pass when the edited feature disappears: {fill_ok}")
|
||||
_assert(fill_ok.get("removalCheck", {}).get("ok") is True, f"feature.fill should record removal evidence: {fill_ok}")
|
||||
fill_still_present = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=fill_signature,
|
||||
before_cache=fill_before,
|
||||
after_cache=fill_before,
|
||||
capability_key="feature.fill",
|
||||
edited_object_id="hole:85",
|
||||
)
|
||||
_assert(
|
||||
fill_still_present.get("ok") is False and fill_still_present.get("reason") == "feature-still-present",
|
||||
f"feature.fill should fail when the edited feature still matches: {fill_still_present}",
|
||||
)
|
||||
fill_no_cache = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=fill_signature,
|
||||
capability_key="feature.fill",
|
||||
)
|
||||
_assert(
|
||||
fill_no_cache.get("ok") is False and fill_no_cache.get("reason") == "removal-check-unavailable",
|
||||
f"feature.fill should require a new cache for removal verification: {fill_no_cache}",
|
||||
)
|
||||
|
||||
round_before = _cache(_feature("round:60", "round", 60, center=(1.0, 0.0, 2.0), capability_key="feature.delete_round_or_chamfer"))
|
||||
round_after = _cache(_feature("hole:85", "hole", 85, center=(5.0, 0.0, 2.0), capability_key="hole.diameter"))
|
||||
round_delete_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=round_before["objects"][0]["geometrySignature"], # type: ignore[index]
|
||||
before_cache=round_before,
|
||||
after_cache=round_after,
|
||||
capability_key="feature.delete_round_or_chamfer",
|
||||
edited_object_id="round:60",
|
||||
)
|
||||
_assert(round_delete_ok.get("ok") is True, f"round/chamfer delete should pass when the edited feature disappears: {round_delete_ok}")
|
||||
_assert(round_delete_ok.get("targetCheck", {}).get("reason") == "removed", f"round/chamfer delete should use removal target check: {round_delete_ok}")
|
||||
|
||||
ambiguous_after = _cache(
|
||||
_hole("hole:100", 100, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
||||
_hole("hole:101", 101, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
||||
|
||||
@@ -100,8 +100,8 @@ def main() -> int:
|
||||
"confidence": "low",
|
||||
},
|
||||
{
|
||||
"capabilityKey": "pattern.spacing",
|
||||
"displayName": "阵列间距",
|
||||
"capabilityKey": "pattern.instance_position",
|
||||
"displayName": "阵列实例位置",
|
||||
"evidenceCount": 2,
|
||||
"confidence": "low",
|
||||
},
|
||||
@@ -114,9 +114,7 @@ def main() -> int:
|
||||
}
|
||||
],
|
||||
"planned_not_productized": [
|
||||
{"capabilityKey": "slot.width", "objectType": "slot"},
|
||||
{"capabilityKey": "slot.width", "objectType": "slot"},
|
||||
{"capabilityKey": "round.radius", "objectType": "round"},
|
||||
{"capabilityKey": "shell.thickness", "objectType": "shell"},
|
||||
],
|
||||
"discovered_not_productized": [
|
||||
{"objectType": "mystery_feature"},
|
||||
@@ -126,14 +124,30 @@ def main() -> int:
|
||||
}
|
||||
progress = summarize_scdm_capability_progress(
|
||||
feature_cache=progress_cache,
|
||||
execution_ready={"face.offset", "hole.diameter", "hole.position", "slot.position", "boss.position"},
|
||||
execution_ready={
|
||||
"face.offset",
|
||||
"hole.diameter",
|
||||
"hole.position",
|
||||
"feature.fill",
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"slot.position",
|
||||
"boss.diameter",
|
||||
"boss.height",
|
||||
"boss.position",
|
||||
"round.radius",
|
||||
"chamfer.distance",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"pattern.spacing",
|
||||
"pattern.segment_spacing",
|
||||
},
|
||||
)
|
||||
summary = progress.get("summary")
|
||||
_assert(isinstance(summary, dict), f"capability progress should include summary: {progress}")
|
||||
_assert(summary.get("productized") == 6, f"productized capability count should include S5 plus Move-based S7 entries: {summary}")
|
||||
_assert(summary.get("runnerReady") == 5, f"runner-ready capability count should honor UI gate: {summary}")
|
||||
_assert(summary.get("executableCapabilities") == 2, f"blocked/ungated capabilities should not be executable: {summary}")
|
||||
_assert(summary.get("plannedDetected") == 3, f"planned S7 detections should be counted: {summary}")
|
||||
_assert(summary.get("productized") == 15, f"productized capability count should include S5 plus slot dimensions, boss dimensions, round/chamfer dimensions, pattern spacing, local segment spacing, Move-based S7 entries and round/chamfer delete: {summary}")
|
||||
_assert(summary.get("runnerReady") == 15, f"runner-ready capability count should honor UI gate: {summary}")
|
||||
_assert(summary.get("executableCapabilities") == 3, f"blocked/ungated capabilities should not be executable: {summary}")
|
||||
_assert(summary.get("plannedDetected") == 1, f"planned S7 detections should be counted: {summary}")
|
||||
_assert(summary.get("discoveredNotProductized") == 2, f"unknown discoveries should be counted: {summary}")
|
||||
_assert(summary.get("faceAdjacency") == 2 and summary.get("circularEdges") == 4, f"probe topology evidence should be counted: {summary}")
|
||||
_assert(
|
||||
@@ -147,9 +161,16 @@ def main() -> int:
|
||||
evidence_lines = "\n".join(str(line) for line in progress.get("probeEvidence", {}).get("lines", []))
|
||||
_assert("偏移:已开放" in productized_lines, f"open SCDM capability should be visible: {productized_lines}")
|
||||
_assert("直径:已开放但被后端阻止" in productized_lines, f"blocked SCDM capability should be explicit: {productized_lines}")
|
||||
_assert("填孔/删除小特征:已识别待执行器" in productized_lines, f"recognized but ungated capability should be visible: {productized_lines}")
|
||||
_assert("槽宽:已识别待验证" in planned_lines, f"planned S7 candidate should be visible: {planned_lines}")
|
||||
_assert("凸台高度:几何证据待分类" in planned_lines, f"geometry-only S7 hint should be visible: {planned_lines}")
|
||||
_assert("填孔/删除小特征:已开放" in productized_lines, f"recognized command capability should be visible as open: {productized_lines}")
|
||||
_assert("槽深:已开放待识别" in productized_lines, f"productized slot.depth should be visible: {productized_lines}")
|
||||
_assert("凸台直径:已开放待识别" in productized_lines, f"productized boss.diameter should be visible: {productized_lines}")
|
||||
_assert("凸台高度:已开放待识别" in productized_lines, f"productized boss.height should be visible: {productized_lines}")
|
||||
_assert("圆角半径:已开放待识别" in productized_lines, f"productized round.radius should be visible: {productized_lines}")
|
||||
_assert("倒角距离:已开放待识别" in productized_lines, f"productized chamfer.distance should be visible: {productized_lines}")
|
||||
_assert("阵列间距:已开放待识别" in productized_lines, f"productized pattern.spacing should be visible: {productized_lines}")
|
||||
_assert("局部间距:已开放待识别" in productized_lines, f"productized pattern.segment_spacing should be visible: {productized_lines}")
|
||||
_assert("壳体厚度:已识别待验证" in planned_lines, f"planned shell.thickness candidate should be visible: {planned_lines}")
|
||||
_assert("阵列实例位置:几何证据待分类" in planned_lines, f"geometry-only S7 hint should be visible: {planned_lines}")
|
||||
_assert("Face 邻接 2 组" in evidence_lines and "圆边 4 条" in evidence_lines, f"probe evidence lines should be readable: {evidence_lines}")
|
||||
_assert("对象分布" in evidence_lines and "命令候选分布" in evidence_lines, f"probe inventory lines should be readable: {evidence_lines}")
|
||||
_assert("几何候选 凸台高度:3" in evidence_lines, f"probe geometry hint lines should be readable: {evidence_lines}")
|
||||
|
||||
+19
-3
@@ -274,7 +274,23 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.scdm_feature_cache_state = "empty"
|
||||
self.scdm_feature_cache_message = ""
|
||||
self.scdm_feature_cache_path = ""
|
||||
self.scdm_edit_runner_ready = {"face.offset", "hole.diameter", "hole.position", "slot.position", "boss.position"}
|
||||
self.scdm_edit_runner_ready = {
|
||||
"face.offset",
|
||||
"hole.diameter",
|
||||
"hole.position",
|
||||
"feature.fill",
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"slot.position",
|
||||
"boss.diameter",
|
||||
"boss.height",
|
||||
"boss.position",
|
||||
"round.radius",
|
||||
"chamfer.distance",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"pattern.spacing",
|
||||
"pattern.segment_spacing",
|
||||
}
|
||||
self.load_in_progress = False
|
||||
self.load_thread: QThread | None = None
|
||||
self.load_worker: LoadWorker | None = None
|
||||
@@ -1050,7 +1066,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
|
||||
mode_box = QWidget()
|
||||
mode_box.setMinimumHeight(62)
|
||||
help_tip(mode_box, "决定鼠标点模型时选中零件、Solid、Face、Edge,还是识别几何特征。")
|
||||
help_tip(mode_box, "决定鼠标点模型时选中 Part、Solid、Face、Edge,还是用 Feature 模式把点到的 Face 解释成孔、槽、圆角等特征。")
|
||||
self.mode_section_title = QLabel("选择模式", mode_box)
|
||||
self.mode_section_title.setObjectName("modeSectionTitle")
|
||||
self.mode_section_title.setFixedSize(74, 20)
|
||||
@@ -1085,7 +1101,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.mode_combo.setMaximumWidth(112)
|
||||
help_tip(
|
||||
self.mode_combo,
|
||||
"选择模式决定鼠标点击模型时要选什么:零件、Solid、Face、Edge,或把 Face 解释成孔/槽/圆角等几何特征候选。",
|
||||
"选择模式决定鼠标点击模型时要选什么:Part、Solid、Face、Edge,或用 Feature 模式把点到的 Face 解释成孔、槽、圆角等特征。",
|
||||
)
|
||||
self.mode_combo.currentIndexChanged.connect(lambda _index: self._on_mode_changed(self._current_selection_mode()))
|
||||
mode_pick_layout.addWidget(self.mouse_mode_label)
|
||||
|
||||
@@ -229,17 +229,34 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
def scdm_local_face_signatures(self) -> list[dict[str, object]]:
|
||||
"""Return cheap geometry hints used to map SCDM raw objects back to local Face IDs."""
|
||||
signatures: list[dict[str, object]] = []
|
||||
face_ordinal_by_solid: dict[int, int] = {}
|
||||
for face_id, face in enumerate(self.faces):
|
||||
try:
|
||||
surf = BRepAdaptor_Surface(face)
|
||||
surface_type = surf.GetType()
|
||||
except Exception:
|
||||
continue
|
||||
solid_id = int(self.face_solid_ids[face_id]) if face_id < len(self.face_solid_ids) else -1
|
||||
part_id = int(self.face_part_ids[face_id]) if face_id < len(self.face_part_ids) else -1
|
||||
face_ordinal = face_ordinal_by_solid.get(solid_id, 0)
|
||||
face_ordinal_by_solid[solid_id] = face_ordinal + 1
|
||||
signature: dict[str, object] = {
|
||||
"faceId": int(face_id),
|
||||
"logicalFaceId": self.face_logical_id(face_id),
|
||||
"partId": part_id,
|
||||
"solidId": solid_id,
|
||||
"bodyIndex": solid_id,
|
||||
"faceOrdinal": face_ordinal,
|
||||
"globalFaceOrdinal": int(face_id),
|
||||
"surfaceType": SURFACE_TYPES.get(surface_type, f"type {surface_type}"),
|
||||
}
|
||||
try:
|
||||
bounds = _shape_bounds_info(face)
|
||||
signature["bboxMin"] = bounds.get("bbox_min")
|
||||
signature["bboxMax"] = bounds.get("bbox_max")
|
||||
signature["bboxSize"] = bounds.get("bbox_size")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if surface_type == GeomAbs_Plane:
|
||||
plane = surf.Plane()
|
||||
|
||||
@@ -144,6 +144,44 @@ def evaluate_relation_formula(
|
||||
return _coerce_formula_value(value)
|
||||
|
||||
|
||||
def validate_relation_formula_graph(formulas: Iterable[RelationFormula]) -> None:
|
||||
target_to_formula: dict[str, RelationFormula] = {}
|
||||
for formula in formulas:
|
||||
target_token = formula.target.token
|
||||
if target_token in target_to_formula:
|
||||
raise RelationFormulaError(f"同一目标参数只能由一条关系式控制:{target_token}。")
|
||||
target_to_formula[target_token] = formula
|
||||
|
||||
target_tokens = set(target_to_formula)
|
||||
graph: dict[str, list[str]] = {}
|
||||
for target_token, formula in target_to_formula.items():
|
||||
reference_tokens = [ref.token for ref in formula.references]
|
||||
if target_token in reference_tokens:
|
||||
raise RelationFormulaError(f"关系式不能引用自身:{target_token}。")
|
||||
graph[target_token] = [ref_token for ref_token in reference_tokens if ref_token in target_tokens]
|
||||
|
||||
visit_state: dict[str, str] = {}
|
||||
stack: list[str] = []
|
||||
|
||||
def visit(token: str) -> None:
|
||||
state = visit_state.get(token)
|
||||
if state == "visiting":
|
||||
start_index = stack.index(token) if token in stack else 0
|
||||
cycle = [*stack[start_index:], token]
|
||||
raise RelationFormulaError(f"关系式存在循环依赖:{' -> '.join(cycle)}。")
|
||||
if state == "visited":
|
||||
return
|
||||
visit_state[token] = "visiting"
|
||||
stack.append(token)
|
||||
for dependency in graph.get(token, []):
|
||||
visit(dependency)
|
||||
stack.pop()
|
||||
visit_state[token] = "visited"
|
||||
|
||||
for target_token in graph:
|
||||
visit(target_token)
|
||||
|
||||
|
||||
def relation_value_to_text(value: object) -> str:
|
||||
value = _coerce_formula_value(value)
|
||||
if isinstance(value, Vector3):
|
||||
|
||||
@@ -94,22 +94,20 @@ CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
||||
default_intent="修改槽宽",
|
||||
backend_operation="change_slot_width",
|
||||
post_check="target_slot_width",
|
||||
productized=False,
|
||||
required_backend_command_groups=(("OffsetFaces",),),
|
||||
roadmap_stage="S7.2",
|
||||
block_reason="槽宽属于 S7 第二批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"slot.depth": ScdmCapabilityDefinition(
|
||||
key="slot.depth",
|
||||
display_name="槽深",
|
||||
object_types=("slot", "obround_slot", "rectangular_slot"),
|
||||
value_kind="number",
|
||||
current_fields=("geometry.depth",),
|
||||
current_fields=("geometry.slotInfo.depth", "geometry.depth"),
|
||||
default_intent="修改槽深",
|
||||
backend_operation="change_slot_depth",
|
||||
post_check="target_slot_depth",
|
||||
productized=False,
|
||||
required_backend_command_groups=(("Move",), ("OffsetFaces",)),
|
||||
roadmap_stage="S7.2",
|
||||
block_reason="槽深属于 S7 第二批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"slot.position": ScdmCapabilityDefinition(
|
||||
key="slot.position",
|
||||
@@ -132,9 +130,8 @@ CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
||||
default_intent="修改凸台高度",
|
||||
backend_operation="change_boss_height",
|
||||
post_check="target_boss_height",
|
||||
productized=False,
|
||||
required_backend_command_groups=(("Move",), ("OffsetFaces",)),
|
||||
roadmap_stage="S7.3",
|
||||
block_reason="凸台高度属于 S7 第三批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"boss.diameter": ScdmCapabilityDefinition(
|
||||
key="boss.diameter",
|
||||
@@ -145,9 +142,8 @@ CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
||||
default_intent="修改凸台直径",
|
||||
backend_operation="change_boss_diameter",
|
||||
post_check="target_boss_diameter",
|
||||
productized=False,
|
||||
required_backend_command_groups=(("OffsetFaces",),),
|
||||
roadmap_stage="S7.3",
|
||||
block_reason="凸台直径属于 S7 第三批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"boss.position": ScdmCapabilityDefinition(
|
||||
key="boss.position",
|
||||
@@ -166,28 +162,24 @@ CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
||||
display_name="圆角半径",
|
||||
object_types=("round", "fillet"),
|
||||
value_kind="number",
|
||||
current_fields=("geometry.radius",),
|
||||
current_fields=("geometry.roundInfo.radius", "geometry.radius"),
|
||||
default_intent="修改圆角半径",
|
||||
backend_operation="change_round_radius",
|
||||
post_check="target_round_radius",
|
||||
required_backend_command_groups=(("ConstantRound",),),
|
||||
productized=False,
|
||||
roadmap_stage="S7.4",
|
||||
block_reason="圆角半径属于 S7 第四批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"chamfer.distance": ScdmCapabilityDefinition(
|
||||
key="chamfer.distance",
|
||||
display_name="倒角距离",
|
||||
object_types=("chamfer",),
|
||||
value_kind="number",
|
||||
current_fields=("geometry.distance", "geometry.offset"),
|
||||
current_fields=("geometry.chamferInfo.distance", "geometry.distance", "geometry.offset"),
|
||||
default_intent="修改倒角距离",
|
||||
backend_operation="change_chamfer_distance",
|
||||
post_check="target_chamfer_distance",
|
||||
required_backend_command_groups=(("Chamfer",),),
|
||||
productized=False,
|
||||
roadmap_stage="S7.4",
|
||||
block_reason="倒角距离属于 S7 第四批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"feature.delete_round_or_chamfer": ScdmCapabilityDefinition(
|
||||
key="feature.delete_round_or_chamfer",
|
||||
@@ -199,9 +191,7 @@ CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
||||
backend_operation="delete_round_or_chamfer",
|
||||
post_check="target_feature_removed",
|
||||
required_backend_command_groups=(("Fill",), ("Delete",)),
|
||||
productized=False,
|
||||
roadmap_stage="S7.4",
|
||||
block_reason="删除圆角/倒角属于 S7 第四批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"pattern.spacing": ScdmCapabilityDefinition(
|
||||
key="pattern.spacing",
|
||||
@@ -212,9 +202,20 @@ CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
||||
default_intent="修改阵列间距",
|
||||
backend_operation="change_pattern_spacing",
|
||||
post_check="target_pattern_spacing",
|
||||
productized=False,
|
||||
required_backend_command_groups=(("Move",),),
|
||||
roadmap_stage="S7.5",
|
||||
),
|
||||
"pattern.segment_spacing": ScdmCapabilityDefinition(
|
||||
key="pattern.segment_spacing",
|
||||
display_name="局部间距",
|
||||
object_types=("pattern", "linear_pattern"),
|
||||
value_kind="number",
|
||||
current_fields=("geometry.spacing", "geometry.pitch"),
|
||||
default_intent="修改相邻阵列成员间距",
|
||||
backend_operation="change_pattern_segment_spacing",
|
||||
post_check="target_pattern_segment_spacing",
|
||||
required_backend_command_groups=(("Move",),),
|
||||
roadmap_stage="S7.5",
|
||||
block_reason="阵列间距属于 S7 第五批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
||||
),
|
||||
"pattern.instance_position": ScdmCapabilityDefinition(
|
||||
key="pattern.instance_position",
|
||||
@@ -318,6 +319,7 @@ def capability_keys_for_raw_object(raw_object: Mapping[str, object], *, include_
|
||||
if object_type in {"pattern", "linear_pattern"}:
|
||||
if _has_any(geometry, ("spacing", "pitch")) or _has_command_token(commands, ("pattern_spacing", "spacing", "pitch")):
|
||||
keys.append("pattern.spacing")
|
||||
keys.append("pattern.segment_spacing")
|
||||
if _has_any(geometry, ("instanceCenter", "center")) or _has_command_token(commands, ("move_instance", "instance_position")):
|
||||
keys.append("pattern.instance_position")
|
||||
|
||||
|
||||
+1020
-1
File diff suppressed because it is too large
Load Diff
+1173
-11
File diff suppressed because it is too large
Load Diff
+296
-4
@@ -241,9 +241,18 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" geometry['surfaceType'] = 'plane'\n"
|
||||
" elif 'cylinder' in lowered:\n"
|
||||
" geometry['surfaceType'] = 'cylinder'\n"
|
||||
" slot_info = _slot_info_from_face(face, geometry)\n"
|
||||
" if slot_info:\n"
|
||||
" geometry['slotInfo'] = slot_info\n"
|
||||
" for key in ('width', 'depth', 'center', 'depthAxis'):\n"
|
||||
" if slot_info.get(key) is not None:\n"
|
||||
" geometry[key] = slot_info.get(key)\n"
|
||||
" round_info = _round_info_from_face(face, geometry)\n"
|
||||
" if round_info:\n"
|
||||
" geometry['roundInfo'] = round_info\n"
|
||||
" chamfer_info = _chamfer_info_from_face(face, geometry)\n"
|
||||
" if chamfer_info:\n"
|
||||
" geometry['chamferInfo'] = chamfer_info\n"
|
||||
" return geometry\n"
|
||||
"\n"
|
||||
"def _round_info_from_face(face, geometry):\n"
|
||||
@@ -274,6 +283,101 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" pass\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _same_object(left, right):\n"
|
||||
" try:\n"
|
||||
" if left is right:\n"
|
||||
" return True\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" return left == right\n"
|
||||
" except Exception:\n"
|
||||
" return False\n"
|
||||
"\n"
|
||||
"def _slot_info_from_face(face, geometry):\n"
|
||||
" slot_info_type = globals().get('SlotInfo')\n"
|
||||
" if slot_info_type is None:\n"
|
||||
" return {}\n"
|
||||
" try:\n"
|
||||
" info = slot_info_type.Create(face)\n"
|
||||
" except Exception:\n"
|
||||
" return {}\n"
|
||||
" payload = {'available': True, 'type': _safe_name(info)}\n"
|
||||
" for key, attrs in (\n"
|
||||
" ('width', ('Width', 'SlotWidth', 'Diameter')),\n"
|
||||
" ('depth', ('Depth', 'SlotDepth', 'Height')),\n"
|
||||
" ):\n"
|
||||
" value = _float_attr(info, attrs)\n"
|
||||
" if value is not None:\n"
|
||||
" payload[key] = value\n"
|
||||
" center = _xyz(_first_path_value(info, ('Center', 'AxisCenter', 'Frame.Origin')))\n"
|
||||
" if center:\n"
|
||||
" payload['center'] = center\n"
|
||||
" depth_axis = _xyz(_first_path_value(info, ('DepthAxis', 'DepthDirection', 'Direction', 'Frame.DirZ')))\n"
|
||||
" if depth_axis:\n"
|
||||
" payload['depthAxis'] = depth_axis\n"
|
||||
" for attr in ('IsBlind', 'IsThrough', 'IsSlot'):\n"
|
||||
" try:\n"
|
||||
" payload[attr[0].lower() + attr[1:]] = bool(getattr(info, attr))\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" for attr in ('BottomFace', 'DepthFace', 'FloorFace'):\n"
|
||||
" try:\n"
|
||||
" if _same_object(getattr(info, attr), face):\n"
|
||||
" payload['depthFaceIsCurrent'] = True\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" for attr in ('BottomFaces', 'DepthFaces', 'FloorFaces'):\n"
|
||||
" try:\n"
|
||||
" for item in _items(getattr(info, attr)):\n"
|
||||
" if _same_object(item, face):\n"
|
||||
" payload['depthFaceIsCurrent'] = True\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" payload['attributes'] = [name for name in dir(info) if not name.startswith('_')][:40]\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _chamfer_info_from_face(face, geometry):\n"
|
||||
" if str(geometry.get('surfaceType', '')).lower() != 'plane':\n"
|
||||
" return {}\n"
|
||||
" chamfer_info_type = globals().get('ChamferInfo')\n"
|
||||
" if chamfer_info_type is None:\n"
|
||||
" return {}\n"
|
||||
" try:\n"
|
||||
" info = chamfer_info_type.Create(face)\n"
|
||||
" except Exception:\n"
|
||||
" return {}\n"
|
||||
" payload = {'available': True, 'type': _safe_name(info)}\n"
|
||||
" for attr in ('Distance', 'ChamferDistance', 'Offset', 'Width'):\n"
|
||||
" value = _float_attr(info, (attr, attr[0].lower() + attr[1:]))\n"
|
||||
" if value is not None:\n"
|
||||
" payload['distance'] = value\n"
|
||||
" break\n"
|
||||
" distance1 = _float_attr(info, ('Distance1', 'distance1', 'FirstDistance'))\n"
|
||||
" distance2 = _float_attr(info, ('Distance2', 'distance2', 'SecondDistance'))\n"
|
||||
" if distance1 is not None:\n"
|
||||
" payload['distance1'] = distance1\n"
|
||||
" if distance2 is not None:\n"
|
||||
" payload['distance2'] = distance2\n"
|
||||
" if distance1 is not None and distance2 is not None and abs(distance1 - distance2) <= max(abs(distance1), abs(distance2), 1.0) * 1e-6:\n"
|
||||
" payload.setdefault('distance', distance1)\n"
|
||||
" payload['isEqualDistance'] = True\n"
|
||||
" for attr in ('IsEqualDistance', 'IsSymmetric', 'IsChamfer'):\n"
|
||||
" try:\n"
|
||||
" payload[attr[0].lower() + attr[1:]] = bool(getattr(info, attr))\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" if payload.get('isSymmetric') is True:\n"
|
||||
" payload['isEqualDistance'] = True\n"
|
||||
" try:\n"
|
||||
" payload['attributes'] = [name for name in dir(info) if not name.startswith('_')][:40]\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _path_value(value, expr):\n"
|
||||
" current = value\n"
|
||||
" for part in expr.split('.'):\n"
|
||||
@@ -462,10 +566,21 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" result.append({'operation': 'move_hole_axis', 'enabled': True, 'parameterFields': {'center': geometry.get('center')}})\n"
|
||||
" if object_type == 'hole' and surface_type == 'cylinder':\n"
|
||||
" result.append({'operation': 'fill_feature', 'enabled': True, 'parameterFields': {}})\n"
|
||||
" if object_type in ('slot', 'obround_slot', 'rectangular_slot'):\n"
|
||||
" if geometry.get('width') is not None:\n"
|
||||
" result.append({'operation': 'change_slot_width', 'enabled': True, 'parameterFields': {'width': geometry.get('width')}})\n"
|
||||
" if geometry.get('depth') is not None:\n"
|
||||
" result.append({'operation': 'change_slot_depth', 'enabled': True, 'parameterFields': {'depth': geometry.get('depth')}})\n"
|
||||
" if geometry.get('center') is not None:\n"
|
||||
" result.append({'operation': 'move_slot', 'enabled': True, 'parameterFields': {'center': geometry.get('center')}})\n"
|
||||
" round_info = geometry.get('roundInfo')\n"
|
||||
" if isinstance(round_info, dict) and round_info.get('radius') is not None:\n"
|
||||
" result.append({'operation': 'change_round_radius', 'enabled': True, 'parameterFields': {'radius': round_info.get('radius')}})\n"
|
||||
" result.append({'operation': 'delete_round_or_chamfer', 'enabled': True, 'parameterFields': {}})\n"
|
||||
" chamfer_info = geometry.get('chamferInfo')\n"
|
||||
" if isinstance(chamfer_info, dict) and chamfer_info.get('distance') is not None:\n"
|
||||
" result.append({'operation': 'change_chamfer_distance', 'enabled': True, 'parameterFields': {'distance': chamfer_info.get('distance')}})\n"
|
||||
" result.append({'operation': 'delete_round_or_chamfer', 'enabled': True, 'parameterFields': {}})\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _open_step(path):\n"
|
||||
@@ -503,6 +618,167 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" except Exception:\n"
|
||||
" return value\n"
|
||||
"\n"
|
||||
"def _safe_str(value):\n"
|
||||
" if value is None:\n"
|
||||
" return ''\n"
|
||||
" try:\n"
|
||||
" return str(value)\n"
|
||||
" except Exception:\n"
|
||||
" return _safe_name(value)\n"
|
||||
"\n"
|
||||
"def _matrix_payload(matrix):\n"
|
||||
" if matrix is None:\n"
|
||||
" return {}\n"
|
||||
" payload = {'type': _safe_name(matrix), 'text': _safe_str(matrix)}\n"
|
||||
" translation = _xyz(_path_value(matrix, 'Translation'))\n"
|
||||
" if translation:\n"
|
||||
" payload['translation'] = translation\n"
|
||||
" for attr in ('OffsetX', 'OffsetY', 'OffsetZ'):\n"
|
||||
" value = _float_attr(matrix, (attr, attr[0].lower() + attr[1:]))\n"
|
||||
" if value is not None:\n"
|
||||
" payload[attr] = value\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _moniker_text(value):\n"
|
||||
" try:\n"
|
||||
" return _safe_str(getattr(value, 'Moniker'))\n"
|
||||
" except Exception:\n"
|
||||
" return ''\n"
|
||||
"\n"
|
||||
"def _component_name(component):\n"
|
||||
" for attr in ('Name', 'DisplayName'):\n"
|
||||
" try:\n"
|
||||
" text = _safe_str(getattr(component, attr)).strip()\n"
|
||||
" if text:\n"
|
||||
" return text\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return ''\n"
|
||||
"\n"
|
||||
"def _immediate_components(part):\n"
|
||||
" if part is None:\n"
|
||||
" return []\n"
|
||||
" try:\n"
|
||||
" items = _items(getattr(part, 'Components'))\n"
|
||||
" if items:\n"
|
||||
" return items\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return []\n"
|
||||
"\n"
|
||||
"def _component_content(component):\n"
|
||||
" for attr in ('Content', 'ContentMaster', 'Template', 'Part'):\n"
|
||||
" try:\n"
|
||||
" value = getattr(component, attr)\n"
|
||||
" if value is not None:\n"
|
||||
" return value\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return None\n"
|
||||
"\n"
|
||||
"def _component_locator(component, component_index, component_path):\n"
|
||||
" locator = {\n"
|
||||
" 'backendId': 'component:' + '.'.join(str(item) for item in component_path),\n"
|
||||
" 'componentIndex': component_index,\n"
|
||||
" 'componentPath': list(component_path),\n"
|
||||
" 'componentName': _component_name(component),\n"
|
||||
" }\n"
|
||||
" try:\n"
|
||||
" locator['componentMoniker'] = _moniker_text(component)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" content = getattr(component, 'Content')\n"
|
||||
" locator['contentMoniker'] = _moniker_text(content)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" template = getattr(component, 'Template')\n"
|
||||
" locator['templateMoniker'] = _moniker_text(template)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" placement = _matrix_payload(getattr(component, 'Placement'))\n"
|
||||
" if placement:\n"
|
||||
" locator['placement'] = placement\n"
|
||||
" if placement.get('translation'):\n"
|
||||
" locator['placementTranslation'] = placement.get('translation')\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return locator\n"
|
||||
"\n"
|
||||
"def _component_entries(root):\n"
|
||||
" result = []\n"
|
||||
" queue = [(root, [])]\n"
|
||||
" while queue:\n"
|
||||
" part, path = queue.pop(0)\n"
|
||||
" if part is None or len(path) > 8:\n"
|
||||
" continue\n"
|
||||
" for child_index, component in enumerate(_immediate_components(part)):\n"
|
||||
" component_path = list(path) + [child_index]\n"
|
||||
" content = _component_content(component)\n"
|
||||
" entry = {\n"
|
||||
" 'component': component,\n"
|
||||
" 'content': content,\n"
|
||||
" 'locator': _component_locator(component, len(result), component_path),\n"
|
||||
" }\n"
|
||||
" result.append(entry)\n"
|
||||
" if content is not None:\n"
|
||||
" queue.append((content, component_path))\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _component_body_locator_map(component_entries):\n"
|
||||
" result = {}\n"
|
||||
" for entry in component_entries:\n"
|
||||
" content = entry.get('content')\n"
|
||||
" if content is None:\n"
|
||||
" continue\n"
|
||||
" for component_body_index, body in enumerate(_items(_maybe_call(content, 'Bodies'))):\n"
|
||||
" locator = dict(entry.get('locator') or {})\n"
|
||||
" locator['componentBodyIndex'] = component_body_index\n"
|
||||
" key = str(id(body))\n"
|
||||
" result.setdefault(key, []).append(locator)\n"
|
||||
" try:\n"
|
||||
" master = getattr(body, 'Master')\n"
|
||||
" result.setdefault(str(id(master)), []).append(locator)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _body_locators_for_body(component_body_locators, body, body_index):\n"
|
||||
" result = [{'bodyIndex': body_index}]\n"
|
||||
" seen = set(['body:' + str(body_index)])\n"
|
||||
" for locator in component_body_locators.get(str(id(body)), []) or []:\n"
|
||||
" item = dict(locator)\n"
|
||||
" item['bodyIndex'] = body_index\n"
|
||||
" key = str(item.get('componentIndex')) + ':' + '.'.join(str(value) for value in item.get('componentPath', []) or []) + ':' + str(item.get('componentBodyIndex'))\n"
|
||||
" if key in seen:\n"
|
||||
" continue\n"
|
||||
" seen.add(key)\n"
|
||||
" result.append(item)\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _component_locators_for_body(component_body_locators, body):\n"
|
||||
" result = []\n"
|
||||
" seen = set()\n"
|
||||
" for locator in component_body_locators.get(str(id(body)), []) or []:\n"
|
||||
" key = str(locator.get('componentIndex')) + ':' + '.'.join(str(value) for value in locator.get('componentPath', []) or []) + ':' + str(locator.get('componentBodyIndex'))\n"
|
||||
" if key in seen:\n"
|
||||
" continue\n"
|
||||
" seen.add(key)\n"
|
||||
" result.append(dict(locator))\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _component_inventory(component_entries):\n"
|
||||
" result = []\n"
|
||||
" for entry in component_entries:\n"
|
||||
" locator = dict(entry.get('locator') or {})\n"
|
||||
" content = entry.get('content')\n"
|
||||
" locator['contentBodyCount'] = len(_items(_maybe_call(content, 'Bodies'))) if content is not None else 0\n"
|
||||
" locator['childComponentCount'] = len(_immediate_components(content)) if content is not None else 0\n"
|
||||
" result.append(locator)\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _body_faces(body):\n"
|
||||
" for name in ('Faces', 'GetFaces'):\n"
|
||||
" items = _items(_maybe_call(body, name))\n"
|
||||
@@ -596,7 +872,7 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" return set(str(id(face)) for face in faces)\n"
|
||||
"\n"
|
||||
"def _available_commands():\n"
|
||||
" names = ('StandardHoles', 'OffsetFaces', 'Move', 'Fill', 'Delete', 'Chamfer', 'ConstantRound', 'RoundInfo')\n"
|
||||
" names = ('StandardHoles', 'OffsetFaces', 'Move', 'Fill', 'Delete', 'Chamfer', 'ConstantRound', 'RoundInfo', 'ChamferInfo', 'SlotInfo')\n"
|
||||
" result = []\n"
|
||||
" for name in names:\n"
|
||||
" result.append({'name': name, 'available': globals().get(name) is not None})\n"
|
||||
@@ -612,6 +888,8 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" _open_step(model.get('path'))\n"
|
||||
" root = _root_part()\n"
|
||||
" bodies = _all_bodies(root)\n"
|
||||
" component_entries = _component_entries(root)\n"
|
||||
" component_body_locators = _component_body_locator_map(component_entries)\n"
|
||||
" hole_face_markers = _hole_face_markers(bodies)\n"
|
||||
" objects = []\n"
|
||||
" face_adjacency = {}\n"
|
||||
@@ -620,24 +898,37 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" edge_counter = 0\n"
|
||||
" for body_index, body in enumerate(bodies):\n"
|
||||
" body_faces = _body_faces(body)\n"
|
||||
" body_locators = _body_locators_for_body(component_body_locators, body, body_index)\n"
|
||||
" component_locators = _component_locators_for_body(component_body_locators, body)\n"
|
||||
" face_ordinals_by_marker = dict((str(id(face)), index) for index, face in enumerate(body_faces))\n"
|
||||
" for face_index, face in enumerate(body_faces):\n"
|
||||
" geometry = _geometry_from_face(face)\n"
|
||||
" object_type = 'hole' if str(id(face)) in hole_face_markers else 'face'\n"
|
||||
" if object_type == 'face' and isinstance(geometry.get('slotInfo'), dict) and (geometry.get('depth') is not None or geometry.get('width') is not None):\n"
|
||||
" object_type = 'slot'\n"
|
||||
" if object_type == 'face' and isinstance(geometry.get('roundInfo'), dict) and geometry.get('roundInfo', {}).get('radius') is not None:\n"
|
||||
" object_type = 'round'\n"
|
||||
" if object_type == 'face' and isinstance(geometry.get('chamferInfo'), dict) and geometry.get('chamferInfo', {}).get('distance') is not None:\n"
|
||||
" object_type = 'chamfer'\n"
|
||||
" topology_hint = {'bodyIndex': body_index, 'faceOrdinal': face_index, 'globalFaceOrdinal': face_counter, 'bodyLocators': body_locators}\n"
|
||||
" if component_locators:\n"
|
||||
" topology_hint['componentLocators'] = component_locators\n"
|
||||
" if object_type == 'slot' and isinstance(geometry.get('slotInfo'), dict) and geometry.get('slotInfo', {}).get('depthFaceIsCurrent') is True:\n"
|
||||
" topology_hint['depthFaceLocators'] = [dict(topology_hint)]\n"
|
||||
" objects.append({\n"
|
||||
" 'backendId': 'body:%d/face:%d' % (body_index, face_index),\n"
|
||||
" 'objectType': object_type,\n"
|
||||
" 'geometry': geometry,\n"
|
||||
" 'topologyHint': {'bodyIndex': body_index, 'faceOrdinal': face_index, 'globalFaceOrdinal': face_counter},\n"
|
||||
" 'topologyHint': topology_hint,\n"
|
||||
" 'backendCommandCandidates': _command_candidates(object_type, geometry),\n"
|
||||
" 'rawLimitations': [],\n"
|
||||
" })\n"
|
||||
" face_counter += 1\n"
|
||||
" for edge_index, edge in enumerate(_body_edges(body)):\n"
|
||||
" geometry = _geometry_from_edge(edge)\n"
|
||||
" edge_topology = {'bodyIndex': body_index, 'edgeOrdinal': edge_index, 'globalEdgeOrdinal': edge_counter}\n"
|
||||
" edge_topology = {'bodyIndex': body_index, 'edgeOrdinal': edge_index, 'globalEdgeOrdinal': edge_counter, 'bodyLocators': body_locators}\n"
|
||||
" if component_locators:\n"
|
||||
" edge_topology['componentLocators'] = component_locators\n"
|
||||
" edge_topology.update(_edge_adjacent_face_ordinals(edge, face_ordinals_by_marker))\n"
|
||||
" _add_edge_geometry_summary(edge_geometry_summary, geometry)\n"
|
||||
" _record_face_adjacency(face_adjacency, body_index, edge_topology, geometry)\n"
|
||||
@@ -661,8 +952,9 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" 'faceAdjacency': _face_adjacency_rows(face_adjacency),\n"
|
||||
" 'edgeGeometrySummary': _final_edge_geometry_summary(edge_geometry_summary),\n"
|
||||
" 'featureInventory': _feature_inventory(objects),\n"
|
||||
" 'componentInstances': _component_inventory(component_entries),\n"
|
||||
" },\n"
|
||||
" 'summary': {'bodyCount': len(bodies), 'objectCount': len(objects), 'faceCount': face_counter, 'edgeCount': edge_counter, 'holeFaceCount': len(hole_face_markers)},\n"
|
||||
" 'summary': {'bodyCount': len(bodies), 'objectCount': len(objects), 'faceCount': face_counter, 'edgeCount': edge_counter, 'holeFaceCount': len(hole_face_markers), 'componentCount': len(component_entries)},\n"
|
||||
" }\n"
|
||||
" _write_json(raw_path, payload)\n"
|
||||
" except Exception as exc:\n"
|
||||
|
||||
@@ -27,9 +27,12 @@ def property_specs_from_scdm_cache(
|
||||
continue
|
||||
for capability in capabilities:
|
||||
if isinstance(capability, Mapping):
|
||||
if str(capability.get("key") or "") == "pattern.segment_spacing":
|
||||
continue
|
||||
spec = _capability_spec(item, capability, execution_ready=execution_ready)
|
||||
if spec is not None:
|
||||
specs.append(spec)
|
||||
specs.extend(_pattern_segment_spacing_specs(item, execution_ready=execution_ready))
|
||||
return specs
|
||||
|
||||
|
||||
@@ -38,6 +41,7 @@ def _object_matches(raw_object: Mapping[str, object], *, face_ids: set[int], edg
|
||||
if not isinstance(signature, Mapping):
|
||||
return False
|
||||
object_faces = set(_int_values(signature.get("faceIds")))
|
||||
object_faces.update(_int_values(signature.get("supportFaceIds")))
|
||||
object_edges = set(_int_values(signature.get("edgeIds")))
|
||||
return bool((face_ids and object_faces & face_ids) or (edge_ids and object_edges & edge_ids))
|
||||
|
||||
@@ -55,14 +59,23 @@ def _capability_spec(
|
||||
value_kind = str(capability.get("valueKind") or "number")
|
||||
current = capability.get("currentValue")
|
||||
value_type = _value_type(value_kind, key)
|
||||
signature = raw_object.get("geometrySignature") if isinstance(raw_object.get("geometrySignature"), Mapping) else {}
|
||||
unit_scale = _unit_scale(signature if isinstance(signature, Mapping) else {})
|
||||
current_display = _display_value(current, key=key, value_type=value_type, unit_scale=unit_scale)
|
||||
command_value = value_type == "command"
|
||||
current_text = "可执行" if command_value else _format_value(current, value_type=value_type)
|
||||
target_text = "执行" if command_value else _format_value(current, value_type=value_type)
|
||||
current_text = "可执行" if command_value else _format_value(current_display, value_type=value_type)
|
||||
target_text = "执行" if command_value else _format_value(current_display, value_type=value_type)
|
||||
capability_block = str(capability.get("blockReason") or "").strip()
|
||||
object_block = str(raw_object.get("blockReason") or "").strip()
|
||||
block_reason = capability_block or object_block
|
||||
if not command_value and not _current_value_available(current_display, value_type=value_type):
|
||||
block_reason = block_reason or f"SCDM 已识别“{label}”,但没有返回可用于编辑的当前值。"
|
||||
backend_operation = str(capability.get("backendOperation") or "")
|
||||
post_check = str(capability.get("postCheck") or "")
|
||||
max_value = _display_max_value(key=key, signature=signature if isinstance(signature, Mapping) else {}, unit_scale=unit_scale)
|
||||
range_hint = "来源:SCDM 结构化识别结果。执行前仍需生成 edit job,并在结果 STEP 上做 OCCT 校验和目标值回测。"
|
||||
if key == "pattern.spacing" and max_value is not None:
|
||||
range_hint = f"该阵列受承载面范围限制,保持阵列中心不变时最大间距约 {max_value:g};超过后会跑出承载面。"
|
||||
can_execute = bool(_capability_execution_ready(key, execution_ready) and capability.get("editable", True) and not block_reason)
|
||||
if can_execute:
|
||||
disabled_tip = ""
|
||||
@@ -79,7 +92,9 @@ def _capability_spec(
|
||||
return {
|
||||
"key": f"scdm:{key}",
|
||||
"label": label,
|
||||
"current_raw": current if current is not None else "",
|
||||
"current_raw": current_display if current_display is not None else "",
|
||||
"scdm_current_raw": current if current is not None else "",
|
||||
"scdm_unit_scale": unit_scale,
|
||||
"current_text": current_text,
|
||||
"target_text": target_text,
|
||||
"editable": True,
|
||||
@@ -90,24 +105,443 @@ def _capability_spec(
|
||||
"value_type": value_type,
|
||||
"enabled_tip": enabled_tip,
|
||||
"disabled_tip": disabled_tip,
|
||||
"range_hint": "来源:SCDM 结构化识别结果。执行前仍需生成 edit job,并在结果 STEP 上做 OCCT 校验和目标值回测。",
|
||||
"range_hint": range_hint,
|
||||
"min_value": 0.0 if value_type == "positive" else None,
|
||||
"min_exclusive": True if value_type == "positive" else False,
|
||||
"max_value": max_value,
|
||||
"scdm_object_id": raw_object.get("objectId"),
|
||||
"scdm_source_backend_id": raw_object.get("sourceBackendId"),
|
||||
"scdm_capability_key": key,
|
||||
"scdm_backend_operation": backend_operation,
|
||||
"scdm_post_check": post_check,
|
||||
"scdm_geometry_signature": raw_object.get("geometrySignature") if isinstance(raw_object.get("geometrySignature"), Mapping) else {},
|
||||
"scdm_geometry_signature": signature if isinstance(signature, Mapping) else {},
|
||||
}
|
||||
|
||||
|
||||
def _unit_scale(signature: Mapping[str, object]) -> float:
|
||||
try:
|
||||
value = float(str(signature.get("localUnitScale")).strip())
|
||||
except (TypeError, ValueError):
|
||||
return 1.0
|
||||
return value if value > 0 else 1.0
|
||||
|
||||
|
||||
def _display_value(value: object, *, key: str, value_type: str, unit_scale: float) -> object:
|
||||
if unit_scale <= 0 or abs(unit_scale - 1.0) <= 1.0e-12 or not _uses_length_units(key, value_type):
|
||||
return value
|
||||
if value_type == "vector3":
|
||||
values = _float_values(value)
|
||||
if len(values) == 3:
|
||||
return [item / unit_scale for item in values]
|
||||
return value
|
||||
try:
|
||||
return float(str(value).strip()) / unit_scale
|
||||
except (TypeError, ValueError):
|
||||
return value
|
||||
|
||||
|
||||
def _uses_length_units(key: str, value_type: str) -> bool:
|
||||
if value_type == "vector3":
|
||||
return True
|
||||
suffixes = (
|
||||
".diameter",
|
||||
".radius",
|
||||
".offset",
|
||||
".width",
|
||||
".depth",
|
||||
".height",
|
||||
".distance",
|
||||
".thickness",
|
||||
".spacing",
|
||||
".segment_spacing",
|
||||
".position",
|
||||
)
|
||||
return key.endswith(suffixes)
|
||||
|
||||
|
||||
def _display_max_value(*, key: str, signature: Mapping[str, object], unit_scale: float) -> float | None:
|
||||
if key != "pattern.spacing":
|
||||
return None
|
||||
fit = signature.get("supportPatternFit")
|
||||
if not isinstance(fit, Mapping):
|
||||
return None
|
||||
value = fit.get("maxSpacingLocal")
|
||||
try:
|
||||
result = float(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
backend_value = fit.get("maxSpacing")
|
||||
try:
|
||||
return float(str(backend_value).strip()) / unit_scale if unit_scale > 0 else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return result if result > 0 else None
|
||||
|
||||
|
||||
def _pattern_segment_spacing_specs(
|
||||
raw_object: Mapping[str, object],
|
||||
*,
|
||||
execution_ready: bool | Iterable[str],
|
||||
) -> list[dict[str, object]]:
|
||||
if str(raw_object.get("objectType") or "").strip().lower() != "linear_pattern":
|
||||
return []
|
||||
signature = raw_object.get("geometrySignature")
|
||||
if not isinstance(signature, Mapping):
|
||||
return []
|
||||
axis = _unit_vector(_float_values(signature.get("axis")))
|
||||
if len(axis) != 3:
|
||||
return []
|
||||
instances = _sorted_pattern_instances(signature, axis)
|
||||
if len(instances) < 2:
|
||||
return []
|
||||
unit_scale = _unit_scale(signature)
|
||||
can_execute = bool(_capability_execution_ready("pattern.segment_spacing", execution_ready) and not str(raw_object.get("blockReason") or "").strip())
|
||||
specs: list[dict[str, object]] = []
|
||||
for segment_index in range(len(instances) - 1):
|
||||
left = instances[segment_index]
|
||||
right = instances[segment_index + 1]
|
||||
left_label = _segment_instance_label(left, segment_index + 1)
|
||||
right_label = _segment_instance_label(right, segment_index + 2)
|
||||
segment_label = f"{left_label}-{right_label}间距"
|
||||
current = max(0.0, float(right["projection"]) - float(left["projection"]))
|
||||
if current <= 0:
|
||||
continue
|
||||
current_display = current / unit_scale if unit_scale > 0 else current
|
||||
scope_modes = _segment_scope_modes(
|
||||
signature,
|
||||
instances,
|
||||
segment_index,
|
||||
current,
|
||||
current_display,
|
||||
unit_scale,
|
||||
left_label=left_label,
|
||||
right_label=right_label,
|
||||
segment_label=segment_label,
|
||||
can_execute=can_execute,
|
||||
)
|
||||
default_mode = scope_modes.get("fix_left_move_right", {}) if isinstance(scope_modes, Mapping) else {}
|
||||
max_display = default_mode.get("max_value")
|
||||
range_hint = str(default_mode.get("range_hint") or "")
|
||||
enabled_tip = str(default_mode.get("enabled_tip") or range_hint)
|
||||
segment_signature = default_mode.get("scdm_geometry_signature")
|
||||
if not isinstance(segment_signature, Mapping):
|
||||
segment_signature = _segment_signature(
|
||||
signature,
|
||||
segment_index,
|
||||
current,
|
||||
unit_scale,
|
||||
left_label=left_label,
|
||||
right_label=right_label,
|
||||
moving_side="after",
|
||||
motion_semantics="fix_left_move_right_group",
|
||||
)
|
||||
specs.append(
|
||||
{
|
||||
"key": f"scdm:pattern.segment_spacing:{segment_index}",
|
||||
"label": segment_label,
|
||||
"current_raw": current_display,
|
||||
"scdm_current_raw": current,
|
||||
"scdm_unit_scale": unit_scale,
|
||||
"current_text": _format_value(current_display, value_type="positive"),
|
||||
"target_text": _format_value(current_display, value_type="positive"),
|
||||
"editable": True,
|
||||
"enabled": can_execute,
|
||||
"status_text": "可修改" if can_execute else "暂未接入",
|
||||
"scope_text": "固定前项,移动后侧",
|
||||
"scope_modes": scope_modes,
|
||||
"scope_default": "fix_left_move_right",
|
||||
"action": "apply_scdm_property_edit",
|
||||
"value_type": "positive",
|
||||
"enabled_tip": enabled_tip,
|
||||
"disabled_tip": "" if can_execute else "SCDM 已识别该局部间距,但当前修改执行器尚未开放。",
|
||||
"range_hint": range_hint,
|
||||
"min_value": 0.0,
|
||||
"min_exclusive": True,
|
||||
"max_value": max_display,
|
||||
"scdm_object_id": raw_object.get("objectId"),
|
||||
"scdm_source_backend_id": raw_object.get("sourceBackendId"),
|
||||
"scdm_capability_key": "pattern.segment_spacing",
|
||||
"scdm_backend_operation": "change_pattern_segment_spacing",
|
||||
"scdm_post_check": "target_pattern_segment_spacing",
|
||||
"scdm_geometry_signature": segment_signature,
|
||||
}
|
||||
)
|
||||
return specs
|
||||
|
||||
|
||||
def _sorted_pattern_instances(signature: Mapping[str, object], axis: list[float]) -> list[dict[str, object]]:
|
||||
value = signature.get("patternInstances")
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return []
|
||||
result: list[dict[str, object]] = []
|
||||
for index, item in enumerate(value):
|
||||
if not isinstance(item, Mapping):
|
||||
continue
|
||||
center = _float_values(item.get("center") or item.get("instanceCenter"))
|
||||
if len(center) != 3:
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"index": index,
|
||||
"source": item,
|
||||
"center": center,
|
||||
"projection": _point_projection(center, axis),
|
||||
}
|
||||
)
|
||||
result.sort(key=lambda item: float(item["projection"]))
|
||||
return result
|
||||
|
||||
|
||||
def _segment_max_spacing_display(
|
||||
signature: Mapping[str, object],
|
||||
instances: list[dict[str, object]],
|
||||
segment_index: int,
|
||||
current_display: float,
|
||||
unit_scale: float,
|
||||
*,
|
||||
moving_side: str = "after",
|
||||
) -> float | None:
|
||||
fit = signature.get("supportPatternFit")
|
||||
if not isinstance(fit, Mapping):
|
||||
return None
|
||||
projection_min = _float_or_none(fit.get("supportProjectionMinLocal"))
|
||||
projection_max = _float_or_none(fit.get("supportProjectionMaxLocal"))
|
||||
member_span = _float_or_none(fit.get("memberSpanLocal"))
|
||||
if projection_min is None or projection_max is None or member_span is None or member_span <= 0:
|
||||
return None
|
||||
first_projection = float(instances[0]["projection"])
|
||||
last_projection = float(instances[-1]["projection"])
|
||||
first_projection_display = first_projection / unit_scale if unit_scale > 0 else first_projection
|
||||
last_projection_display = last_projection / unit_scale if unit_scale > 0 else last_projection
|
||||
backward_capacity = first_projection_display - projection_min - (member_span * 0.5)
|
||||
forward_capacity = projection_max - (member_span * 0.5) - last_projection_display
|
||||
if moving_side in {"before", "left"}:
|
||||
extra = backward_capacity
|
||||
elif moving_side in {"split", "both", "center"}:
|
||||
extra = 2.0 * min(backward_capacity, forward_capacity)
|
||||
else:
|
||||
extra = forward_capacity
|
||||
return max(current_display, current_display + max(0.0, extra))
|
||||
|
||||
|
||||
def _segment_scope_modes(
|
||||
signature: Mapping[str, object],
|
||||
instances: list[dict[str, object]],
|
||||
segment_index: int,
|
||||
current_backend: float,
|
||||
current_display: float,
|
||||
unit_scale: float,
|
||||
*,
|
||||
left_label: str,
|
||||
right_label: str,
|
||||
segment_label: str,
|
||||
can_execute: bool,
|
||||
) -> dict[str, dict[str, object]]:
|
||||
modes: dict[str, dict[str, object]] = {}
|
||||
for key, label, moving_side, semantics, description in (
|
||||
(
|
||||
"fix_left_move_right",
|
||||
"固定前项,移动后侧",
|
||||
"after",
|
||||
"fix_left_move_right_group",
|
||||
f"固定 {left_label},平移 {right_label} 及其右侧所有阵列成员,右侧已有间距保持不变。",
|
||||
),
|
||||
(
|
||||
"fix_right_move_left",
|
||||
"固定后项,移动前侧",
|
||||
"before",
|
||||
"fix_right_move_left_group",
|
||||
f"固定 {right_label},平移 {left_label} 及其左侧所有阵列成员,左侧已有间距保持不变。",
|
||||
),
|
||||
(
|
||||
"split_keep_center",
|
||||
"两侧均分,中心不变",
|
||||
"split",
|
||||
"split_groups_keep_segment_center",
|
||||
f"{left_label} 及左侧向前移动一半,{right_label} 及右侧向后移动一半,保持这段间距中心不变。",
|
||||
),
|
||||
):
|
||||
max_display = _segment_max_spacing_display(
|
||||
signature,
|
||||
instances,
|
||||
segment_index,
|
||||
current_display,
|
||||
unit_scale,
|
||||
moving_side=moving_side,
|
||||
)
|
||||
mode_signature = _segment_signature(
|
||||
signature,
|
||||
segment_index,
|
||||
current_backend,
|
||||
unit_scale,
|
||||
left_label=left_label,
|
||||
right_label=right_label,
|
||||
moving_side=moving_side,
|
||||
motion_semantics=semantics,
|
||||
max_display=max_display,
|
||||
)
|
||||
range_hint = f"{label}:{description} 对象段:{segment_label},沿阵列方向由 {left_label} 到 {right_label}。"
|
||||
if max_display is not None and max_display > 0:
|
||||
range_hint += f" 当前支撑面约允许该策略最大间距 {max_display:g}。"
|
||||
modes[key] = {
|
||||
"label": label,
|
||||
"enabled": can_execute,
|
||||
"enabled_tip": range_hint,
|
||||
"disabled_tip": "" if can_execute else "SCDM 已识别该局部间距,但当前修改执行器尚未开放。",
|
||||
"range_hint": range_hint,
|
||||
"max_value": max_display,
|
||||
"scdm_geometry_signature": mode_signature,
|
||||
}
|
||||
modes["move_single_right"] = {
|
||||
"label": "只移动后项(未开放)",
|
||||
"enabled": False,
|
||||
"disabled_tip": (
|
||||
f"只移动后项(未开放):只移动 {right_label} 会同时改变它和右侧下一个成员的间距,容易破坏阵列规律;"
|
||||
"需要交互确认后再开放。"
|
||||
),
|
||||
"range_hint": f"只移动后项(未开放):该策略暂不执行。对象段:{segment_label}。",
|
||||
"scdm_geometry_signature": _segment_signature(
|
||||
signature,
|
||||
segment_index,
|
||||
current_backend,
|
||||
unit_scale,
|
||||
left_label=left_label,
|
||||
right_label=right_label,
|
||||
moving_side="single_right",
|
||||
motion_semantics="move_only_right_instance_blocked",
|
||||
),
|
||||
}
|
||||
return modes
|
||||
|
||||
|
||||
def _segment_signature(
|
||||
signature: Mapping[str, object],
|
||||
segment_index: int,
|
||||
current_backend: float,
|
||||
unit_scale: float,
|
||||
*,
|
||||
left_label: str,
|
||||
right_label: str,
|
||||
moving_side: str,
|
||||
motion_semantics: str,
|
||||
max_display: object = None,
|
||||
) -> dict[str, object]:
|
||||
result = dict(signature)
|
||||
segment_fit = dict(result.get("supportPatternFit") if isinstance(result.get("supportPatternFit"), Mapping) else {})
|
||||
max_number = _float_or_none(max_display)
|
||||
if max_number is not None and max_number > 0:
|
||||
segment_fit["maxSegmentSpacingLocal"] = max_number
|
||||
segment_fit["maxSegmentSpacing"] = max_number * unit_scale if unit_scale > 0 else max_number
|
||||
result["supportPatternFit"] = segment_fit
|
||||
result["segmentIndex"] = segment_index
|
||||
result["segmentLabel"] = f"{left_label}-{right_label}"
|
||||
result["segmentLeftLabel"] = left_label
|
||||
result["segmentRightLabel"] = right_label
|
||||
result["segmentSpacing"] = current_backend
|
||||
result["movingSide"] = moving_side
|
||||
result["motionSemantics"] = motion_semantics
|
||||
axis = _unit_vector(_float_values(signature.get("axis")))
|
||||
instances = _sorted_pattern_instances(signature, axis) if len(axis) == 3 else []
|
||||
if segment_index < len(instances) - 1:
|
||||
result["segmentLeft"] = _segment_instance_reference(instances[segment_index], label=left_label)
|
||||
result["segmentRight"] = _segment_instance_reference(instances[segment_index + 1], label=right_label)
|
||||
result["localUnitScale"] = unit_scale
|
||||
return result
|
||||
|
||||
|
||||
def _segment_instance_reference(item: Mapping[str, object], *, label: str = "") -> dict[str, object]:
|
||||
source = item.get("source")
|
||||
if not isinstance(source, Mapping):
|
||||
return {}
|
||||
return {
|
||||
"displayLabel": label,
|
||||
"sourceObjectId": source.get("sourceObjectId"),
|
||||
"faceIds": _int_values(source.get("faceIds")),
|
||||
"bodyIndex": _int_or_none(source.get("bodyIndex")),
|
||||
"componentLocators": source.get("componentLocators") or source.get("bodyLocators") or [],
|
||||
}
|
||||
|
||||
|
||||
def _segment_instance_label(item: Mapping[str, object], ordinal: int) -> str:
|
||||
source = item.get("source")
|
||||
if not isinstance(source, Mapping):
|
||||
return f"成员{ordinal}"
|
||||
instance_kind = str(source.get("instanceKind") or "").strip().lower()
|
||||
if instance_kind in {"body", "part", "component"}:
|
||||
local_solid_ids = sorted(set(_int_values(source.get("localSolidIds") or [source.get("localSolidId")])))
|
||||
if local_solid_ids:
|
||||
return f"Solid{local_solid_ids[0]}{'组' if len(local_solid_ids) > 1 else ''}"
|
||||
local_part_ids = sorted(set(_int_values(source.get("localPartIds") or [source.get("localPartId")])))
|
||||
if local_part_ids:
|
||||
return f"Part{local_part_ids[0]}{'组' if len(local_part_ids) > 1 else ''}"
|
||||
component_label = _component_locator_label(source.get("componentLocators") or source.get("bodyLocators"), include_index=False)
|
||||
if component_label:
|
||||
return component_label
|
||||
body_index = _int_or_none(source.get("bodyIndex"))
|
||||
if body_index is not None:
|
||||
return f"零件{body_index}"
|
||||
face_ids = sorted(set(_int_values(source.get("faceIds"))))
|
||||
if face_ids:
|
||||
return f"Face{face_ids[0]}{'组' if len(face_ids) > 1 else ''}"
|
||||
component_label = _component_locator_label(source.get("componentLocators") or source.get("bodyLocators"))
|
||||
if component_label:
|
||||
return component_label
|
||||
body_index = _int_or_none(source.get("bodyIndex"))
|
||||
if body_index is not None:
|
||||
return f"零件{body_index}"
|
||||
source_id = str(source.get("sourceObjectId") or "").strip()
|
||||
if source_id:
|
||||
return source_id
|
||||
return f"成员{ordinal}"
|
||||
|
||||
|
||||
def _component_locator_label(value: object, *, include_index: bool = True) -> str:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return ""
|
||||
for locator in value:
|
||||
if not isinstance(locator, Mapping):
|
||||
continue
|
||||
for key in ("componentName", "name", "displayName"):
|
||||
text = str(locator.get(key) or "").strip()
|
||||
if text:
|
||||
return text
|
||||
if not include_index:
|
||||
return ""
|
||||
for locator in value:
|
||||
if not isinstance(locator, Mapping):
|
||||
continue
|
||||
component_index = _int_or_none(locator.get("componentIndex"))
|
||||
if component_index is not None:
|
||||
return f"组件{component_index + 1}"
|
||||
return ""
|
||||
|
||||
|
||||
def _unit_vector(values: list[float]) -> list[float]:
|
||||
if len(values) != 3:
|
||||
return []
|
||||
length = sum(item * item for item in values) ** 0.5
|
||||
if length <= 1.0e-12:
|
||||
return []
|
||||
return [item / length for item in values]
|
||||
|
||||
|
||||
def _point_projection(point: list[float], axis: list[float]) -> float:
|
||||
return sum(float(point[index]) * float(axis[index]) for index in range(3))
|
||||
|
||||
|
||||
def _float_or_none(value: object) -> float | None:
|
||||
try:
|
||||
return float(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _value_type(value_kind: str, key: str) -> str:
|
||||
if value_kind == "vector3":
|
||||
return "vector3"
|
||||
if value_kind == "command":
|
||||
return "command"
|
||||
if key.endswith(".diameter") or key.endswith(".radius"):
|
||||
positive_suffixes = (".diameter", ".radius", ".width", ".depth", ".height", ".distance", ".thickness", ".spacing", ".segment_spacing")
|
||||
if key.endswith(positive_suffixes):
|
||||
return "positive"
|
||||
return "number"
|
||||
|
||||
@@ -132,6 +566,19 @@ def _format_value(value: object, *, value_type: str) -> str:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _current_value_available(value: object, *, value_type: str) -> bool:
|
||||
if value is None or value == "":
|
||||
return False
|
||||
if value_type == "vector3":
|
||||
return len(_float_values(value)) == 3
|
||||
if value_type in {"number", "positive"}:
|
||||
try:
|
||||
return float(str(value).strip()) > 0 if value_type == "positive" else True
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _float_values(value: object) -> list[float]:
|
||||
if isinstance(value, (str, bytes)) or value is None:
|
||||
return []
|
||||
@@ -164,4 +611,11 @@ def _int_values(value: object) -> list[int]:
|
||||
return result
|
||||
|
||||
|
||||
def _int_or_none(value: object) -> int | None:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
__all__ = ["property_specs_from_scdm_cache"]
|
||||
|
||||
@@ -79,10 +79,75 @@ def validate_scdm_edit_result(
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
|
||||
if _is_removal_capability(capability_key) and (not before_signature or not after_cache):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "removal-check-unavailable",
|
||||
"message": "Removed feature cannot be verified without the old feature signature and the new SCDM cache.",
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
|
||||
if capability_key == "pattern.segment_spacing":
|
||||
check = _check_pattern_segment_spacing_edit_result(edit_result, expected_target, tolerance=tolerance)
|
||||
if check.get("ok") is not True:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": str(check.get("reason") or "target-check-failed"),
|
||||
"message": str(check.get("message") or "SCDM result did not reach the target segment spacing."),
|
||||
"targetCheck": check,
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "SCDM edit result passed the available validation checks.",
|
||||
"output_step": str(output_step),
|
||||
"matchedObject": None,
|
||||
"targetCheck": check,
|
||||
"removalCheck": {"ok": None, "reason": "not-run", "message": "Removal check is not needed for this capability."},
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
|
||||
matched: dict[str, object] | None = None
|
||||
removal_check: dict[str, object] = {"ok": None, "reason": "not-run", "message": "Removal check is not needed for this capability."}
|
||||
if before_signature and after_cache:
|
||||
match = match_scdm_object_by_signature(before_signature, after_cache, capability_key=capability_key)
|
||||
status = str(match.get("status") or "")
|
||||
if _is_removal_capability(capability_key):
|
||||
removal_check = _check_removed_object_match(match)
|
||||
if removal_check.get("ok") is not True:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": str(removal_check.get("reason") or "feature-still-present"),
|
||||
"message": str(removal_check.get("message") or "Removed feature is still present in the new SCDM cache."),
|
||||
"removalCheck": removal_check,
|
||||
"match": match,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
check = {"ok": True, "reason": "removed", "message": "Target feature disappeared from the new SCDM cache."}
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "SCDM edit result passed the available validation checks.",
|
||||
"output_step": str(output_step),
|
||||
"matchedObject": None,
|
||||
"targetCheck": check,
|
||||
"removalCheck": removal_check,
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
if status != "unique":
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -118,6 +183,7 @@ def validate_scdm_edit_result(
|
||||
"output_step": str(output_step),
|
||||
"matchedObject": matched,
|
||||
"targetCheck": check,
|
||||
"removalCheck": removal_check,
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
@@ -251,6 +317,8 @@ def check_scdm_unedited_objects(
|
||||
continue
|
||||
if edited_signature and _same_signature_subject(signature, edited_signature):
|
||||
continue
|
||||
if edited_signature and _is_expected_pattern_spacing_subject(signature, edited_signature, capability_key=capability_key):
|
||||
continue
|
||||
checked += 1
|
||||
match = match_scdm_object_by_signature(signature, after_cache, capability_key=capability_key)
|
||||
status = str(match.get("status") or "")
|
||||
@@ -324,15 +392,88 @@ def check_scdm_target(
|
||||
actual_vector = _vector(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "center"))
|
||||
expected_vector = _vector(expected_target)
|
||||
return _vector_check(actual_vector, expected_vector, "hole.position", tolerance)
|
||||
if capability_key in {"slot.position", "boss.position", "pattern.instance_position"}:
|
||||
actual_vector = _vector(
|
||||
_capability_value(raw_object, capability_key)
|
||||
or _geometry_value(raw_object, "center")
|
||||
or _geometry_value(raw_object, "axisCenter")
|
||||
or _geometry_value(raw_object, "instanceCenter")
|
||||
)
|
||||
expected_vector = _vector(expected_target)
|
||||
return _vector_check(actual_vector, expected_vector, capability_key, tolerance)
|
||||
if capability_key == "slot.width":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "width"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "slot.width", tolerance)
|
||||
if capability_key == "slot.depth":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "depth"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "slot.depth", tolerance)
|
||||
if capability_key == "boss.height":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "height"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "boss.height", tolerance)
|
||||
if capability_key == "boss.diameter":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "diameter"))
|
||||
if actual is None:
|
||||
radius = _number(_geometry_value(raw_object, "radius"))
|
||||
actual = radius * 2.0 if radius is not None else None
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "boss.diameter", tolerance)
|
||||
if capability_key == "round.radius":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "radius"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "round.radius", tolerance)
|
||||
if capability_key == "chamfer.distance":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "distance") or _geometry_value(raw_object, "offset"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "chamfer.distance", tolerance)
|
||||
if capability_key == "pattern.spacing":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "spacing") or _geometry_value(raw_object, "pitch"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "pattern.spacing", tolerance)
|
||||
if capability_key == "pattern.segment_spacing":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "segmentSpacing") or _geometry_value(raw_object, "spacing") or _geometry_value(raw_object, "pitch"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "pattern.segment_spacing", tolerance)
|
||||
if capability_key == "shell.thickness":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "thickness"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "shell.thickness", tolerance)
|
||||
if capability_key == "face.offset":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "offset") or _geometry_value(raw_object, "planeOffset"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "face.offset", tolerance)
|
||||
if capability_key == "feature.fill":
|
||||
return {"ok": True, "reason": "not-applicable", "message": "feature.fill is checked by object disappearance in the caller."}
|
||||
if _is_removal_capability(capability_key):
|
||||
return {"ok": True, "reason": "not-applicable", "message": f"{capability_key} is checked by object disappearance in the caller."}
|
||||
return {"ok": None, "reason": "unsupported-post-check", "message": f"No target checker is registered for {capability_key}."}
|
||||
|
||||
|
||||
def _check_pattern_segment_spacing_edit_result(
|
||||
edit_result: Mapping[str, object],
|
||||
expected_target: object,
|
||||
*,
|
||||
tolerance: float,
|
||||
) -> dict[str, object]:
|
||||
applied = edit_result.get("applied")
|
||||
nested = edit_result.get("result")
|
||||
if not isinstance(applied, Mapping) and isinstance(nested, Mapping):
|
||||
applied = nested.get("applied")
|
||||
if not isinstance(applied, Mapping):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "missing-edit-applied",
|
||||
"message": "SCDM edit result did not report the applied local segment spacing.",
|
||||
}
|
||||
actual = _number(applied.get("segmentSpacing") or applied.get("targetSpacing"))
|
||||
expected = _number(expected_target)
|
||||
check = _number_check(actual, expected, "pattern.segment_spacing", tolerance)
|
||||
if check.get("ok") is True:
|
||||
check["segmentIndex"] = applied.get("segmentIndex")
|
||||
check["spacingMode"] = applied.get("spacingMode")
|
||||
return check
|
||||
|
||||
|
||||
def check_scdm_summary_delta(
|
||||
before_cache: Mapping[str, object],
|
||||
after_cache: Mapping[str, object],
|
||||
@@ -401,6 +542,27 @@ def _signature_score(before: Mapping[str, object], after: Mapping[str, object],
|
||||
if before_surface and before_surface == after_surface:
|
||||
score += 1.0
|
||||
|
||||
before_components = _component_locator_keys(before.get("componentLocators") or before.get("bodyLocators"))
|
||||
after_components = _component_locator_keys(after.get("componentLocators") or after.get("bodyLocators"))
|
||||
if before_components and after_components:
|
||||
if before_components & after_components:
|
||||
score += 3.0
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
before_body = _int_or_none(before.get("bodyIndex"))
|
||||
after_body = _int_or_none(after.get("bodyIndex"))
|
||||
if before_body is not None and after_body is not None and before_body == after_body:
|
||||
score += 2.0
|
||||
before_face_ordinal = _int_or_none(before.get("faceOrdinal"))
|
||||
after_face_ordinal = _int_or_none(after.get("faceOrdinal"))
|
||||
if before_face_ordinal is not None and before_face_ordinal == after_face_ordinal:
|
||||
score += 1.0
|
||||
before_edge_ordinal = _int_or_none(before.get("edgeOrdinal"))
|
||||
after_edge_ordinal = _int_or_none(after.get("edgeOrdinal"))
|
||||
if before_edge_ordinal is not None and before_edge_ordinal == after_edge_ordinal:
|
||||
score += 1.0
|
||||
|
||||
before_faces = set(_int_values(before.get("faceIds")))
|
||||
after_faces = set(_int_values(after.get("faceIds")))
|
||||
if before_faces and after_faces:
|
||||
@@ -413,8 +575,13 @@ def _signature_score(before: Mapping[str, object], after: Mapping[str, object],
|
||||
if before_edges and after_edges and before_edges & after_edges:
|
||||
score += 0.5
|
||||
|
||||
if capability_key != "hole.position":
|
||||
center_score = _vector_distance_score(_vector(before.get("center")), _vector(after.get("center")))
|
||||
if not _is_position_capability(capability_key):
|
||||
before_center = _vector(before.get("center"))
|
||||
after_center = _vector(after.get("center"))
|
||||
if _is_removal_capability(capability_key) and before_center and after_center:
|
||||
if _vector_error(before_center, after_center) > _vector_tolerance(before_center, after_center):
|
||||
return 0.0
|
||||
center_score = _vector_distance_score(before_center, after_center)
|
||||
score += center_score
|
||||
|
||||
axis_score = _axis_score(_vector(before.get("axis")), _vector(after.get("axis")))
|
||||
@@ -426,6 +593,98 @@ def _signature_score(before: Mapping[str, object], after: Mapping[str, object],
|
||||
return score
|
||||
|
||||
|
||||
def _is_position_capability(capability_key: str) -> bool:
|
||||
return capability_key in {"hole.position", "slot.position", "boss.position", "pattern.instance_position"} or capability_key.endswith(".position")
|
||||
|
||||
|
||||
def _is_removal_capability(capability_key: str) -> bool:
|
||||
return capability_key in {"feature.fill", "feature.delete_round_or_chamfer"} or capability_key.endswith(".remove") or capability_key.startswith("feature.delete")
|
||||
|
||||
|
||||
def _check_removed_object_match(match: Mapping[str, object]) -> dict[str, object]:
|
||||
status = str(match.get("status") or "")
|
||||
if status == "none":
|
||||
return {"ok": True, "reason": "removed", "message": "Edited feature is no longer present in the new SCDM cache."}
|
||||
if status == "unique":
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "feature-still-present",
|
||||
"message": "SCDM reported success, but the edited feature still matches an object in the new cache.",
|
||||
"match": dict(match),
|
||||
}
|
||||
if status == "multiple":
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "feature-removal-ambiguous",
|
||||
"message": "SCDM reported success, but multiple new objects still match the edited feature.",
|
||||
"match": dict(match),
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": f"feature-removal-{status or 'failed'}",
|
||||
"message": str(match.get("message") or "Removed feature could not be verified."),
|
||||
"match": dict(match),
|
||||
}
|
||||
|
||||
|
||||
def _is_expected_pattern_spacing_subject(
|
||||
signature: Mapping[str, object],
|
||||
edited_signature: Mapping[str, object],
|
||||
*,
|
||||
capability_key: str,
|
||||
) -> bool:
|
||||
if capability_key not in {"pattern.spacing", "pattern.segment_spacing"}:
|
||||
return False
|
||||
if str(edited_signature.get("objectType") or "") != "linear_pattern":
|
||||
return False
|
||||
touched_faces = set(_int_values(edited_signature.get("faceIds")))
|
||||
touched_bodies = set(_int_values(edited_signature.get("bodyIndices")))
|
||||
touched_components = _component_locator_keys(edited_signature.get("componentLocators"))
|
||||
for instance in _pattern_instances(edited_signature):
|
||||
touched_faces.update(_int_values(instance.get("faceIds")))
|
||||
body_index = _int_or_none(instance.get("bodyIndex"))
|
||||
if body_index is not None:
|
||||
touched_bodies.add(body_index)
|
||||
touched_bodies.update(_int_values(instance.get("bodyIndices")))
|
||||
touched_components.update(_component_locator_keys(instance.get("componentLocators") or instance.get("bodyLocators")))
|
||||
|
||||
subject_faces = set(_int_values(signature.get("faceIds")))
|
||||
if touched_faces and subject_faces and touched_faces.intersection(subject_faces):
|
||||
return True
|
||||
subject_body = _int_or_none(signature.get("bodyIndex"))
|
||||
if subject_body is not None and subject_body in touched_bodies:
|
||||
return True
|
||||
subject_bodies = set(_int_values(signature.get("bodyIndices")))
|
||||
if touched_bodies and subject_bodies and touched_bodies.intersection(subject_bodies):
|
||||
return True
|
||||
subject_components = _component_locator_keys(signature.get("componentLocators") or signature.get("bodyLocators"))
|
||||
return bool(touched_components and subject_components and touched_components.intersection(subject_components))
|
||||
|
||||
|
||||
def _pattern_instances(signature: Mapping[str, object]) -> list[Mapping[str, object]]:
|
||||
value = signature.get("patternInstances")
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return []
|
||||
return [item for item in value if isinstance(item, Mapping)]
|
||||
|
||||
|
||||
def _component_locator_keys(value: object) -> set[str]:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return set()
|
||||
result: set[str] = set()
|
||||
for locator in value:
|
||||
if not isinstance(locator, Mapping):
|
||||
continue
|
||||
path = _ordered_int_values(locator.get("componentPath"))
|
||||
if path:
|
||||
result.add("path:" + ".".join(str(item) for item in path))
|
||||
continue
|
||||
component_index = _int_or_none(locator.get("componentIndex"))
|
||||
if component_index is not None:
|
||||
result.add("index:" + str(component_index))
|
||||
return result
|
||||
|
||||
|
||||
def _signature_has_enough_identity(signature: Mapping[str, object]) -> bool:
|
||||
if _vector(signature.get("center")) and _vector(signature.get("axis")):
|
||||
return True
|
||||
@@ -473,11 +732,12 @@ def _unchanged_signature_still_matches(before: Mapping[str, object], after: Mapp
|
||||
if abs(float(before_diameter) - float(after_diameter)) > tolerance:
|
||||
return False
|
||||
|
||||
before_offset = _number(before.get("planeOffset"))
|
||||
after_offset = _number(after.get("planeOffset"))
|
||||
if before_offset is not None and after_offset is not None:
|
||||
tolerance = max(abs(before_offset), abs(after_offset), 1.0) * 1.0e-5
|
||||
if abs(float(before_offset) - float(after_offset)) > tolerance:
|
||||
for key in ("planeOffset", "width", "depth", "height", "distance", "spacing", "pitch", "thickness"):
|
||||
before_value = _number(before.get(key))
|
||||
after_value = _number(after.get(key))
|
||||
if before_value is not None and after_value is not None:
|
||||
tolerance = max(abs(before_value), abs(after_value), 1.0) * 1.0e-5
|
||||
if abs(float(before_value) - float(after_value)) > tolerance:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -645,6 +905,22 @@ def _int_values(value: object) -> list[int]:
|
||||
return result
|
||||
|
||||
|
||||
def _ordered_int_values(value: object) -> list[int]:
|
||||
if isinstance(value, (str, bytes)) or value is None:
|
||||
return []
|
||||
try:
|
||||
values = list(value) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
return []
|
||||
result: list[int] = []
|
||||
for item in values:
|
||||
try:
|
||||
result.append(int(item))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def _raw_summary(cache: Mapping[str, object]) -> Mapping[str, object]:
|
||||
diagnostics = cache.get("diagnostics")
|
||||
if not isinstance(diagnostics, Mapping):
|
||||
|
||||
@@ -1105,13 +1105,27 @@ EDITABLE_TARGET_KIND_ROLE = Qt.UserRole + 2
|
||||
|
||||
|
||||
SELECTION_MODE_LABELS = {
|
||||
"Part": "零件",
|
||||
"Part": "Part",
|
||||
"Solid": "Solid",
|
||||
"Face": "Face",
|
||||
"Edge": "Edge",
|
||||
"Feature": "特征",
|
||||
"Feature": "Feature",
|
||||
}
|
||||
SELECTION_MODE_VALUES = {label: mode for mode, label in SELECTION_MODE_LABELS.items()}
|
||||
SELECTION_MODE_VALUES.update(
|
||||
{
|
||||
"装配零件": "Part",
|
||||
"零件": "Part",
|
||||
"实体": "Solid",
|
||||
"面": "Face",
|
||||
"边": "Edge",
|
||||
"智能特征": "Feature",
|
||||
"Solid": "Solid",
|
||||
"Face": "Face",
|
||||
"Edge": "Edge",
|
||||
"特征": "Feature",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
SURFACE_VALUE_LABELS = {
|
||||
|
||||
+85
-15
@@ -21,9 +21,9 @@ from PySide6.QtWidgets import (
|
||||
from .model import StepModel
|
||||
from .asitus_bridge import run_asitus_hole_recognition
|
||||
from .records import OperationRecord
|
||||
from .scdm_feature_mapper import attach_local_face_ids_to_scdm_cache, map_scdm_raw_features
|
||||
from .scdm_feature_mapper import SCDM_FEATURE_CACHE_REVISION, attach_local_face_ids_to_scdm_cache, map_scdm_raw_features
|
||||
from .scdm_probe import run_scdm_probe
|
||||
from .scdm_schema import write_json
|
||||
from .scdm_schema import default_scdm_work_dir, file_fingerprint, read_json, write_json
|
||||
from .ui_helpers import * # noqa: F403
|
||||
from .workers import EditWorker, LoadWorker, ScanWorker
|
||||
|
||||
@@ -1308,10 +1308,12 @@ class WindowCoreMixin:
|
||||
f"Loaded {self.step_path.name}; 大模型已跳过全量边线补绘,旋转会更流畅,切到 Edge 选择时再按需生成。"
|
||||
)
|
||||
pending_scdm_reload = isinstance(getattr(self, "pending_scdm_edit_reload", None), dict)
|
||||
if large_interaction_model and not pending_scdm_reload:
|
||||
scdm_cache_restored = False if pending_scdm_reload else self._restore_scdm_feature_cache_from_disk()
|
||||
if large_interaction_model and not pending_scdm_reload and not scdm_cache_restored:
|
||||
self._defer_large_model_recognition_preloads()
|
||||
else:
|
||||
QTimer.singleShot(160, self._start_asitus_hole_recognition_preload)
|
||||
if pending_scdm_reload or not scdm_cache_restored:
|
||||
QTimer.singleShot(240, lambda: self._start_scdm_probe_preload(force=pending_scdm_reload))
|
||||
|
||||
def _large_model_interaction_mode(self, stats: object | None = None) -> bool:
|
||||
@@ -1354,9 +1356,84 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
|
||||
def _install_scdm_feature_cache(self, cache: dict[str, object], *, cache_path: str = "", message: str = "") -> None:
|
||||
self.scdm_feature_cache = dict(cache)
|
||||
self.scdm_feature_cache_state = "ready"
|
||||
self.scdm_feature_cache_message = message or "SCDM 可修改参数识别完成。"
|
||||
self.scdm_feature_cache_path = str(cache_path or "")
|
||||
if self.model is not None:
|
||||
try:
|
||||
self.model.scdm_feature_cache = dict(cache)
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "_refresh_property_editor"):
|
||||
self._refresh_property_editor()
|
||||
|
||||
def _restore_scdm_feature_cache_from_disk(self) -> bool:
|
||||
if self.model is None or self.step_path is None:
|
||||
return False
|
||||
step_path = Path(self.step_path)
|
||||
try:
|
||||
fingerprint = file_fingerprint(step_path)
|
||||
except OSError:
|
||||
return False
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
work_dir = default_scdm_work_dir(step_path, project_root=project_root, fingerprint=fingerprint)
|
||||
cache_path = work_dir / "scdm_feature_cache.json"
|
||||
raw_path = work_dir / "scdm_raw_features.json"
|
||||
|
||||
cache: dict[str, object] | None = None
|
||||
if cache_path.is_file():
|
||||
try:
|
||||
candidate = read_json(cache_path)
|
||||
revision = _safe_int_or_none(candidate.get("mapperRevision")) or 0
|
||||
if str(candidate.get("modelFingerprint") or "") == fingerprint and revision >= SCDM_FEATURE_CACHE_REVISION:
|
||||
cache = candidate
|
||||
except Exception:
|
||||
cache = None
|
||||
|
||||
if cache is None and raw_path.is_file():
|
||||
try:
|
||||
raw = read_json(raw_path)
|
||||
raw_model = raw.get("model")
|
||||
raw_fingerprint = str(raw_model.get("fingerprint") or "") if isinstance(raw_model, dict) else ""
|
||||
if raw_fingerprint == fingerprint:
|
||||
cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(raw),
|
||||
self._scdm_local_face_signatures(),
|
||||
)
|
||||
write_json(cache_path, cache)
|
||||
except Exception:
|
||||
cache = None
|
||||
|
||||
if cache is None:
|
||||
return False
|
||||
self._install_scdm_feature_cache(
|
||||
cache,
|
||||
cache_path=str(cache_path),
|
||||
message="已从本地 SCDM 识别缓存恢复;模型变更后会重新识别。",
|
||||
)
|
||||
self.statusBar().showMessage("已恢复本地 SCDM 识别缓存,参数表可直接使用。")
|
||||
return True
|
||||
|
||||
def _current_scdm_feature_cache_matches_loaded_step(self) -> bool:
|
||||
if self.step_path is None or not isinstance(getattr(self, "scdm_feature_cache", None), dict):
|
||||
return False
|
||||
cache = getattr(self, "scdm_feature_cache", None)
|
||||
try:
|
||||
fingerprint = file_fingerprint(Path(self.step_path))
|
||||
except OSError:
|
||||
return False
|
||||
revision = _safe_int_or_none(cache.get("mapperRevision")) or 0
|
||||
return str(cache.get("modelFingerprint") or "") == fingerprint and revision >= SCDM_FEATURE_CACHE_REVISION
|
||||
|
||||
def _start_scdm_probe_preload(self, *, force: bool = False) -> None:
|
||||
if self.model is None or self.step_path is None:
|
||||
return
|
||||
if not force and self._current_scdm_feature_cache_matches_loaded_step():
|
||||
return
|
||||
if not force and self._large_model_interaction_mode():
|
||||
self._defer_large_model_recognition_preloads()
|
||||
return
|
||||
@@ -1476,21 +1553,14 @@ class WindowCoreMixin:
|
||||
return
|
||||
if isinstance(result.get("backend"), dict):
|
||||
self.scdm_backend_status = dict(result["backend"])
|
||||
self.scdm_feature_cache = dict(cache)
|
||||
self.scdm_feature_cache_state = "ready"
|
||||
self.scdm_feature_cache_message = "SCDM 可修改参数识别完成。"
|
||||
self.scdm_feature_cache_path = str(result.get("cache_path") or "")
|
||||
try:
|
||||
self.model.scdm_feature_cache = dict(cache)
|
||||
except Exception:
|
||||
pass
|
||||
self._install_scdm_feature_cache(
|
||||
dict(cache),
|
||||
cache_path=str(result.get("cache_path") or ""),
|
||||
message="SCDM 可修改参数识别完成。",
|
||||
)
|
||||
objects = cache.get("objects")
|
||||
count = len(objects) if isinstance(objects, list) else 0
|
||||
self.statusBar().showMessage(f"SCDM 可修改参数识别完成:{count} 个产品化对象。")
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "_refresh_property_editor"):
|
||||
self._refresh_property_editor()
|
||||
if hasattr(self, "_finish_pending_scdm_edit_reload"):
|
||||
self._finish_pending_scdm_edit_reload(cache_ready=True)
|
||||
finally:
|
||||
|
||||
@@ -42,6 +42,7 @@ from .relation_formulas import (
|
||||
parse_relation_formula,
|
||||
relation_value_to_text,
|
||||
rewrite_relation_formula_ids,
|
||||
validate_relation_formula_graph,
|
||||
)
|
||||
from .scdm_backend import resolve_scdm_backend
|
||||
from .scdm_edit_runner import run_scdm_edit_job
|
||||
@@ -3100,6 +3101,7 @@ class WindowStateMixin:
|
||||
text = self.relation_formula_input.text().strip() if hasattr(self, "relation_formula_input") else ""
|
||||
try:
|
||||
formula = parse_relation_formula(text)
|
||||
self._validate_relation_formula_graph(formula)
|
||||
if not replay_active:
|
||||
self._validate_relation_formula_references(formula)
|
||||
except RelationFormulaError as exc:
|
||||
@@ -3453,6 +3455,18 @@ class WindowStateMixin:
|
||||
for ref in formula.references:
|
||||
self._relation_value_for_ref(ref)
|
||||
|
||||
def _validate_relation_formula_graph(self, new_formula) -> None:
|
||||
formulas = []
|
||||
for item in getattr(self, "relation_formula_items", []) or []:
|
||||
if not bool(item.get("enabled", True)):
|
||||
continue
|
||||
try:
|
||||
formulas.append(parse_relation_formula(str(item.get("text") or "")))
|
||||
except RelationFormulaError:
|
||||
continue
|
||||
formulas.append(new_formula)
|
||||
validate_relation_formula_graph(formulas)
|
||||
|
||||
def _relation_parameter_supported(self, ref: ObjectParameterRef, *, target: bool = False) -> None:
|
||||
if target and self._relation_visible_spec_for_ref(ref) is not None:
|
||||
return
|
||||
@@ -4565,7 +4579,7 @@ class WindowStateMixin:
|
||||
return inner_wires > 0 or boundary_edges >= 16
|
||||
|
||||
def _scdm_selection_status_message(self, scdm_specs: list[dict[str, object]]) -> str:
|
||||
if self.selected_kind not in {"feature", "face", "edge"}:
|
||||
if self.selected_kind not in {"feature", "face", "edge", "solid", "part"}:
|
||||
return ""
|
||||
if self.model is None:
|
||||
return ""
|
||||
@@ -8915,8 +8929,12 @@ class WindowStateMixin:
|
||||
|
||||
def _scdm_property_target_value(self, spec: dict[str, object], text: str) -> object:
|
||||
value_type = str(spec.get("value_type") or "number")
|
||||
unit_scale = _float_or_none(spec.get("scdm_unit_scale"))
|
||||
if unit_scale is None or unit_scale <= 0:
|
||||
unit_scale = 1.0
|
||||
if value_type == "vector3":
|
||||
return list(self._parse_property_vector3(text))
|
||||
values = list(self._parse_property_vector3(text))
|
||||
return [value * unit_scale for value in values] if self._scdm_property_uses_length_units(spec) else values
|
||||
if value_type in {"number", "positive"}:
|
||||
try:
|
||||
value = float(text)
|
||||
@@ -8924,11 +8942,32 @@ class WindowStateMixin:
|
||||
raise ValueError("请输入数字形式的目标值。") from exc
|
||||
if value_type == "positive" and value <= 0:
|
||||
raise ValueError("请输入大于 0 的目标值。")
|
||||
return value
|
||||
return value * unit_scale if self._scdm_property_uses_length_units(spec) else value
|
||||
if value_type == "command":
|
||||
return True
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def _scdm_property_uses_length_units(spec: dict[str, object]) -> bool:
|
||||
key = str(spec.get("scdm_capability_key") or spec.get("key") or "")
|
||||
value_type = str(spec.get("value_type") or "")
|
||||
if value_type == "vector3":
|
||||
return True
|
||||
suffixes = (
|
||||
".diameter",
|
||||
".radius",
|
||||
".offset",
|
||||
".width",
|
||||
".depth",
|
||||
".height",
|
||||
".distance",
|
||||
".thickness",
|
||||
".spacing",
|
||||
".segment_spacing",
|
||||
".position",
|
||||
)
|
||||
return key.endswith(suffixes)
|
||||
|
||||
@Slot(object)
|
||||
def _finish_scdm_edit_action(self, result: object) -> None:
|
||||
if hasattr(self, "_reroute_to_ui_thread") and self._reroute_to_ui_thread(lambda result=result: self._finish_scdm_edit_action(result)):
|
||||
|
||||
Reference in New Issue
Block a user