Compare commits
2 Commits
19364d81b5
...
066d28660b
| Author | SHA1 | Date | |
|---|---|---|---|
| 066d28660b | |||
| 7d814d2939 |
@@ -26,7 +26,9 @@ assets/screenshots/
|
|||||||
local/
|
local/
|
||||||
tmp.md
|
tmp.md
|
||||||
data.json
|
data.json
|
||||||
|
nodes/
|
||||||
Analysis-Component/
|
Analysis-Component/
|
||||||
|
third_party/AnalysisSitus/
|
||||||
|
|
||||||
# Local reference docs; keep them on disk, never commit them.
|
# Local reference docs; keep them on disk, never commit them.
|
||||||
Face一级关系专项测试说明.md
|
Face一级关系专项测试说明.md
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ python scripts\verify_first_level_edit_suites.py
|
|||||||
python scripts\verify_first_level_edit_suites.py --quick
|
python scripts\verify_first_level_edit_suites.py --quick
|
||||||
```
|
```
|
||||||
|
|
||||||
`--quick` 会检查 smoke test、属性表规格、参数表 UI、特征识别优先级、一级事实图、关联探测、显示网格预算,以及 README 里的一级验收口径是否仍和脚本入口一致。
|
`--quick` 会检查 smoke test、属性表规格、参数表 UI、参数导出组件生成、ICEPAK 真实 STEP 同域圆柱孔、特征识别优先级、一级事实图、关联探测、显示网格预算,以及 README 里的一级验收口径是否仍和脚本入口一致。
|
||||||
|
|
||||||
只验证某个阶段时,例如 Edge:
|
只验证某个阶段时,例如 Edge:
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ git diff --check
|
|||||||
- 用户可以做基础测量,例如把两个拾取点或对象中心设为 A/B 并计算距离。
|
- 用户可以做基础测量,例如把两个拾取点或对象中心设为 A/B 并计算距离。
|
||||||
- 程序可以识别孔、圆角、凸台、槽、壳体局部区域等候选特征。
|
- 程序可以识别孔、圆角、凸台、槽、壳体局部区域等候选特征。
|
||||||
- 识别结果会区分 `当前可改`、`当前受限修改`、`识别限制` 和 `受限能力`:比如一个通孔可以改孔径,但盲孔深度属于受限能力;一个带内孔的平面可以拉伸/切除,但“局部重建尺寸/偏移”会被标成受限修改。
|
- 识别结果会区分 `当前可改`、`当前受限修改`、`识别限制` 和 `受限能力`:比如一个通孔可以改孔径,但盲孔深度属于受限能力;一个带内孔的平面可以拉伸/切除,但“局部重建尺寸/偏移”会被标成受限修改。
|
||||||
- 用户可以修改可控的特征,比如 Face 的面内长度、面内宽度、偏移和壳体厚度,并通过 `建模意图` 选择拉伸/切除、局部重建、移动特征或缩放特征;面积是结果变量,放在诊断信息里查看,不作为特征参数表里的驱动变量。Face/直线 Edge 的 `中心` 移动后端能力暂时保留,但不在特征参数表开放。还可以做孔径/半径调整、孔/槽轴心并通过 `建模意图` 选择移动局部特征或移动特征、槽宽/槽深/弧长并通过 `建模意图` 选择局部重建或整体缩放、弧角、开口角和槽孔总长度调整、盲孔/盲槽深度(局部切补或整体缩放)、壳体厚度(局部拉伸/切除或整体缩放)、凸台直径/半径/高度/轴心并通过 `建模意图` 选择局部修改或整体调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度、简单等半径圆角链半径/弧长并通过 `建模意图` 选择重建圆角或整体调整、已有等距倒角距离、圆锥参考半径/直径/半角(简单圆锥解析重建,嵌入式锥孔优先局部重切)、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge倒圆、Edge倒角、圆Edge半径/直径并通过 `建模意图` 选择自动/相邻圆柱/缩放所属、修改Edge长度并通过 `建模意图` 选择自动/只改当前Edge/移动端面/相邻圆柱/缩放所属,以及直线Edge起点/终点坐标。
|
- 用户可以修改可控的特征,比如 Face 的偏移、稳定矩形/简单全平面 Face 的面内长度和面内宽度、壳体厚度,并通过 `建模意图` 选择拉伸/切除、局部重建、移动特征或缩放特征;面积是结果变量,放在诊断信息里查看,不作为特征参数表里的驱动变量。Face/直线 Edge 的 `中心` 移动后端能力暂时保留,但不在特征参数表开放。还可以做孔径/半径调整、孔/槽轴心并通过 `建模意图` 选择移动局部特征或移动特征、槽宽/槽深/弧长并通过 `建模意图` 选择局部重建或整体缩放、弧角、开口角和槽孔总长度调整、盲孔/盲槽深度(局部切补或整体缩放)、壳体厚度(局部拉伸/切除或整体缩放)、凸台直径/半径/高度/轴心并通过 `建模意图` 选择局部修改或整体调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度、简单等半径圆角链半径/弧长并通过 `建模意图` 选择重建圆角或整体调整、已有等距倒角距离、圆锥参考半径/直径/半角(简单圆锥解析重建,嵌入式锥孔优先局部重切)、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge倒圆、Edge倒角、圆Edge半径/直径并通过 `建模意图` 选择自动/相邻圆柱/缩放所属、修改Edge长度并通过 `建模意图` 选择自动/只改当前Edge/移动端面/相邻圆柱/缩放所属,以及直线Edge起点/终点坐标。
|
||||||
- 用户可以撤销/重做修改,避免一步编辑做坏。
|
- 用户可以撤销/重做修改,避免一步编辑做坏。
|
||||||
- 用户可以导出修改后的完整模型。
|
- 用户可以导出修改后的完整模型。
|
||||||
- 如果 STEP 文件里存在多个互不关联的零件,用户可以只导出选中的某个零件。
|
- 如果 STEP 文件里存在多个互不关联的零件,用户可以只导出选中的某个零件。
|
||||||
@@ -465,7 +465,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
|
|||||||
| 对象 | 现在能改哪些 | 程序大概怎么改 | 暂时还不能稳定改 |
|
| 对象 | 现在能改哪些 | 程序大概怎么改 | 暂时还不能稳定改 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Edge / 边 | 直线边的长度、起点、终点;直线边新增圆角/倒角;圆形或圆弧边的半径/直径、圆心/相邻轴心;椭圆边的主半径/小半径。 | 简单模型优先局部重建当前边和相邻面;圆边可复用相邻圆柱改孔/槽/凸台直径或轴心;椭圆边可沿主轴或小轴单轴缩放所属特征。 | 任意复杂曲线边、B-spline 边、需要复杂约束跟随的边;直线 Edge 中心移动暂不在参数表开放。 |
|
| Edge / 边 | 直线边的长度、起点、终点;直线边新增圆角/倒角;圆形或圆弧边的半径/直径、圆心/相邻轴心;椭圆边的主半径/小半径。 | 简单模型优先局部重建当前边和相邻面;圆边可复用相邻圆柱改孔/槽/凸台直径或轴心;椭圆边可沿主轴或小轴单轴缩放所属特征。 | 任意复杂曲线边、B-spline 边、需要复杂约束跟随的边;直线 Edge 中心移动暂不在参数表开放。 |
|
||||||
| Face / 面 | 面内长度、面内宽度、偏移、壳体厚度;圆柱端盖或完整圆柱侧面的高度。面积作为诊断结果只读展示。 | 拉伸/切除当前平面或圆柱端盖;按尺寸局部重建相邻面;或缩放所属特征。 | 自由曲面会明确识别为只读,不开放伪参数化修改;Face 中心移动暂不在参数表开放;带复杂内孔的面、复杂壳体局部区域、非完整圆柱侧面高度。 |
|
| Face / 面 | 偏移、壳体厚度;稳定矩形/简单全平面 Face 的面内长度、面内宽度;圆柱端盖或完整圆柱侧面的高度。面积作为诊断结果只读展示。 | 拉伸/切除当前平面或圆柱端盖;稳定矩形特征按尺寸局部重建相邻面;或缩放所属特征。 | 自由曲面会明确识别为只读,不开放伪参数化修改;Face 中心移动暂不在参数表开放;带复杂内孔的面、普通曲面、复杂壳体局部区域、非完整圆柱侧面高度。 |
|
||||||
| 孔 / 圆柱孔 | 规则孔的直径/半径、完整孔轴心、完整孔封堵、可识别盲孔/盲槽深度。 | 改孔径时同轴重切孔壁;移动孔时先补旧孔再切新孔;改深度时切削或补料孔底。 | 螺纹孔、锥孔、复杂孔组、底面识别不可靠的盲孔。 |
|
| 孔 / 圆柱孔 | 规则孔的直径/半径、完整孔轴心、完整孔封堵、可识别盲孔/盲槽深度。 | 改孔径时同轴重切孔壁;移动孔时先补旧孔再切新孔;改深度时切削或补料孔底。 | 螺纹孔、锥孔、复杂孔组、底面识别不可靠的盲孔。 |
|
||||||
| 槽 / 半孔 | 槽宽、槽深、弧长、弧角、开口角、简单轴心、长圆槽总长度/中心距;规则矩形槽/口袋的长宽和深度。 | 按圆柱槽、长圆槽胶囊区域或矩形切除包络,先补旧槽,再切出新槽。 | 复杂草图槽、非圆柱槽、多槽联动、跨复杂面的槽;矩形槽中心移动暂不在参数表开放。 |
|
| 槽 / 半孔 | 槽宽、槽深、弧长、弧角、开口角、简单轴心、长圆槽总长度/中心距;规则矩形槽/口袋的长宽和深度。 | 按圆柱槽、长圆槽胶囊区域或矩形切除包络,先补旧槽,再切出新槽。 | 复杂草图槽、非圆柱槽、多槽联动、跨复杂面的槽;矩形槽中心移动暂不在参数表开放。 |
|
||||||
| 凸台 / 外圆 | 圆柱凸台的直径/半径、高度、轴心;规则矩形凸台/口袋的长度、宽度、高度/深度;多台阶矩形凸台的顶层规则台阶。 | 移除旧特征包络后重建;或拉伸/切除端盖、轴向缩放;多台阶中间过渡面会识别为受限并快速阻止。 | 异形凸台、多台阶凸台整组联动、复杂相邻拓扑;矩形中心移动暂不在参数表开放。 |
|
| 凸台 / 外圆 | 圆柱凸台的直径/半径、高度、轴心;规则矩形凸台/口袋的长度、宽度、高度/深度;多台阶矩形凸台的顶层规则台阶。 | 移除旧特征包络后重建;或拉伸/切除端盖、轴向缩放;多台阶中间过渡面会识别为受限并快速阻止。 | 异形凸台、多台阶凸台整组联动、复杂相邻拓扑;矩形中心移动暂不在参数表开放。 |
|
||||||
@@ -478,7 +478,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
|
|||||||
|
|
||||||
| 对象 / 特征 | 现在可以修改 | 暂不能保证或还不能修改 |
|
| 对象 / 特征 | 现在可以修改 | 暂不能保证或还不能修改 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Face | 平面 Face 可以改 `面内长度`、`面内宽度` 和 `偏移`;这些行会在 `建模意图` 里选择 `局部重建`、`拉伸/切除`、`移动特征` 或 `缩放特征`。面积是结果变量,只在诊断信息里展示。识别到相对平面时还可以改 `壳体厚度`,并通过 `建模意图` 选择 `拉伸/切除` 或 `缩放特征`。圆柱上/下端盖、同轴空心圆柱的环形端面可以通过 `拉伸/切除` 改高度;选中完整圆柱侧面时,也可以把 `高度` 换算成端盖拉伸/切除,或者选择沿轴向 `缩放特征`。简单端盖会解析重建,复杂同轴筒体会优先只处理旧端盖到新端盖之间的局部段,端盖边界上有槽口/缺口时会按真实端盖轮廓向外拉伸或在安全范围内向内切削。 | `面内长度/面内宽度` 是这个 Face 自身平面里的两个方向尺寸,不是面积,也不是模型高度;`偏移` 是沿当前面垂直方向测到的位置,不再单独显示容易混淆的额外的“本次移动量”提示。`拉伸/切除` 会加料或切削;`局部重建` 会移动当前 Face 顶点并重建相邻平面,相邻面可能变斜;`缩放特征` 会缩放所属特征或 Solid 并影响其它尺寸;`中心` 移动后端能力暂时保留,但客户参数表不开放;自由曲面会显示为只读受限对象,不把面积、中心、包围盒或底层 UV 参数伪装成可修改尺寸;复杂壳体局部区域、非完整圆柱侧面、非同轴多孔环形端面、端盖边界本身连着槽/缺口/台阶且向内切除越过槽底/台阶终点的面和其它带复杂边界的面仍不能稳定做局部尺寸编辑,界面会禁用或快速阻止并在提示里说明具体原因,建议改用孔/槽专门入口、`拉伸/切除` 的简单端盖路径或整体调整。 |
|
| Face | 平面 Face 总是优先开放 `偏移`;只有识别为稳定矩形凸台/口袋或简单全平面实体时,才显示 `面内长度` 和 `面内宽度`。这些行会在 `建模意图` 里选择 `局部重建`、`拉伸/切除`、`移动特征` 或 `缩放特征`。面积是结果变量,只在诊断信息里展示。识别到相对平面时还可以改 `壳体厚度`,并通过 `建模意图` 选择 `拉伸/切除` 或 `缩放特征`。圆柱上/下端盖、同轴空心圆柱的环形端面可以通过 `拉伸/切除` 改高度;选中完整圆柱侧面时,也可以把 `高度` 换算成端盖拉伸/切除,或者选择沿轴向 `缩放特征`。简单端盖会解析重建,复杂同轴筒体会优先只处理旧端盖到新端盖之间的局部段,端盖边界上有槽口/缺口时会按真实端盖轮廓向外拉伸或在安全范围内向内切削。 | `面内长度/面内宽度` 是矩形平面特征在两个主方向上的驱动尺寸,不是任意曲面尺寸,不是面积,也不是模型高度;不满足稳定识别时这两行不会出现在特征参数表里。`偏移` 是沿当前面垂直方向测到的位置,不再单独显示容易混淆的额外的“本次移动量”提示。`拉伸/切除` 会加料或切削;`局部重建` 会重建当前矩形特征,相邻面按新边界更新;`缩放特征` 会缩放所属特征或 Solid 并影响其它尺寸;`中心` 移动后端能力暂时保留,但客户参数表不开放;自由曲面会显示为只读受限对象,不把面积、中心、包围盒或底层 UV 参数伪装成可修改尺寸;复杂壳体局部区域、非完整圆柱侧面、非同轴多孔环形端面、端盖边界本身连着槽/缺口/台阶且向内切除越过槽底/台阶终点的面和其它带复杂边界的面仍不能稳定做局部尺寸编辑,界面会禁用或快速阻止并在提示里说明具体原因,建议改用孔/槽专门入口、`拉伸/切除` 的简单端盖路径或整体调整。 |
|
||||||
| 圆柱孔 / 圆柱面 | 孔或圆柱的 `直径`、`半径`,并通过 `建模意图` 选择 `只改孔/槽壁` 或 `缩放特征`;完整圆柱孔可以改轴心坐标;完整孔可以尝试封堵。普通孔径语义是同轴圆柱重切,孔轴心语义是先填旧孔再切新孔。 | 复杂孔、螺纹孔、锥孔、不完整圆柱孔和拓扑不稳定的孔可能失败并回滚;`缩放特征` 会影响同一对象上的高度、厚度和其它尺寸,不适合只想改孔壁的场景。 |
|
| 圆柱孔 / 圆柱面 | 孔或圆柱的 `直径`、`半径`,并通过 `建模意图` 选择 `只改孔/槽壁` 或 `缩放特征`;完整圆柱孔可以改轴心坐标;完整孔可以尝试封堵。普通孔径语义是同轴圆柱重切,孔轴心语义是先填旧孔再切新孔。 | 复杂孔、螺纹孔、锥孔、不完整圆柱孔和拓扑不稳定的孔可能失败并回滚;`缩放特征` 会影响同一对象上的高度、厚度和其它尺寸,不适合只想改孔壁的场景。 |
|
||||||
| 盲孔 / 盲槽 | 识别到疑似底面 Face 时,可以改 `盲孔/盲槽深度`,并通过 `建模意图` 选择 `改底面深度` 或 `缩放特征`。`改底面深度` 加深会沿开口到底面方向切削,变浅会从新底面到旧底面补料;`缩放特征` 会沿孔/槽轴向缩放所属特征或 Solid。 | STEP 不保存真实孔深历史;找不到可靠底面或底面被复杂拓扑切碎时,深度只能只读或需要手动辅助;`缩放特征` 会影响同一对象上的壁厚、孔距和其它轴向尺寸,不适合只想改孔底位置的场景。 |
|
| 盲孔 / 盲槽 | 识别到疑似底面 Face 时,可以改 `盲孔/盲槽深度`,并通过 `建模意图` 选择 `改底面深度` 或 `缩放特征`。`改底面深度` 加深会沿开口到底面方向切削,变浅会从新底面到旧底面补料;`缩放特征` 会沿孔/槽轴向缩放所属特征或 Solid。 | STEP 不保存真实孔深历史;找不到可靠底面或底面被复杂拓扑切碎时,深度只能只读或需要手动辅助;`缩放特征` 会影响同一对象上的壁厚、孔距和其它轴向尺寸,不适合只想改孔底位置的场景。 |
|
||||||
| 槽 / 半孔 | 可以改槽宽、槽深、弧长、弧角、开口角和简单局部轴心;槽宽、槽深和弧长会在 `建模意图` 里选择 `只改槽壁` 或 `缩放特征`。长圆槽的轴心、总长度和中心距会在点击修改时自动尝试配对另一个半圆端,也可以手动填写配对端 Face ID。规则矩形槽/口袋可以改长宽和深度。复杂/交叉槽会识别为受限,不暴露伪可改槽参数。 | 轴心仍是受限的局部扇形槽/胶囊槽重建;复杂草图槽、非圆柱槽、多槽关联迁移、链式槽和跨多个不规则面的槽还不是稳定能力。矩形槽/口袋会局部重建矩形切除包络,但长宽方向交换和中心移动暂不在参数表开放。`只改槽壁` 会局部重建槽,`缩放特征` 会缩放所属对象并影响其它尺寸;如果结果真的把一个 Solid 拆成多个 Solid,会自动回滚。 |
|
| 槽 / 半孔 | 可以改槽宽、槽深、弧长、弧角、开口角和简单局部轴心;槽宽、槽深和弧长会在 `建模意图` 里选择 `只改槽壁` 或 `缩放特征`。长圆槽的轴心、总长度和中心距会在点击修改时自动尝试配对另一个半圆端,也可以手动填写配对端 Face ID。规则矩形槽/口袋可以改长宽和深度。复杂/交叉槽会识别为受限,不暴露伪可改槽参数。 | 轴心仍是受限的局部扇形槽/胶囊槽重建;复杂草图槽、非圆柱槽、多槽关联迁移、链式槽和跨多个不规则面的槽还不是稳定能力。矩形槽/口袋会局部重建矩形切除包络,但长宽方向交换和中心移动暂不在参数表开放。`只改槽壁` 会局部重建槽,`缩放特征` 会缩放所属对象并影响其它尺寸;如果结果真的把一个 Solid 拆成多个 Solid,会自动回滚。 |
|
||||||
@@ -826,7 +826,7 @@ vertices: 3262
|
|||||||
|
|
||||||
Face 阶段的当前验收口径(R1 已收口):
|
Face 阶段的当前验收口径(R1 已收口):
|
||||||
|
|
||||||
- 已验收:平面 Face 的 `面内长度`、`面内宽度`、`偏移`,以及壳体厚度、圆柱端盖/圆柱侧面高度这类挂在 Face 入口上的高频编辑;支持 `局部重建`、`拉伸/切除`、`移动特征`、`缩放特征` 和受限的 `保持关系`。`中心` 移动后端保留,但特征参数表暂不开放。
|
- 已验收:平面 Face 的 `偏移`,稳定矩形/简单全平面 Face 的 `面内长度`、`面内宽度`,以及壳体厚度、圆柱端盖/圆柱侧面高度这类挂在 Face 入口上的高频编辑;支持 `局部重建`、`拉伸/切除`、`移动特征`、`缩放特征` 和受限的 `保持关系`。`中心` 移动后端保留,但特征参数表暂不开放。
|
||||||
- 已验收:Face 的 `一级关系` 定义为选中 Face 本身、必要的同域/共面碎片 Face、这些 Face 的边界 Edge/Vertex,以及与该区域共享边的直接相邻 Face。只共享顶点的对象、相邻 Face 再连出去的 Face 都不作为 R1 自动传播范围。
|
- 已验收:Face 的 `一级关系` 定义为选中 Face 本身、必要的同域/共面碎片 Face、这些 Face 的边界 Edge/Vertex,以及与该区域共享边的直接相邻 Face。只共享顶点的对象、相邻 Face 再连出去的 Face 都不作为 R1 自动传播范围。
|
||||||
- 已验收:选中平面 Face 时,程序会生成 `一级关系` 事实包,记录当前 Face 区域、同域/共面碎片、一级平行/垂直关系、一级同轴圆柱事实、边界 Edge/Vertex、共享边相邻 Face,并明确二级、三级关系暂不自动传播;这些事实用于计划、校验和诊断,不再作为特征参数行显示。
|
- 已验收:选中平面 Face 时,程序会生成 `一级关系` 事实包,记录当前 Face 区域、同域/共面碎片、一级平行/垂直关系、一级同轴圆柱事实、边界 Edge/Vertex、共享边相邻 Face,并明确二级、三级关系暂不自动传播;这些事实用于计划、校验和诊断,不再作为特征参数行显示。
|
||||||
- 可用但受限:带内孔平面、曲面 Solid 上的平面、自由曲面、非矩形面、复杂端盖、接近切穿的内切等场景会按明确守门执行;能稳定改就走隔离/回滚,不能稳定改就提前 blocked 并说明原因。
|
- 可用但受限:带内孔平面、曲面 Solid 上的平面、自由曲面、非矩形面、复杂端盖、接近切穿的内切等场景会按明确守门执行;能稳定改就走隔离/回滚,不能稳定改就提前 blocked 并说明原因。
|
||||||
@@ -982,17 +982,18 @@ Face 阶段的当前验收口径(R1 已收口):
|
|||||||
- `偏移`:
|
- `偏移`:
|
||||||
- 先选择一个平面 Face。
|
- 先选择一个平面 Face。
|
||||||
- 在当前选中对象表里修改 `偏移`,再用 `建模意图` 选择改法。
|
- 在当前选中对象表里修改 `偏移`,再用 `建模意图` 选择改法。
|
||||||
- `偏移` 是沿当前面垂直方向测到的位置,不是面积,不是移动距离,也不是 X/Y/Z 坐标。
|
- `偏移` 是沿程序判定的材料外侧法向测到的位置,不是面积,不是移动距离,也不是 X/Y/Z 坐标;它不一定等同于 STEP 原始拓扑法向坐标。
|
||||||
- `拉伸/切除` 会把目标位置换算成这次要移动的距离,然后沿当前面的垂直方向加料或切削,适合改变厚度、拉出凸起或切入凹槽。
|
- `拉伸/切除` 会把目标位置换算成这次要移动的距离,然后沿判定的材料外侧法向加料或切削,适合改变厚度、拉出凸起或切入凹槽。
|
||||||
- `局部重建` 会把当前 Face 移到目标位置,并重建相邻平面;相邻面可能变斜。
|
- `局部重建` 会把当前 Face 移到目标位置,并重建相邻平面;相邻面可能变斜。
|
||||||
- `移动特征` 会沿当前面垂直方向平移所属特征或 Solid;形状和内部尺寸不变。
|
- `移动特征` 会沿当前面垂直方向平移所属特征或 Solid;形状和内部尺寸不变。
|
||||||
- 如果选中Face周围有共面且相接/重叠的碎面,`拉伸/切除` 会自动把这些Face合成同一片拉伸/切除区域,属性表和历史记录会显示 `拉伸/切除共面区域 Face 数`。
|
- 如果选中Face周围有共面且相接/重叠的碎面,`拉伸/切除` 会自动把这些Face合成同一片拉伸/切除区域,属性表和历史记录会显示 `拉伸/切除共面区域 Face 数`。
|
||||||
- 当前拉伸/切除方向判断通过Solid的 inside / outside 采样得到;如果采样不明确,会退回到拓扑方向法向,并在属性里显示低置信度说明。
|
- 当前拉伸/切除方向判断通过Solid的 inside / outside 采样得到;如果采样不明确,会退回到拓扑方向法向,并在属性里显示低置信度说明。
|
||||||
- `面内长度` / `面内宽度`:
|
- `面内长度` / `面内宽度`:
|
||||||
- 先选择一个 Face,在当前选中对象表里修改目标值,再用 `建模意图` 选择改法。
|
- 这两个参数只在稳定矩形凸台/口袋或简单全平面实体上显示;普通曲面、复杂平面和带内孔平面不会把它们伪装成可改参数。
|
||||||
- `局部重建` 会在当前 Face 所在平面内移动顶点并重建周边相邻平面;当前只对简单全平面实体开放。
|
- 在当前选中对象表里修改目标值,再用 `建模意图` 选择改法。
|
||||||
|
- `局部重建` 会在当前矩形平面特征内移动边界并重建周边相邻平面;当前只对稳定矩形特征或简单全平面实体开放。
|
||||||
- `缩放特征` 会缩放所属特征或 Solid;同一对象上的孔、槽、凸台、厚度和其它间距会跟着变化。
|
- `缩放特征` 会缩放所属特征或 Solid;同一对象上的孔、槽、凸台、厚度和其它间距会跟着变化。
|
||||||
- `面内长度` / `面内宽度` 是当前 Face 在自身平面内两个稳定方向上的投影尺寸,不是面积,不是模型整体高度,也不是全局 X/Y/Z 包围盒尺寸。面积会在诊断信息中作为结果值展示。
|
- `面内长度` / `面内宽度` 是矩形平面特征在自身平面内两个稳定方向上的驱动尺寸,不是面积,不是任意曲面的通用尺寸,不是模型整体高度,也不是全局 X/Y/Z 包围盒尺寸。面积会在诊断信息中作为结果值展示。
|
||||||
- `中心`:
|
- `中心`:
|
||||||
- 后端保留了 Face 中心移动能力和回归基线,但模型工程师认为当前作用不大,特征参数表暂时不开放这一行。
|
- 后端保留了 Face 中心移动能力和回归基线,但模型工程师认为当前作用不大,特征参数表暂时不开放这一行。
|
||||||
- `孔直径` + `调整圆柱孔径`:
|
- `孔直径` + `调整圆柱孔径`:
|
||||||
@@ -1231,6 +1232,7 @@ Face 阶段的当前验收口径(R1 已收口):
|
|||||||
- `导出选中面区域`:先切换到 `Face` 选择模式并选中一个Face,再导出该Face;如果当前选择已经带有特征区域信息,则会导出这片已知区域。
|
- `导出选中面区域`:先切换到 `Face` 选择模式并选中一个Face,再导出该Face;如果当前选择已经带有特征区域信息,则会导出这片已知区域。
|
||||||
- `导出选中特征区域`:先切换到 `特征` 选择模式并选中特征,再导出当前特征高亮的Face集合。
|
- `导出选中特征区域`:先切换到 `特征` 选择模式并选中特征,再导出当前特征高亮的Face集合。
|
||||||
- `导出选中Edge`:先切换到 `Edge` 选择模式并选中一个Edge,再导出该Edge。
|
- `导出选中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 文件。
|
||||||
- `检查导出质量`:检查当前选中对象;如果没有选中对象,就检查当前完整模型。
|
- `检查导出质量`:检查当前选中对象;如果没有选中对象,就检查当前完整模型。
|
||||||
- `修复当前模型`:对当前完整模型执行 ShapeFix 和同域面/边合并,完成后会刷新显示并写入操作历史;如果修复失败,会尝试回滚到修复前状态。
|
- `修复当前模型`:对当前完整模型执行 ShapeFix 和同域面/边合并,完成后会刷新显示并写入操作历史;如果修复失败,会尝试回滚到修复前状态。
|
||||||
- `修复选中零件/Solid`:优先修复当前选中对象所属Solid;如果当前对象没有Solid,则修复所属零件。Face或Edge被选中时,也会按其所属Solid/零件执行局部修复。
|
- `修复选中零件/Solid`:优先修复当前选中对象所属Solid;如果当前对象没有Solid,则修复所属零件。Face或Edge被选中时,也会按其所属Solid/零件执行局部修复。
|
||||||
@@ -1244,6 +1246,8 @@ Face 阶段的当前验收口径(R1 已收口):
|
|||||||
```text
|
```text
|
||||||
pythonocc-step-editor/
|
pythonocc-step-editor/
|
||||||
main.py # 程序入口,保持 python main.py 启动
|
main.py # 程序入口,保持 python main.py 启动
|
||||||
|
data.json # 导出参数生成的本地流程参数文件,默认不提交
|
||||||
|
nodes/ # 导出参数生成的本地组件目录,组件参数会嵌入 main.py,默认不提交
|
||||||
assets/
|
assets/
|
||||||
models/
|
models/
|
||||||
geom_extract.step # 当前默认测试模型
|
geom_extract.step # 当前默认测试模型
|
||||||
@@ -1293,6 +1297,7 @@ pythonocc-step-editor/
|
|||||||
verify_hole_resize.py # 临时生成通孔/盲孔 STEP 并验证孔径/孔轴心/盲孔深度修改
|
verify_hole_resize.py # 临时生成通孔/盲孔 STEP 并验证孔径/孔轴心/盲孔深度修改
|
||||||
verify_property_editor_specs.py # 验证属性表不会把通用 Face 编辑混入孔/槽/凸台/圆角/解析曲面特征
|
verify_property_editor_specs.py # 验证属性表不会把通用 Face 编辑混入孔/槽/凸台/圆角/解析曲面特征
|
||||||
verify_property_card_editor_ui.py # 验证参数表 UI 能构建、检测目标值修改并展开完整参数
|
verify_property_card_editor_ui.py # 验证参数表 UI 能构建、检测目标值修改并展开完整参数
|
||||||
|
verify_parametric_component_export.py # 验证导出参数会生成嵌入参数列表的组件 main.py,组件目录不额外写 data.json
|
||||||
verify_shell_edit_suite.py # 一键运行壳体厚度专项修改验证
|
verify_shell_edit_suite.py # 一键运行壳体厚度专项修改验证
|
||||||
verify_shell_thickness_resize.py # 临时生成薄板 STEP 并验证壳体厚度局部/整体修改
|
verify_shell_thickness_resize.py # 临时生成薄板 STEP 并验证壳体厚度局部/整体修改
|
||||||
verify_slot_resize.py # 临时生成槽 STEP 并验证槽宽/槽深/弧长/弧角/半圆槽轴心/长圆槽轴心/总长度/中心距修改
|
verify_slot_resize.py # 临时生成槽 STEP 并验证槽宽/槽深/弧长/弧角/半圆槽轴心/长圆槽轴心/总长度/中心距修改
|
||||||
@@ -1408,7 +1413,7 @@ git diff --check
|
|||||||
- `assets/screenshots/`:调试截图和问题截图,默认忽略,不提交。
|
- `assets/screenshots/`:调试截图和问题截图,默认忽略,不提交。
|
||||||
- `scripts/generate_cube_step.py`:生成 `assets/models/cube_10mm.step`。
|
- `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_acceptance_docs.py`:验证 README 里声明的一级验收口径、阶段命令和脚本清单仍然能对上 `verify_first_level_edit_suites.py`,避免目标文档和实际验证入口脱节。
|
||||||
- `scripts/verify_first_level_edit_suites.py`:一级编辑总验证入口。默认按 Face、孔/槽、Edge、凸台、圆角/倒角、壳体和解析曲面阶段串起现有专项套件;`--quick` 跑烟测、属性表、参数表 UI、一级事实图、关联探测、显示网格预算和验收文档一致性;`--stage edge` 这类参数可只跑某个阶段。
|
- `scripts/verify_first_level_edit_suites.py`:一级编辑总验证入口。默认按 Face、孔/槽、Edge、凸台、圆角/倒角、壳体和解析曲面阶段串起现有专项套件;`--quick` 跑烟测、属性表、参数表 UI、ICEPAK 真实 STEP 同域圆柱孔、一级事实图、关联探测、显示网格预算和验收文档一致性;`--stage edge` 这类参数可只跑某个阶段。
|
||||||
- `scripts/verify_analytic_surface_resize.py`:临时生成圆锥、球面和环面 STEP,验证简单圆锥参考半径/半角解析重建,以及球面半径、环面主半径和环面小半径整体缩放。
|
- `scripts/verify_analytic_surface_resize.py`:临时生成圆锥、球面和环面 STEP,验证简单圆锥参考半径/半角解析重建,以及球面半径、环面主半径和环面小半径整体缩放。
|
||||||
- `scripts/verify_cone_semi_angle_isolation.py`:临时生成简单圆锥、旋转圆锥、嵌入式沉头锥孔和带圆柱通孔的沉头锥孔,验证圆锥半角/参考半径的大参数修改会走解析重建或锥孔局部重切;同时覆盖非端面参考半径的局部重切推导、圆锥特征的小端/大端/高度/半角信息、真实模型复杂浅锥大幅半角修改的提前阻断、`1499.5 -> 1700` 这类参考半径修改的快速阻止,以及隔离子进程保护主界面。
|
- `scripts/verify_cone_semi_angle_isolation.py`:临时生成简单圆锥、旋转圆锥、嵌入式沉头锥孔和带圆柱通孔的沉头锥孔,验证圆锥半角/参考半径的大参数修改会走解析重建或锥孔局部重切;同时覆盖非端面参考半径的局部重切推导、圆锥特征的小端/大端/高度/半角信息、真实模型复杂浅锥大幅半角修改的提前阻断、`1499.5 -> 1700` 这类参考半径修改的快速阻止,以及隔离子进程保护主界面。
|
||||||
- `scripts/verify_boss_resize.py`:临时生成底板加圆柱凸台 STEP,验证凸台直径扩大、直径缩小、高度修改和轴心移动的局部重建链路,并断言不会把单 Solid 拆成多个 Solid。
|
- `scripts/verify_boss_resize.py`:临时生成底板加圆柱凸台 STEP,验证凸台直径扩大、直径缩小、高度修改和轴心移动的局部重建链路,并断言不会把单 Solid 拆成多个 Solid。
|
||||||
@@ -1448,6 +1453,7 @@ git diff --check
|
|||||||
- `scripts/verify_cylindrical_height_resize.py`:临时生成简单圆柱、同轴空心圆柱、多孔圆柱和顶端带缺口圆柱,验证选中圆柱侧面修改高度时会走端盖拉伸/切除并校验目标高度、半径和一级拓扑;同时覆盖 isolated worker 子进程路径,确认这些高频 Face 修改不会把主界面拖进危险计算;`缩放特征` 的轴向缩放会按轴向跨度校验结果。
|
- `scripts/verify_cylindrical_height_resize.py`:临时生成简单圆柱、同轴空心圆柱、多孔圆柱和顶端带缺口圆柱,验证选中圆柱侧面修改高度时会走端盖拉伸/切除并校验目标高度、半径和一级拓扑;同时覆盖 isolated worker 子进程路径,确认这些高频 Face 修改不会把主界面拖进危险计算;`缩放特征` 的轴向缩放会按轴向跨度校验结果。
|
||||||
- `scripts/verify_large_stepped_cap_push_pull.py`:加载 `assets/models/geom_extract.step`,自动找到 y=-39 的同轴阶梯环形端盖和 y=-57.5 的多内孔大平面端盖,分别验证 `-39 -> 50` 走 `local-shell-rebuild`、`57.5 -> 92` 走 `boundary-shell-rebuild`,并验证多内孔大端面浅范围向内收缩也走 `boundary-shell-rebuild`,深向内切除会快速阻止且说明材料厚度或二级关系原因;成功路径保持单 Solid,原逻辑 Face ID 会继续指向移动后的新端面,一级边界 Edge/Vertex、共享边相邻 Face 和内孔线圈数量不能丢,并同时覆盖隔离子进程路径,避免 UI 操作退回 180s 布尔/全模型结果扫描慢路径。
|
- `scripts/verify_large_stepped_cap_push_pull.py`:加载 `assets/models/geom_extract.step`,自动找到 y=-39 的同轴阶梯环形端盖和 y=-57.5 的多内孔大平面端盖,分别验证 `-39 -> 50` 走 `local-shell-rebuild`、`57.5 -> 92` 走 `boundary-shell-rebuild`,并验证多内孔大端面浅范围向内收缩也走 `boundary-shell-rebuild`,深向内切除会快速阻止且说明材料厚度或二级关系原因;成功路径保持单 Solid,原逻辑 Face ID 会继续指向移动后的新端面,一级边界 Edge/Vertex、共享边相邻 Face 和内孔线圈数量不能丢,并同时覆盖隔离子进程路径,避免 UI 操作退回 180s 布尔/全模型结果扫描慢路径。
|
||||||
- `scripts/verify_hole_slot_edit_suite.py`:集中运行当前孔/槽阶段验证,串起孔/槽一级圆柱拓扑、通孔/盲孔局部重建、半圆槽/长圆槽局部重建、隔离 worker 执行、逻辑 Face ID 保持、识别摘要、一级事实图和属性表分组。后续孔/槽能力新增或重构时优先跑这个套件,而不是只跑单个孔径或槽宽脚本。
|
- `scripts/verify_hole_slot_edit_suite.py`:集中运行当前孔/槽阶段验证,串起孔/槽一级圆柱拓扑、通孔/盲孔局部重建、半圆槽/长圆槽局部重建、隔离 worker 执行、逻辑 Face ID 保持、识别摘要、一级事实图和属性表分组。后续孔/槽能力新增或重构时优先跑这个套件,而不是只跑单个孔径或槽宽脚本。
|
||||||
|
- `scripts/verify_icepak_cylindrical_region_selection.py`:加载本地 `assets/models/ICEPAK-NATURAL.stp` 时,验证 Face87/94 这类被 STEP 拆成两片的同域圆柱孔会按整孔高亮、按整孔生成孔径计划,并能通过隔离 worker 完成 `0.5 -> 0.3` 的孔径缩小;模型文件不存在时自动跳过。
|
||||||
- `scripts/verify_hole_resize.py`:临时生成通孔和盲孔 STEP,验证孔径扩大、孔径缩小、通孔轴心移动、完整通孔封堵、盲孔加深和盲孔变浅的局部重建链路;盲孔/盲槽深度修改现在会在操作内部确认目标深度和一级邻域,失败会回滚。
|
- `scripts/verify_hole_resize.py`:临时生成通孔和盲孔 STEP,验证孔径扩大、孔径缩小、通孔轴心移动、完整通孔封堵、盲孔加深和盲孔变浅的局部重建链路;盲孔/盲槽深度修改现在会在操作内部确认目标深度和一级邻域,失败会回滚。
|
||||||
- `scripts/verify_hole_slot_isolated_edit.py`:临时生成通孔、盲孔、半圆槽和长圆槽 STEP,通过 `python -m step_editor.isolated_edit_worker` 的真实 worker 入口验证孔径、孔径缩放特征、孔轴心、孔封堵、盲孔深度、槽宽、槽宽缩放特征、槽深、弧长、弧角、槽轴心、长圆槽总长度和中心距可以在隔离执行通道里完成,输出 STEP 能重新加载并达到目标值;同时验证会破坏盲孔圆柱语义的整体缩放会被干净拒绝并回滚。窗口侧会额外验证孔径、孔轴心、槽宽、槽轴心、盲孔/盲槽深度、长圆槽总长度和长圆槽轴心隔离任务不会被误套用通用 Face 参数校验,并会把原逻辑 Face ID 继续绑定到修改后的孔壁、槽壁或长圆槽端部 Face。
|
- `scripts/verify_hole_slot_isolated_edit.py`:临时生成通孔、盲孔、半圆槽和长圆槽 STEP,通过 `python -m step_editor.isolated_edit_worker` 的真实 worker 入口验证孔径、孔径缩放特征、孔轴心、孔封堵、盲孔深度、槽宽、槽宽缩放特征、槽深、弧长、弧角、槽轴心、长圆槽总长度和中心距可以在隔离执行通道里完成,输出 STEP 能重新加载并达到目标值;同时验证会破坏盲孔圆柱语义的整体缩放会被干净拒绝并回滚。窗口侧会额外验证孔径、孔轴心、槽宽、槽轴心、盲孔/盲槽深度、长圆槽总长度和长圆槽轴心隔离任务不会被误套用通用 Face 参数校验,并会把原逻辑 Face ID 继续绑定到修改后的孔壁、槽壁或长圆槽端部 Face。
|
||||||
- `scripts/verify_isolated_face_edit.py`:验证 high-risk Face 的偏移、面内尺寸、中心和壳体厚度局部/整体修改可以在隔离子进程里执行,子进程导出结果 STEP 后主流程仍能加载出单 Solid;面积路径保留为后端隔离守门回归。同时验证复杂大 STEP 多内孔端面的 UI 操作计划会延后完整几何计划,避免按钮点击阶段先卡住主界面。
|
- `scripts/verify_isolated_face_edit.py`:验证 high-risk Face 的偏移、面内尺寸、中心和壳体厚度局部/整体修改可以在隔离子进程里执行,子进程导出结果 STEP 后主流程仍能加载出单 Solid;面积路径保留为后端隔离守门回归。同时验证复杂大 STEP 多内孔端面的 UI 操作计划会延后完整几何计划,避免按钮点击阶段先卡住主界面。
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
|||||||
EXPECTED_FACE_ISOLATED_OPERATIONS = {
|
EXPECTED_FACE_ISOLATED_OPERATIONS = {
|
||||||
"push_pull_face",
|
"push_pull_face",
|
||||||
"push_pull_face_keep_relations",
|
"push_pull_face_keep_relations",
|
||||||
|
"translate_face_plane_offset_owning",
|
||||||
"move_face_plane_offset_local",
|
"move_face_plane_offset_local",
|
||||||
"resize_face_area_local",
|
"resize_face_area_local",
|
||||||
"resize_face_area",
|
"resize_face_area",
|
||||||
@@ -27,6 +28,8 @@ EXPECTED_FACE_ISOLATED_OPERATIONS = {
|
|||||||
"resize_shell_thickness_owning_scale",
|
"resize_shell_thickness_owning_scale",
|
||||||
"resize_cylindrical_height",
|
"resize_cylindrical_height",
|
||||||
"resize_cylindrical_boss_height",
|
"resize_cylindrical_boss_height",
|
||||||
|
"resize_cylindrical_boss",
|
||||||
|
"move_cylindrical_boss_axis",
|
||||||
"resize_cylindrical_height_owning_scale",
|
"resize_cylindrical_height_owning_scale",
|
||||||
"resize_cone_reference_radius",
|
"resize_cone_reference_radius",
|
||||||
"resize_cone_semi_angle",
|
"resize_cone_semi_angle",
|
||||||
@@ -232,6 +235,8 @@ def _face_property_actions_from_specs() -> set[str]:
|
|||||||
"bbox_center": (5.0, 5.0, 0.0),
|
"bbox_center": (5.0, 5.0, 0.0),
|
||||||
"local_face_width": 10.0,
|
"local_face_width": 10.0,
|
||||||
"local_face_height": 10.0,
|
"local_face_height": 10.0,
|
||||||
|
"local_face_size_edit_ready": True,
|
||||||
|
"local_face_size_edit_blocker": "",
|
||||||
"local_face_size_center": (5.0, 5.0, 0.0),
|
"local_face_size_center": (5.0, 5.0, 0.0),
|
||||||
"plane_origin": (0.0, 0.0, 0.0),
|
"plane_origin": (0.0, 0.0, 0.0),
|
||||||
"push_pull_outward_direction": (0.0, 0.0, 1.0),
|
"push_pull_outward_direction": (0.0, 0.0, 1.0),
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
assert_keys(
|
assert_keys(
|
||||||
{"surface": "plane"},
|
{"surface": "plane"},
|
||||||
|
(
|
||||||
|
"face_center_position",
|
||||||
|
"face_target_normal_position",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert_keys(
|
||||||
|
{"surface": "plane", "local_face_size_edit_ready": True},
|
||||||
(
|
(
|
||||||
"local_face_width",
|
"local_face_width",
|
||||||
"local_face_height",
|
"local_face_height",
|
||||||
@@ -64,6 +71,14 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
assert_keys(
|
assert_keys(
|
||||||
{"surface": "plane", "shell_region_status": "candidate"},
|
{"surface": "plane", "shell_region_status": "candidate"},
|
||||||
|
(
|
||||||
|
"face_center_position",
|
||||||
|
"face_target_normal_position",
|
||||||
|
"shell_thickness_estimate",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert_keys(
|
||||||
|
{"surface": "plane", "shell_region_status": "candidate", "local_face_size_edit_ready": True},
|
||||||
(
|
(
|
||||||
"local_face_width",
|
"local_face_width",
|
||||||
"local_face_height",
|
"local_face_height",
|
||||||
@@ -78,7 +93,16 @@ def main() -> int:
|
|||||||
"prismatic_profile_status": "candidate",
|
"prismatic_profile_status": "candidate",
|
||||||
"prismatic_extrusion_status": "candidate",
|
"prismatic_extrusion_status": "candidate",
|
||||||
},
|
},
|
||||||
("local_face_width", "local_face_height", "shell_thickness_estimate"),
|
("face_target_normal_position", "shell_thickness_estimate"),
|
||||||
|
)
|
||||||
|
assert_keys(
|
||||||
|
{
|
||||||
|
"surface": "plane",
|
||||||
|
"prismatic_profile_status": "candidate",
|
||||||
|
"prismatic_extrusion_status": "candidate",
|
||||||
|
"local_face_size_edit_ready": True,
|
||||||
|
},
|
||||||
|
("local_face_width", "local_face_height", "face_target_normal_position", "shell_thickness_estimate"),
|
||||||
)
|
)
|
||||||
assert_keys(
|
assert_keys(
|
||||||
{"surface": "torus"},
|
{"surface": "torus"},
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def _verify_readme_mentions(readme: str) -> None:
|
|||||||
"[不能修改 -> 立即说明原因]",
|
"[不能修改 -> 立即说明原因]",
|
||||||
"[一级影响范围 -> 明确显示]",
|
"[一级影响范围 -> 明确显示]",
|
||||||
"Face 阶段的当前验收口径(R1 已收口)",
|
"Face 阶段的当前验收口径(R1 已收口)",
|
||||||
"已验收:平面 Face 的 `面内长度`、`面内宽度`、`偏移`",
|
"已验收:平面 Face 的 `偏移`,稳定矩形/简单全平面 Face 的 `面内长度`、`面内宽度`",
|
||||||
"未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建",
|
"未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建",
|
||||||
"孔/槽阶段的当前验收口径(R2/R3 已收口)",
|
"孔/槽阶段的当前验收口径(R2/R3 已收口)",
|
||||||
"已验收:圆柱孔/盲孔的 `直径`、`半径`、`轴心`、`盲孔深度`",
|
"已验收:圆柱孔/盲孔的 `直径`、`半径`、`轴心`、`盲孔深度`",
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ QUICK_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|||||||
("Smoke test", ("main.py", "--smoke-test")),
|
("Smoke test", ("main.py", "--smoke-test")),
|
||||||
("Property editor specs", ("verify_property_editor_specs.py",)),
|
("Property editor specs", ("verify_property_editor_specs.py",)),
|
||||||
("Property table editor UI", ("verify_property_card_editor_ui.py",)),
|
("Property table editor UI", ("verify_property_card_editor_ui.py",)),
|
||||||
|
("Parametric component export", ("verify_parametric_component_export.py",)),
|
||||||
|
("ICEPAK cylindrical same-domain hole", ("verify_icepak_cylindrical_region_selection.py",)),
|
||||||
("Feature recognition priority", ("verify_feature_recognition_summary.py",)),
|
("Feature recognition priority", ("verify_feature_recognition_summary.py",)),
|
||||||
("First-level fact graph", ("verify_first_level_fact_graph.py",)),
|
("First-level fact graph", ("verify_first_level_fact_graph.py",)),
|
||||||
("Associated feature probe and display budget", ("verify_associated_features.py",)),
|
("Associated feature probe and display budget", ("verify_associated_features.py",)),
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
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.isolated_edit_worker import run_request
|
||||||
|
from step_editor.model import StepModel
|
||||||
|
from step_editor.window_core import WindowCoreMixin
|
||||||
|
from step_editor.window_state import WindowStateMixin
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "ICEPAK-NATURAL.stp"
|
||||||
|
|
||||||
|
|
||||||
|
class _SelectionHarness(WindowCoreMixin, WindowStateMixin):
|
||||||
|
def __init__(self, model: StepModel) -> None:
|
||||||
|
self.model = model
|
||||||
|
self.feature_detection_level = "current-only"
|
||||||
|
self.operation_in_progress = False
|
||||||
|
self.scan_in_progress = False
|
||||||
|
self.load_in_progress = False
|
||||||
|
self.selected_face_id = None
|
||||||
|
self.selected_edge_id = None
|
||||||
|
self.selected_kind = None
|
||||||
|
self.selected_part_id = None
|
||||||
|
self.selected_solid_id = None
|
||||||
|
self.manual_bottom_face_id = None
|
||||||
|
self.manual_slot_pair_face_id = None
|
||||||
|
|
||||||
|
def _current_feature_detection_level(self) -> str:
|
||||||
|
return str(self.feature_detection_level)
|
||||||
|
|
||||||
|
def select_feature_face(self, face_id: int) -> None:
|
||||||
|
self.selected_face_id = face_id
|
||||||
|
self.selected_edge_id = None
|
||||||
|
self.selected_kind = "feature"
|
||||||
|
self.selected_part_id = self.model.face_part_ids[face_id]
|
||||||
|
self.selected_solid_id = self.model.face_solid_ids[face_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _assert(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
raise AssertionError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def _isolated_hole_resize(model: StepModel, face_id: int, diameter: float, root: Path) -> str:
|
||||||
|
input_path = root / "face87_input.brep"
|
||||||
|
output_path = root / "face87_output.brep"
|
||||||
|
request_path = root / "face87_resize_request.json"
|
||||||
|
model.export_internal_brep(input_path)
|
||||||
|
request_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"input_path": str(input_path),
|
||||||
|
"output_path": str(output_path),
|
||||||
|
"input_format": "brep",
|
||||||
|
"output_format": "brep",
|
||||||
|
"operation": "resize_cylindrical_hole",
|
||||||
|
"args": [face_id, diameter],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
code = run_request(request_path)
|
||||||
|
response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8"))
|
||||||
|
_assert(code == 0 and response.get("ok") is True, f"isolated Face {face_id} resize should pass: {response}")
|
||||||
|
_assert(output_path.exists(), f"isolated Face {face_id} resize should write output BREP")
|
||||||
|
return str(response.get("message") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _edge_ids_from_polydata(polydata) -> set[int]:
|
||||||
|
edge_arr = polydata.GetCellData().GetArray("edge_id") if polydata is not None else None
|
||||||
|
if edge_arr is None:
|
||||||
|
return set()
|
||||||
|
return {int(edge_arr.GetValue(index)) for index in range(edge_arr.GetNumberOfTuples())}
|
||||||
|
|
||||||
|
|
||||||
|
def _polydata_edge_sample_counts(polydata) -> dict[int, list[int]]:
|
||||||
|
edge_arr = polydata.GetCellData().GetArray("edge_id") if polydata is not None else None
|
||||||
|
if edge_arr is None:
|
||||||
|
return {}
|
||||||
|
counts: dict[int, list[int]] = {}
|
||||||
|
for cell_id in range(polydata.GetNumberOfCells()):
|
||||||
|
edge_id = int(edge_arr.GetValue(cell_id))
|
||||||
|
counts.setdefault(edge_id, []).append(int(polydata.GetCell(cell_id).GetNumberOfPoints()))
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_default_edges_hide_same_domain_internal_edges(model: StepModel, label: str) -> None:
|
||||||
|
visible_edge_ids = _edge_ids_from_polydata(model.build_edge_polydata(show_same_domain_internal_edges=False))
|
||||||
|
hidden_internal_ids = set(model._same_domain_internal_edge_ids()) # noqa: SLF001
|
||||||
|
leaked = sorted(visible_edge_ids & hidden_internal_ids)
|
||||||
|
_assert(not leaked, f"{label} default edge display should hide same-domain/internal duplicate edges: {leaked[:12]}")
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_small_circle_edges_are_smooth(model: StepModel, label: str) -> None:
|
||||||
|
edge_polydata = model.build_edge_polydata(deflection=1.6, show_same_domain_internal_edges=False)
|
||||||
|
sample_counts = _polydata_edge_sample_counts(edge_polydata)
|
||||||
|
rough_edges: list[tuple[int, int, float]] = []
|
||||||
|
for edge_id, counts in sample_counts.items():
|
||||||
|
info = model.edge_info(edge_id)
|
||||||
|
if info.get("curve") != "circle":
|
||||||
|
continue
|
||||||
|
radius = float(info.get("radius") or 0.0)
|
||||||
|
if radius <= 0.0 or radius > 0.35:
|
||||||
|
continue
|
||||||
|
point_count = max(counts or [0])
|
||||||
|
if point_count < 9:
|
||||||
|
rough_edges.append((edge_id, point_count, radius))
|
||||||
|
_assert(not rough_edges, f"{label} small circular display edges should not be drawn as triangles: {rough_edges[:12]}")
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_rectangular_face_parameters(model: StepModel, harness: _SelectionHarness) -> None:
|
||||||
|
face_id = 9
|
||||||
|
info = model.feature_info(face_id)
|
||||||
|
_assert(info.get("surface") == "plane", "ICEPAK Face 9 should be a planar rectangular face")
|
||||||
|
_assert(info.get("prismatic_profile_status") == "candidate", "ICEPAK Face 9 should be a rectangular profile")
|
||||||
|
_assert(bool(info.get("local_face_size_edit_ready")), f"ICEPAK Face 9 size parameters should be visible: {info}")
|
||||||
|
width = float(info.get("local_face_width") or 0.0)
|
||||||
|
height = float(info.get("local_face_height") or 0.0)
|
||||||
|
_assert(abs(width - 8.0) <= 1e-7, f"ICEPAK Face 9 length should be read from rectangle edges, got {width:g}")
|
||||||
|
_assert(abs(height - 4.9) <= 1e-7, f"ICEPAK Face 9 width should be read from rectangle edges, got {height:g}")
|
||||||
|
|
||||||
|
harness.select_feature_face(face_id)
|
||||||
|
specs = harness._property_editor_specs(info, info)
|
||||||
|
editable_keys = {str(spec.get("key") or "") for spec in specs if bool(spec.get("editable"))}
|
||||||
|
expected = {"local_face_width", "local_face_height", "face_target_normal_position"}
|
||||||
|
missing = sorted(expected - editable_keys)
|
||||||
|
_assert(not missing, f"ICEPAK Face 9 feature parameters should expose length, width and offset: {missing}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not MODEL_PATH.exists():
|
||||||
|
print("icepak cylindrical region selection skipped: local ICEPAK-NATURAL.stp is not present")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
model = StepModel.load(MODEL_PATH)
|
||||||
|
_assert_default_edges_hide_same_domain_internal_edges(model, "ICEPAK before edit")
|
||||||
|
harness = _SelectionHarness(model)
|
||||||
|
_assert_rectangular_face_parameters(model, harness)
|
||||||
|
expected_regions = {
|
||||||
|
87: [87, 94],
|
||||||
|
94: [87, 94],
|
||||||
|
84: [84, 97],
|
||||||
|
97: [84, 97],
|
||||||
|
}
|
||||||
|
for face_id, expected in expected_regions.items():
|
||||||
|
info = model.quick_face_info(face_id)
|
||||||
|
_assert(info.get("surface") == "cylinder", f"Face {face_id} should be cylindrical")
|
||||||
|
_assert(model.face_region_ids(face_id) == expected, f"Face {face_id} region should be {expected}")
|
||||||
|
_assert(
|
||||||
|
list(info.get("feature_highlight_face_ids") or []) == expected,
|
||||||
|
f"Face {face_id} quick info highlight should include the whole same-domain region: {info.get('feature_highlight_face_ids')}",
|
||||||
|
)
|
||||||
|
_assert(
|
||||||
|
harness._selection_same_domain_face_ids(face_id) == expected,
|
||||||
|
f"Face {face_id} UI selection highlight should include the whole same-domain region",
|
||||||
|
)
|
||||||
|
context = harness._feature_context_info(face_id)
|
||||||
|
_assert(
|
||||||
|
list(context.get("feature_highlight_face_ids") or []) == expected,
|
||||||
|
f"Face {face_id} feature context highlight should include the whole same-domain region: {context.get('feature_highlight_face_ids')}",
|
||||||
|
)
|
||||||
|
feature = model.feature_info(face_id)
|
||||||
|
_assert(
|
||||||
|
list(feature.get("same_domain_face_ids") or []) == expected,
|
||||||
|
f"Face {face_id} feature info should use the whole same-domain region",
|
||||||
|
)
|
||||||
|
_assert(bool(feature.get("is_full_cylinder")), f"Face {face_id} should be treated as a full cylinder")
|
||||||
|
|
||||||
|
resize_model = StepModel.load(MODEL_PATH)
|
||||||
|
plan = resize_model.cylindrical_resize_plan(87, 0.3)
|
||||||
|
_assert(plan.get("feature_type") == "圆柱孔候选", "Face 87 should plan as a cylindrical hole")
|
||||||
|
_assert(list(plan.get("same_domain_face_ids") or []) == [87, 94], "Face 87 resize plan should use both side fragments")
|
||||||
|
_assert(bool(plan.get("is_full_cylinder")), "Face 87 resize plan should preserve full-cylinder semantics")
|
||||||
|
_assert(float(plan.get("angular_span") or 0.0) > 6.0, "Face 87 resize plan should not use the selected half-face span")
|
||||||
|
result = resize_model.resize_cylindrical_hole(87, 0.3)
|
||||||
|
_assert("diameter 0.5 -> 0.3" in result, "Face 87 diameter shrink should complete")
|
||||||
|
_assert_default_edges_hide_same_domain_internal_edges(resize_model, "ICEPAK after Face 87 shrink")
|
||||||
|
_assert_small_circle_edges_are_smooth(resize_model, "ICEPAK after Face 87 shrink")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="icepak_face87_isolated_") as temp_dir:
|
||||||
|
worker_message = _isolated_hole_resize(model, 87, 0.3, Path(temp_dir))
|
||||||
|
_assert("diameter 0.5 -> 0.3" in worker_message, "Face 87 isolated diameter shrink should complete")
|
||||||
|
|
||||||
|
print("icepak cylindrical region selection ok")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||||
|
from OCC.Core.GeomAbs import GeomAbs_Plane
|
||||||
|
|
||||||
|
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.window_actions import WindowActionMixin
|
||||||
|
from step_editor.isolated_edit_worker import run_request
|
||||||
|
from step_editor.model import StepModel
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "ICEPAK-NATURAL.stp"
|
||||||
|
|
||||||
|
|
||||||
|
def _assert(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
raise AssertionError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def _dot(left: tuple[float, float, float], right: tuple[float, float, float]) -> float:
|
||||||
|
return sum(left[index] * right[index] for index in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def _plane_axis(model: StepModel, face_id: int) -> tuple[float, float, float]:
|
||||||
|
surf = BRepAdaptor_Surface(model.faces[face_id])
|
||||||
|
_assert(surf.GetType() == GeomAbs_Plane, f"Face {face_id} should be planar")
|
||||||
|
direction = surf.Plane().Axis().Direction()
|
||||||
|
return (float(direction.X()), float(direction.Y()), float(direction.Z()))
|
||||||
|
|
||||||
|
|
||||||
|
def _isolated_push_pull(model: StepModel, face_id: int, distance: float, temp_dir: Path, stem: str) -> tuple[StepModel, str]:
|
||||||
|
input_path = temp_dir / f"{stem}_input.brep"
|
||||||
|
output_path = temp_dir / f"{stem}_output.brep"
|
||||||
|
request_path = temp_dir / f"{stem}_request.json"
|
||||||
|
model.export_internal_brep(input_path)
|
||||||
|
request_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"input_path": str(input_path),
|
||||||
|
"output_path": str(output_path),
|
||||||
|
"input_format": "brep",
|
||||||
|
"output_format": "brep",
|
||||||
|
"operation": "push_pull_face",
|
||||||
|
"args": [face_id, distance],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
code = run_request(request_path)
|
||||||
|
response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8"))
|
||||||
|
_assert(code == 0 and response.get("ok") is True, f"isolated Face {face_id} push-pull should pass: {response}")
|
||||||
|
_assert(output_path.exists(), f"isolated Face {face_id} push-pull should write an output BREP")
|
||||||
|
return StepModel.load_internal_brep(output_path), str(response.get("message") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _ui_push_pull_context(model: StepModel, face_id: int, plan: dict[str, object]) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"operation_name": "拉伸/切除平面",
|
||||||
|
"target": f"Face {face_id}",
|
||||||
|
"target_kind": "face",
|
||||||
|
"target_id": face_id,
|
||||||
|
"target_logical_id": model.face_logical_id(face_id),
|
||||||
|
"parameters": {
|
||||||
|
"part_id": plan.get("part_id"),
|
||||||
|
"solid_id": plan.get("solid_id"),
|
||||||
|
"semantic_distance": plan.get("distance"),
|
||||||
|
"distance_rule": "positive=outward fuse, negative=inward cut",
|
||||||
|
"surface": plan.get("surface"),
|
||||||
|
"outward_direction": plan.get("outward_direction"),
|
||||||
|
"plane_direction": plan.get("plane_direction"),
|
||||||
|
"current_plane_position": plan.get("current_plane_position"),
|
||||||
|
"target_plane_position": plan.get("target_plane_position"),
|
||||||
|
"current_plane_center": plan.get("current_plane_center"),
|
||||||
|
"target_plane_center": plan.get("target_plane_center"),
|
||||||
|
"direction_confidence": plan.get("direction_confidence"),
|
||||||
|
"direction_note": plan.get("direction_note"),
|
||||||
|
"resize_strategy": plan.get("resize_strategy"),
|
||||||
|
"edit_strategy_label": plan.get("edit_strategy_label"),
|
||||||
|
"edit_semantics": plan.get("edit_semantics"),
|
||||||
|
"push_pull_risk": plan.get("risk"),
|
||||||
|
"push_pull_status": plan.get("status"),
|
||||||
|
"push_pull_message": plan.get("message"),
|
||||||
|
"push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"),
|
||||||
|
"push_pull_scope_face_count": plan.get("push_pull_scope_face_count"),
|
||||||
|
"push_pull_scope_note": plan.get("push_pull_scope_note"),
|
||||||
|
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||||||
|
"selected_boundary_wires": plan.get("selected_boundary_wires"),
|
||||||
|
"selected_inner_boundary_wires": plan.get("selected_inner_boundary_wires"),
|
||||||
|
"selected_has_inner_boundaries": plan.get("selected_has_inner_boundaries"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not MODEL_PATH.exists():
|
||||||
|
print("icepak face45 push-pull skipped: local ICEPAK-NATURAL.stp is not present")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
model = StepModel.load(MODEL_PATH)
|
||||||
|
face_id = 45
|
||||||
|
plan = model.push_pull_plan(face_id, 10.0)
|
||||||
|
_assert(plan.get("status") != "blocked", f"Face 45 positive push-pull should be plannable: {plan}")
|
||||||
|
_assert(float(plan.get("current_plane_position") or 0.0) == 5.0, f"Face 45 should use outward offset +5: {plan}")
|
||||||
|
_assert(float(plan.get("target_plane_position") or 0.0) == 15.0, f"Face 45 target should be +15: {plan}")
|
||||||
|
_assert(plan.get("target_plane_center") is not None, "Face push-pull plan should carry target_plane_center")
|
||||||
|
|
||||||
|
inward_plan = model.push_pull_plan(face_id, -10.0)
|
||||||
|
_assert(inward_plan.get("status") == "blocked", f"Face 45 inward cut through material should be blocked: {inward_plan}")
|
||||||
|
_assert("材料厚度" in str(inward_plan.get("blockers") or ""), f"blocked reason should mention material depth: {inward_plan}")
|
||||||
|
|
||||||
|
direct_model = StepModel.load(MODEL_PATH)
|
||||||
|
direct_model.push_pull_face(face_id, 10.0)
|
||||||
|
direct_followup_plan = direct_model.push_pull_plan(47, 10.0)
|
||||||
|
direct_direction = tuple(float(item) for item in (direct_followup_plan.get("plane_direction") or ()))
|
||||||
|
_assert(
|
||||||
|
len(direct_direction) == 3 and abs(_dot(direct_direction, _plane_axis(direct_model, 47))) >= 0.92,
|
||||||
|
f"Face 47 follow-up direction should stay aligned with its selected plane: {direct_followup_plan}",
|
||||||
|
)
|
||||||
|
direct_followup_message = direct_model.push_pull_face(47, 10.0, plan=dict(direct_followup_plan))
|
||||||
|
direct_followup_target = float(direct_followup_plan.get("target_plane_position") or 0.0)
|
||||||
|
_assert(
|
||||||
|
f"target={direct_followup_target:g}" in direct_followup_message,
|
||||||
|
f"direct second push-pull should match Face 47 target: {direct_followup_message}",
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="icepak_face45_isolated_") as temp_dir:
|
||||||
|
root = Path(temp_dir)
|
||||||
|
edited_model, message = _isolated_push_pull(model, face_id, 10.0, root, "face45")
|
||||||
|
_assert("actual=15" in message and "target=15" in message, f"result check should match target face: {message}")
|
||||||
|
|
||||||
|
followup_face_id = 47
|
||||||
|
followup_plan = edited_model.push_pull_plan(followup_face_id, 10.0)
|
||||||
|
followup_context = _ui_push_pull_context(edited_model, followup_face_id, followup_plan)
|
||||||
|
followup_current = float(followup_plan.get("current_plane_position") or 0.0)
|
||||||
|
followup_target = float(followup_plan.get("target_plane_position") or 0.0)
|
||||||
|
_assert(followup_plan.get("status") != "blocked", f"Face 47 follow-up should be plannable: {followup_plan}")
|
||||||
|
_assert(
|
||||||
|
abs(followup_target - followup_current - 10.0) <= 1e-8,
|
||||||
|
f"Face 47 follow-up target should preserve the requested +10 offset: {followup_plan}",
|
||||||
|
)
|
||||||
|
_second_model, followup_message = _isolated_push_pull(edited_model, followup_face_id, 10.0, root, "face47")
|
||||||
|
_assert(
|
||||||
|
f"actual={followup_target:g}" in followup_message and f"target={followup_target:g}" in followup_message,
|
||||||
|
f"second isolated push-pull should match Face 47 target: {followup_message}",
|
||||||
|
)
|
||||||
|
checker = WindowActionMixin()
|
||||||
|
blocker = checker._face_target_integrity_blocker(_second_model, followup_context)
|
||||||
|
_assert(blocker == "", f"UI target integrity check should accept the second push-pull: {blocker}")
|
||||||
|
|
||||||
|
print("icepak face45/face47 push-pull ok")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import py_compile
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
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.parametric_component import export_parametric_component
|
||||||
|
|
||||||
|
|
||||||
|
def _assert(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
raise AssertionError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="geom_param_component_export_") as temp_dir:
|
||||||
|
root = Path(temp_dir)
|
||||||
|
source_step = root / "source.step"
|
||||||
|
source_step.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
||||||
|
parameters = [
|
||||||
|
{
|
||||||
|
"name": "面内长度",
|
||||||
|
"displayName": "面内长度",
|
||||||
|
"type": "number",
|
||||||
|
"ioRole": "input",
|
||||||
|
"default": "10",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
edits = [
|
||||||
|
{
|
||||||
|
"parameter": "面内长度",
|
||||||
|
"displayName": "面内长度",
|
||||||
|
"targetKind": "feature",
|
||||||
|
"targetId": 0,
|
||||||
|
"uiAction": "resize_face_width_local",
|
||||||
|
"operation": "resize_face_size_local",
|
||||||
|
"args": [0, {"param": "面内长度"}, "width"],
|
||||||
|
"default": 10.0,
|
||||||
|
"valueType": "positive",
|
||||||
|
"scope": "local",
|
||||||
|
"scopeLabel": "局部重建",
|
||||||
|
"sourceStep": str(source_step),
|
||||||
|
"parameterKey": "local_face_width",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
main_py = export_parametric_component(
|
||||||
|
parameters=parameters,
|
||||||
|
edits=edits,
|
||||||
|
source_step=source_step,
|
||||||
|
component_root=root / "nodes",
|
||||||
|
component_name="测试组件",
|
||||||
|
)
|
||||||
|
text = main_py.read_text(encoding="utf-8")
|
||||||
|
_assert("INPUT_PARAMETERS = " in text, "generated main.py should embed selected input parameter list")
|
||||||
|
_assert("PARAMETERS = INPUT_PARAMETERS + OUTPUT_PARAMETERS" in text, "generated main.py should expose FlowEditor parameters")
|
||||||
|
_assert("NODE_INFO = " in text, "generated main.py should expose FlowEditor node info")
|
||||||
|
_assert("def execute(inputs, params, context):" in text, "generated main.py should expose FlowEditor execute entry")
|
||||||
|
_assert("COMPONENT = " in text, "generated main.py should embed execution config")
|
||||||
|
_assert("step_edit_config.json" not in text, "generated component should not require a sidecar config JSON")
|
||||||
|
_assert(not (main_py.parent / "data.json").exists(), "component directory should not contain data.json")
|
||||||
|
_assert(not (main_py.parent / "step_edit_config.json").exists(), "component directory should not contain step_edit_config.json")
|
||||||
|
py_compile.compile(str(main_py), doraise=True)
|
||||||
|
|
||||||
|
namespace: dict[str, object] = {}
|
||||||
|
exec(compile(text, str(main_py), "exec"), namespace)
|
||||||
|
embedded_input_parameters = namespace.get("INPUT_PARAMETERS")
|
||||||
|
embedded_parameters = namespace.get("PARAMETERS")
|
||||||
|
node_info = namespace.get("NODE_INFO")
|
||||||
|
embedded_component = namespace.get("COMPONENT")
|
||||||
|
_assert(embedded_input_parameters == parameters, f"embedded INPUT_PARAMETERS mismatch: {embedded_input_parameters}")
|
||||||
|
_assert(isinstance(embedded_parameters, list), "embedded PARAMETERS should be a list")
|
||||||
|
_assert(embedded_parameters[: len(parameters)] == parameters, f"embedded input parameter prefix mismatch: {embedded_parameters}")
|
||||||
|
_assert(any(row.get("name") == "output_step" and row.get("ioRole") == "output" for row in embedded_parameters if isinstance(row, dict)), "generated PARAMETERS should include output_step output port")
|
||||||
|
_assert(isinstance(node_info, dict), "NODE_INFO should be a dict")
|
||||||
|
_assert(node_info.get("parameters") == embedded_parameters, "NODE_INFO should point to PARAMETERS")
|
||||||
|
_assert(isinstance(embedded_component, dict), "embedded COMPONENT should be a dict")
|
||||||
|
_assert(embedded_component.get("edits") == edits, f"embedded edits mismatch: {json.dumps(embedded_component, ensure_ascii=False)}")
|
||||||
|
|
||||||
|
print("parametric component export ok")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -67,7 +67,9 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
|||||||
self.selected_solid_id = None
|
self.selected_solid_id = None
|
||||||
self.selected_face_id = 0
|
self.selected_face_id = 0
|
||||||
self.selected_edge_id = None
|
self.selected_edge_id = None
|
||||||
|
self.step_path = PROJECT_ROOT / "assets" / "models" / "probe.step"
|
||||||
self.current_info_values = self._plane_info()
|
self.current_info_values = self._plane_info()
|
||||||
|
self.executed_property_actions: list[tuple[str, str]] = []
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
self.object_edit_box = self
|
self.object_edit_box = self
|
||||||
@@ -91,6 +93,8 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
|||||||
self.current_capability_headline = QLabel()
|
self.current_capability_headline = QLabel()
|
||||||
self.apply_property_button = QPushButton()
|
self.apply_property_button = QPushButton()
|
||||||
self.export_parameters_button = QPushButton()
|
self.export_parameters_button = QPushButton()
|
||||||
|
self.face_width_input = QLineEdit()
|
||||||
|
self.face_height_input = QLineEdit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _plane_info() -> dict[str, object]:
|
def _plane_info() -> dict[str, object]:
|
||||||
@@ -101,6 +105,8 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
|||||||
"bbox_diagonal": 14.1421356237,
|
"bbox_diagonal": 14.1421356237,
|
||||||
"local_face_width": 10.0,
|
"local_face_width": 10.0,
|
||||||
"local_face_height": 10.0,
|
"local_face_height": 10.0,
|
||||||
|
"local_face_size_edit_ready": True,
|
||||||
|
"local_face_size_edit_blocker": "",
|
||||||
"plane_origin": (0.0, 0.0, 0.0),
|
"plane_origin": (0.0, 0.0, 0.0),
|
||||||
"push_pull_outward_direction": (0.0, 0.0, 1.0),
|
"push_pull_outward_direction": (0.0, 0.0, 1.0),
|
||||||
"normal": (0.0, 0.0, 1.0),
|
"normal": (0.0, 0.0, 1.0),
|
||||||
@@ -119,6 +125,14 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
|||||||
def _set_control_state(self, widget, enabled: bool, _enabled_tip: str, _disabled_tip: str) -> None:
|
def _set_control_state(self, widget, enabled: bool, _enabled_tip: str, _disabled_tip: str) -> None:
|
||||||
widget.setEnabled(enabled)
|
widget.setEnabled(enabled)
|
||||||
|
|
||||||
|
def resize_face_width_local(self) -> None:
|
||||||
|
self.executed_property_actions.append(("resize_face_width_local", self.face_width_input.text()))
|
||||||
|
self._after_property_edit_finished(success=True)
|
||||||
|
|
||||||
|
def resize_face_height_local(self) -> None:
|
||||||
|
self.executed_property_actions.append(("resize_face_height_local", self.face_height_input.text()))
|
||||||
|
self._after_property_edit_finished(success=True)
|
||||||
|
|
||||||
def statusBar(self) -> _StatusBar:
|
def statusBar(self) -> _StatusBar:
|
||||||
return _StatusBar()
|
return _StatusBar()
|
||||||
|
|
||||||
@@ -130,6 +144,7 @@ class _ActionMessageProbe(WindowActionMixin):
|
|||||||
class _ParameterExportActionProbe(WindowActionMixin):
|
class _ParameterExportActionProbe(WindowActionMixin):
|
||||||
def __init__(self, output_path: Path) -> None:
|
def __init__(self, output_path: Path) -> None:
|
||||||
self.output_path = output_path
|
self.output_path = output_path
|
||||||
|
self.component_path = output_path.parent / "nodes" / "000_test" / "main.py"
|
||||||
self.status_bar = _StatusBarProbe()
|
self.status_bar = _StatusBarProbe()
|
||||||
self.info_text = ""
|
self.info_text = ""
|
||||||
self.export_state_updates = 0
|
self.export_state_updates = 0
|
||||||
@@ -155,6 +170,21 @@ class _ParameterExportActionProbe(WindowActionMixin):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _export_parametric_component_main(self, rows: list[dict[str, str]]) -> Path:
|
||||||
|
assert rows
|
||||||
|
self.component_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
input_rows = json.dumps(rows, ensure_ascii=False, indent=4)
|
||||||
|
self.component_path.write_text(
|
||||||
|
"INPUT_PARAMETERS = "
|
||||||
|
+ input_rows
|
||||||
|
+ "\nOUTPUT_PARAMETERS = [{'name': 'output_step', 'displayName': '输出STEP', 'type': 'file', 'ioRole': 'output', 'default': ''}]"
|
||||||
|
+ "\nPARAMETERS = INPUT_PARAMETERS + OUTPUT_PARAMETERS"
|
||||||
|
+ "\nNODE_INFO = {'parameters': PARAMETERS}"
|
||||||
|
+ "\ndef execute(inputs, params, context):\n return {'output_step': 'modified.step'}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return self.component_path
|
||||||
|
|
||||||
def _update_parameter_export_state(self) -> None:
|
def _update_parameter_export_state(self) -> None:
|
||||||
self.export_state_updates += 1
|
self.export_state_updates += 1
|
||||||
|
|
||||||
@@ -350,6 +380,16 @@ def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
|
|||||||
],
|
],
|
||||||
f"unexpected parameter export payload: {selected_rows}",
|
f"unexpected parameter export payload: {selected_rows}",
|
||||||
)
|
)
|
||||||
|
component_edits = probe._selected_parameter_component_edits(selected_rows)
|
||||||
|
_assert(len(component_edits) == 1, f"selected parameter should map to one component edit: {component_edits}")
|
||||||
|
_assert(
|
||||||
|
component_edits[0]["operation"] == "resize_face_size_local",
|
||||||
|
f"face width export should map to backend resize operation: {component_edits[0]}",
|
||||||
|
)
|
||||||
|
_assert(
|
||||||
|
component_edits[0]["args"] == [0, {"param": "面内长度"}, "width"],
|
||||||
|
f"face width export should embed parameter placeholder args: {component_edits[0]}",
|
||||||
|
)
|
||||||
|
|
||||||
target_widget.setText("12")
|
target_widget.setText("12")
|
||||||
probe._update_property_apply_state()
|
probe._update_property_apply_state()
|
||||||
@@ -357,6 +397,28 @@ def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
|
|||||||
_assert(any(row == target_row for row, _spec, _text in changed), "table target edit was not detected")
|
_assert(any(row == target_row for row, _spec, _text in changed), "table target edit was not detected")
|
||||||
_assert(probe.apply_property_button.isEnabled(), "single parametric modeling button should enable for one changed row")
|
_assert(probe.apply_property_button.isEnabled(), "single parametric modeling button should enable for one changed row")
|
||||||
|
|
||||||
|
width_row = _row_by_label(probe, "面内宽度")
|
||||||
|
width_widget = probe.property_table.cellWidget(width_row, PROPERTY_TARGET_COLUMN)
|
||||||
|
_assert(isinstance(width_widget, QLineEdit), "second editable table row should have a target editor")
|
||||||
|
width_widget.setText("8")
|
||||||
|
probe._update_property_apply_state()
|
||||||
|
changed = probe._changed_property_rows()
|
||||||
|
_assert(len(changed) >= 2, f"two target edits should be detected: {changed}")
|
||||||
|
_assert(probe.apply_property_button.isEnabled(), "parametric modeling button should stay enabled for multiple changed rows")
|
||||||
|
probe.apply_current_property_edit()
|
||||||
|
for _index in range(6):
|
||||||
|
QApplication.processEvents()
|
||||||
|
if not getattr(probe, "property_batch_active", False):
|
||||||
|
break
|
||||||
|
_assert(
|
||||||
|
probe.executed_property_actions == [
|
||||||
|
("resize_face_width_local", "12"),
|
||||||
|
("resize_face_height_local", "8"),
|
||||||
|
],
|
||||||
|
f"batch parametric modeling should execute changed rows in table order: {probe.executed_property_actions}",
|
||||||
|
)
|
||||||
|
_assert(not getattr(probe, "property_batch_active", False), "property batch state should clear after completion")
|
||||||
|
|
||||||
probe.toggle_property_table_expanded()
|
probe.toggle_property_table_expanded()
|
||||||
_assert(probe.property_table_expanded, "property table expand toggle failed")
|
_assert(probe.property_table_expanded, "property table expand toggle failed")
|
||||||
_assert("收起" in probe.property_expand_button.text(), "expanded table button should offer to collapse")
|
_assert("收起" in probe.property_expand_button.text(), "expanded table button should offer to collapse")
|
||||||
@@ -547,6 +609,7 @@ def _assert_parameter_export_action() -> None:
|
|||||||
probe = _ParameterExportActionProbe(output_path)
|
probe = _ParameterExportActionProbe(output_path)
|
||||||
probe.export_selected_parameters()
|
probe.export_selected_parameters()
|
||||||
payload = json.loads(output_path.read_text(encoding="utf-8"))
|
payload = json.loads(output_path.read_text(encoding="utf-8"))
|
||||||
|
component_text = probe.component_path.read_text(encoding="utf-8")
|
||||||
_assert(
|
_assert(
|
||||||
payload == [
|
payload == [
|
||||||
{
|
{
|
||||||
@@ -566,8 +629,22 @@ def _assert_parameter_export_action() -> None:
|
|||||||
],
|
],
|
||||||
f"parameter export action wrote unexpected JSON: {payload}",
|
f"parameter export action wrote unexpected JSON: {payload}",
|
||||||
)
|
)
|
||||||
_assert(probe.status_bar.messages and "已导出 2 个输入参数" in probe.status_bar.messages[-1], "parameter export status should mention exported count")
|
_assert(
|
||||||
_assert("data.json" in probe.info_text and "参数数量:2" in probe.info_text, "parameter export info panel summary should be useful")
|
"INPUT_PARAMETERS" in component_text and "def execute(inputs, params, context):" in component_text and "面内长度" in component_text,
|
||||||
|
"parameter export should generate FlowEditor-style component main.py with embedded input parameters",
|
||||||
|
)
|
||||||
|
_assert(
|
||||||
|
not (probe.component_path.parent / "data.json").exists(),
|
||||||
|
"component export should embed parameters in main.py instead of writing component data.json",
|
||||||
|
)
|
||||||
|
_assert(
|
||||||
|
probe.status_bar.messages and "已导出 2 个输入参数" in probe.status_bar.messages[-1],
|
||||||
|
"parameter export status should mention exported count",
|
||||||
|
)
|
||||||
|
_assert(
|
||||||
|
"data.json" in probe.info_text and "参数数量:2" in probe.info_text and "组件入口:main.py" in probe.info_text,
|
||||||
|
"parameter export info panel summary should mention generated component",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ class _PropertySpecProbe(WindowStateMixin):
|
|||||||
self.manual_slot_pair_face_id = None
|
self.manual_slot_pair_face_id = None
|
||||||
|
|
||||||
|
|
||||||
|
class _PlaneOffsetDirectionModel:
|
||||||
|
faces = (object(),)
|
||||||
|
|
||||||
|
def face_info(self, face_id: int) -> dict[str, object]:
|
||||||
|
if face_id != 0:
|
||||||
|
raise ValueError(f"unexpected face id {face_id}")
|
||||||
|
return {
|
||||||
|
"plane_origin": (4.25, -5.0, 0.0),
|
||||||
|
"push_pull_outward_direction": (0.0, -1.0, 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _spec_keys(info: dict[str, object]) -> set[str]:
|
def _spec_keys(info: dict[str, object]) -> set[str]:
|
||||||
return {str(spec.get("key", "")) for spec in _specs(info)}
|
return {str(spec.get("key", "")) for spec in _specs(info)}
|
||||||
|
|
||||||
@@ -332,6 +344,36 @@ def _assert_target_change_detection() -> None:
|
|||||||
raise SystemExit("changed vector Face target was not detected")
|
raise SystemExit("changed vector Face target was not detected")
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_plane_offset_uses_push_pull_direction() -> None:
|
||||||
|
probe = _PropertySpecProbe()
|
||||||
|
probe.model = _PlaneOffsetDirectionModel()
|
||||||
|
probe.selected_face_id = 0
|
||||||
|
info = {
|
||||||
|
"face_id": 0,
|
||||||
|
"topological_face_id": 0,
|
||||||
|
"surface": "plane",
|
||||||
|
"plane_origin": (4.25, -5.0, 0.0),
|
||||||
|
"normal": (0.0, 1.0, 0.0),
|
||||||
|
"area_center": (4.25, -5.0, 2.5),
|
||||||
|
"bbox_diagonal": 8.0,
|
||||||
|
"local_face_deform_ready": True,
|
||||||
|
"local_face_width": 1.5,
|
||||||
|
"local_face_height": 4.0,
|
||||||
|
}
|
||||||
|
specs, _used = probe._editable_property_specs(info)
|
||||||
|
offset_spec = _spec(specs, "face_target_normal_position")
|
||||||
|
current = float(offset_spec.get("current_raw"))
|
||||||
|
if abs(current - 5.0) > 1e-9:
|
||||||
|
raise SystemExit(f"plane offset should use push-pull outward direction, got current={current:g}")
|
||||||
|
push_pull = _scoped_effective_spec(specs, "face_target_normal_position", "push_pull")
|
||||||
|
distance = probe._transform_property_scalar_target(push_pull, 5.0)
|
||||||
|
if abs(distance) > 1e-9:
|
||||||
|
raise SystemExit(f"unchanged plane offset target should convert to zero distance, got {distance:g}")
|
||||||
|
inward_distance = probe._transform_property_scalar_target(push_pull, -5.0)
|
||||||
|
if abs(inward_distance + 10.0) > 1e-9:
|
||||||
|
raise SystemExit(f"opposite plane offset target should convert relative to outward current, got {inward_distance:g}")
|
||||||
|
|
||||||
|
|
||||||
def _assert_property_table_column_widths() -> None:
|
def _assert_property_table_column_widths() -> None:
|
||||||
for width in (320, 340, 360, 400, 520):
|
for width in (320, 340, 360, 400, 520):
|
||||||
columns = _property_table_column_widths(width)
|
columns = _property_table_column_widths(width)
|
||||||
@@ -369,9 +411,16 @@ def _assert_holed_plane_local_scopes_disabled() -> None:
|
|||||||
"has_inner_boundaries": True,
|
"has_inner_boundaries": True,
|
||||||
"local_face_deform_ready": False,
|
"local_face_deform_ready": False,
|
||||||
"local_face_deform_blocker": "has inner boundary",
|
"local_face_deform_blocker": "has inner boundary",
|
||||||
|
"local_face_size_edit_ready": False,
|
||||||
|
"local_face_size_edit_blocker": "has inner boundary",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for key in ("local_face_width", "local_face_height", "face_center_position"):
|
keys = {str(spec.get("key", "")) for spec in specs}
|
||||||
|
for key in ("local_face_width", "local_face_height"):
|
||||||
|
if key in keys:
|
||||||
|
raise SystemExit(f"{key} should be hidden for a holed planar Face, got {keys}")
|
||||||
|
|
||||||
|
for key in ("face_center_position",):
|
||||||
local_mode = _scope_mode(specs, key, "local")
|
local_mode = _scope_mode(specs, key, "local")
|
||||||
if bool(local_mode.get("enabled", True)):
|
if bool(local_mode.get("enabled", True)):
|
||||||
raise SystemExit(f"{key} local Face scope should be disabled for a holed planar Face")
|
raise SystemExit(f"{key} local Face scope should be disabled for a holed planar Face")
|
||||||
@@ -438,6 +487,19 @@ def main() -> int:
|
|||||||
"first_level_topology_note": "已识别当前 Face 区域 1 个 Face、边界 Edge 4 条、边界 Vertex 4 个、共享边一级相邻 Face 4 个。",
|
"first_level_topology_note": "已识别当前 Face 区域 1 个 Face、边界 Edge 4 条、边界 Vertex 4 个、共享边一级相邻 Face 4 个。",
|
||||||
"topology_ignored_relation_note": "当前阶段只传播一级关系;二级、三级拓扑暂不自动递归编辑。",
|
"topology_ignored_relation_note": "当前阶段只传播一级关系;二级、三级拓扑暂不自动递归编辑。",
|
||||||
}
|
}
|
||||||
|
quick_plane_display_specs = _display_specs(plane_info)
|
||||||
|
quick_plane_display_keys = {str(spec.get("key", "")) for spec in quick_plane_display_specs}
|
||||||
|
if "local_face_width" in quick_plane_display_keys or "local_face_height" in quick_plane_display_keys:
|
||||||
|
raise SystemExit(
|
||||||
|
"quick plane Face should not show面内长度/面内宽度 until local size edit is explicitly ready: "
|
||||||
|
f"{sorted(quick_plane_display_keys)}"
|
||||||
|
)
|
||||||
|
plane_info.update(
|
||||||
|
{
|
||||||
|
"local_face_size_edit_ready": True,
|
||||||
|
"local_face_size_edit_blocker": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
plane_specs = _specs(plane_info)
|
plane_specs = _specs(plane_info)
|
||||||
plane_keys = {str(spec.get("key", "")) for spec in plane_specs}
|
plane_keys = {str(spec.get("key", "")) for spec in plane_specs}
|
||||||
_assert_label(plane_specs, "cad_modeling_form", "建模形式")
|
_assert_label(plane_specs, "cad_modeling_form", "建模形式")
|
||||||
@@ -1217,6 +1279,7 @@ def main() -> int:
|
|||||||
raise SystemExit(f"{key} disabled tip should explain the recognition blocker: {spec}")
|
raise SystemExit(f"{key} disabled tip should explain the recognition blocker: {spec}")
|
||||||
|
|
||||||
_assert_target_change_detection()
|
_assert_target_change_detection()
|
||||||
|
_assert_plane_offset_uses_push_pull_direction()
|
||||||
_assert_property_table_column_widths()
|
_assert_property_table_column_widths()
|
||||||
_assert_holed_plane_local_scopes_disabled()
|
_assert_holed_plane_local_scopes_disabled()
|
||||||
_assert_no_legacy_face_source_terms()
|
_assert_no_legacy_face_source_terms()
|
||||||
|
|||||||
+2
-2
@@ -1243,7 +1243,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.apply_property_button.setObjectName("parametricModelButton")
|
self.apply_property_button.setObjectName("parametricModelButton")
|
||||||
self.apply_property_button.setMinimumHeight(34)
|
self.apply_property_button.setMinimumHeight(34)
|
||||||
self.apply_property_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
self.apply_property_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
help_tip(self.apply_property_button, "应用当前被修改的一个参数;一次只执行一个几何修改,成功后可撤销。")
|
help_tip(self.apply_property_button, "应用当前被修改的参数;多个目标值会按表格顺序依次执行,失败时停止后续修改。")
|
||||||
self.apply_property_button.clicked.connect(self.apply_current_property_edit)
|
self.apply_property_button.clicked.connect(self.apply_current_property_edit)
|
||||||
self.quick_export_all_button = QPushButton("导出模型")
|
self.quick_export_all_button = QPushButton("导出模型")
|
||||||
self.quick_export_all_button.setObjectName("quickExportStepButton")
|
self.quick_export_all_button.setObjectName("quickExportStepButton")
|
||||||
@@ -1255,7 +1255,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.export_parameters_button.setObjectName("exportParametersButton")
|
self.export_parameters_button.setObjectName("exportParametersButton")
|
||||||
self.export_parameters_button.setMinimumHeight(34)
|
self.export_parameters_button.setMinimumHeight(34)
|
||||||
self.export_parameters_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
self.export_parameters_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
help_tip(self.export_parameters_button, "把已勾选为输入参数的尺寸行导出为 data.json。")
|
help_tip(self.export_parameters_button, "把已勾选的尺寸输入导出为 data.json,并生成可外部调用的参数化组件 main.py。")
|
||||||
self.export_parameters_button.clicked.connect(self.export_selected_parameters)
|
self.export_parameters_button.clicked.connect(self.export_selected_parameters)
|
||||||
property_action_row.addWidget(self.apply_property_button)
|
property_action_row.addWidget(self.apply_property_button)
|
||||||
property_action_row.addWidget(self.quick_export_all_button)
|
property_action_row.addWidget(self.quick_export_all_button)
|
||||||
|
|||||||
+21
-2
@@ -1138,6 +1138,12 @@ class FeatureMixin:
|
|||||||
"slot_blockers": blocker,
|
"slot_blockers": blocker,
|
||||||
}
|
}
|
||||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||||
|
feature_angular_span = (
|
||||||
|
_float_or_none(feature.get("same_domain_angular_span"))
|
||||||
|
or _float_or_none(feature.get("angular_span"))
|
||||||
|
or _float_or_none(info.get("same_domain_angular_span"))
|
||||||
|
or _float_or_none(info.get("angular_span"))
|
||||||
|
)
|
||||||
axis_range = self._cylindrical_axis_range(
|
axis_range = self._cylindrical_axis_range(
|
||||||
face_id,
|
face_id,
|
||||||
BRepAdaptor_Surface(self.faces[face_id]),
|
BRepAdaptor_Surface(self.faces[face_id]),
|
||||||
@@ -1146,6 +1152,10 @@ class FeatureMixin:
|
|||||||
scoped_info = dict(info)
|
scoped_info = dict(info)
|
||||||
scoped_info["height_estimate"] = axis_range["span"]
|
scoped_info["height_estimate"] = axis_range["span"]
|
||||||
scoped_info["v_range"] = (axis_range["v_min"], axis_range["v_max"])
|
scoped_info["v_range"] = (axis_range["v_min"], axis_range["v_max"])
|
||||||
|
if feature_angular_span is not None:
|
||||||
|
scoped_info["angular_span"] = feature_angular_span
|
||||||
|
scoped_info["same_domain_angular_span"] = feature_angular_span
|
||||||
|
scoped_info["is_full_cylinder"] = feature_angular_span >= math.tau * 0.92
|
||||||
scoped_info.update(self._cylinder_end_opening_info(face_id, BRepAdaptor_Surface(self.faces[face_id]), axis_range))
|
scoped_info.update(self._cylinder_end_opening_info(face_id, BRepAdaptor_Surface(self.faces[face_id]), axis_range))
|
||||||
readiness = _cylinder_resize_readiness(scoped_info, new_diameter)
|
readiness = _cylinder_resize_readiness(scoped_info, new_diameter)
|
||||||
readiness = self._apply_cylindrical_first_level_guard_to_readiness(
|
readiness = self._apply_cylindrical_first_level_guard_to_readiness(
|
||||||
@@ -1190,7 +1200,6 @@ class FeatureMixin:
|
|||||||
"feature_bottom_note": feature.get("feature_bottom_note"),
|
"feature_bottom_note": feature.get("feature_bottom_note"),
|
||||||
"feature_guess": info.get("feature_guess"),
|
"feature_guess": info.get("feature_guess"),
|
||||||
"confidence": info.get("confidence"),
|
"confidence": info.get("confidence"),
|
||||||
"angular_span": info.get("angular_span"),
|
|
||||||
"height_estimate": scoped_info.get("height_estimate"),
|
"height_estimate": scoped_info.get("height_estimate"),
|
||||||
"same_domain_face_ids": axis_range["same_domain_face_ids"],
|
"same_domain_face_ids": axis_range["same_domain_face_ids"],
|
||||||
"same_domain_face_count": axis_range["same_domain_face_count"],
|
"same_domain_face_count": axis_range["same_domain_face_count"],
|
||||||
@@ -1204,6 +1213,9 @@ class FeatureMixin:
|
|||||||
**topology_fields,
|
**topology_fields,
|
||||||
**cutter_plan,
|
**cutter_plan,
|
||||||
**fill_plan,
|
**fill_plan,
|
||||||
|
"angular_span": scoped_info.get("angular_span"),
|
||||||
|
"same_domain_angular_span": scoped_info.get("same_domain_angular_span"),
|
||||||
|
"is_full_cylinder": scoped_info.get("is_full_cylinder", feature.get("is_full_cylinder")),
|
||||||
}
|
}
|
||||||
|
|
||||||
def cylindrical_axis_move_plan(
|
def cylindrical_axis_move_plan(
|
||||||
@@ -2639,7 +2651,9 @@ class FeatureMixin:
|
|||||||
"radius": info.get("radius"),
|
"radius": info.get("radius"),
|
||||||
"axis": info.get("axis"),
|
"axis": info.get("axis"),
|
||||||
"axis_point": info.get("axis_point"),
|
"axis_point": info.get("axis_point"),
|
||||||
"angular_span": info.get("angular_span"),
|
"angular_span": scoped_info.get("angular_span"),
|
||||||
|
"same_domain_angular_span": scoped_info.get("same_domain_angular_span"),
|
||||||
|
"is_full_cylinder": scoped_info.get("is_full_cylinder", feature.get("is_full_cylinder")),
|
||||||
"height_estimate": scoped_info.get("height_estimate"),
|
"height_estimate": scoped_info.get("height_estimate"),
|
||||||
"feature_type": feature.get("feature_type"),
|
"feature_type": feature.get("feature_type"),
|
||||||
"feature_guess": info.get("feature_guess"),
|
"feature_guess": info.get("feature_guess"),
|
||||||
@@ -3331,6 +3345,7 @@ class FeatureMixin:
|
|||||||
candidates: list[tuple[float, tuple[float, float, float], dict[str, object]]] = []
|
candidates: list[tuple[float, tuple[float, float, float], dict[str, object]]] = []
|
||||||
diagonal = _shape_diagonal(self.shape)
|
diagonal = _shape_diagonal(self.shape)
|
||||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||||
|
plane_axis = surf.Plane().Axis().Direction()
|
||||||
for adjacent_id in adjacent_face_ids:
|
for adjacent_id in adjacent_face_ids:
|
||||||
if adjacent_id < 0 or adjacent_id >= len(self.faces):
|
if adjacent_id < 0 or adjacent_id >= len(self.faces):
|
||||||
continue
|
continue
|
||||||
@@ -3346,6 +3361,9 @@ class FeatureMixin:
|
|||||||
axis = cylinder.Axis()
|
axis = cylinder.Axis()
|
||||||
axis_point = axis.Location()
|
axis_point = axis.Location()
|
||||||
axis_dir = axis.Direction()
|
axis_dir = axis.Direction()
|
||||||
|
plane_axis_alignment = abs(_direction_dot(plane_axis, axis_dir))
|
||||||
|
if plane_axis_alignment < 0.92:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
axis_range = self._cylindrical_axis_range(adjacent_id, side_surf)
|
axis_range = self._cylindrical_axis_range(adjacent_id, side_surf)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -3377,6 +3395,7 @@ class FeatureMixin:
|
|||||||
{
|
{
|
||||||
"cap_axis_face_id": adjacent_id,
|
"cap_axis_face_id": adjacent_id,
|
||||||
"cap_axis_end": end_label,
|
"cap_axis_end": end_label,
|
||||||
|
"cap_plane_axis_alignment": plane_axis_alignment,
|
||||||
"cap_axis_parameter": cap_parameter,
|
"cap_axis_parameter": cap_parameter,
|
||||||
"cap_axis_start_parameter": v_min,
|
"cap_axis_start_parameter": v_min,
|
||||||
"cap_axis_end_parameter": v_max,
|
"cap_axis_end_parameter": v_max,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
|
|||||||
return model.push_pull_face_keep_relations(int(args[0]), float(args[1]))
|
return model.push_pull_face_keep_relations(int(args[0]), float(args[1]))
|
||||||
if operation == "move_face_plane_offset_local":
|
if operation == "move_face_plane_offset_local":
|
||||||
return model.move_face_plane_offset_local(int(args[0]), float(args[1]))
|
return model.move_face_plane_offset_local(int(args[0]), float(args[1]))
|
||||||
|
if operation == "translate_face_plane_offset_owning":
|
||||||
|
return model.translate_face_plane_offset_owning(int(args[0]), float(args[1]))
|
||||||
if operation == "resize_face_area_local":
|
if operation == "resize_face_area_local":
|
||||||
return model.resize_face_area_local(int(args[0]), float(args[1]))
|
return model.resize_face_area_local(int(args[0]), float(args[1]))
|
||||||
if operation == "resize_face_area":
|
if operation == "resize_face_area":
|
||||||
@@ -62,6 +64,8 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
|
|||||||
return model.resize_cylindrical_height(int(args[0]), float(args[1]))
|
return model.resize_cylindrical_height(int(args[0]), float(args[1]))
|
||||||
if operation == "resize_cylindrical_boss_height":
|
if operation == "resize_cylindrical_boss_height":
|
||||||
return model.resize_cylindrical_boss_height(int(args[0]), float(args[1]))
|
return model.resize_cylindrical_boss_height(int(args[0]), float(args[1]))
|
||||||
|
if operation == "resize_cylindrical_boss":
|
||||||
|
return model.resize_cylindrical_boss(int(args[0]), float(args[1]))
|
||||||
if operation == "resize_cylindrical_height_owning_scale":
|
if operation == "resize_cylindrical_height_owning_scale":
|
||||||
return model.resize_cylindrical_height_owning_scale(int(args[0]), float(args[1]))
|
return model.resize_cylindrical_height_owning_scale(int(args[0]), float(args[1]))
|
||||||
if operation == "resize_cone_reference_radius":
|
if operation == "resize_cone_reference_radius":
|
||||||
@@ -78,6 +82,8 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
|
|||||||
return model.resize_cylindrical_owning_scale(int(args[0]), float(args[1]))
|
return model.resize_cylindrical_owning_scale(int(args[0]), float(args[1]))
|
||||||
if operation == "move_cylindrical_hole_axis":
|
if operation == "move_cylindrical_hole_axis":
|
||||||
return model.move_cylindrical_hole_axis(int(args[0]), _point3(args[1], operation))
|
return model.move_cylindrical_hole_axis(int(args[0]), _point3(args[1], operation))
|
||||||
|
if operation == "move_cylindrical_boss_axis":
|
||||||
|
return model.move_cylindrical_boss_axis(int(args[0]), _point3(args[1], operation))
|
||||||
if operation == "suppress_cylindrical_hole":
|
if operation == "suppress_cylindrical_hole":
|
||||||
return model.suppress_cylindrical_hole(int(args[0]))
|
return model.suppress_cylindrical_hole(int(args[0]))
|
||||||
if operation == "resize_cylindrical_depth":
|
if operation == "resize_cylindrical_depth":
|
||||||
|
|||||||
+187
-11
@@ -127,6 +127,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
self._edge_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
self._edge_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||||
self._edge_first_level_fact_cache: dict[int, dict[str, object]] = {}
|
self._edge_first_level_fact_cache: dict[int, dict[str, object]] = {}
|
||||||
self._local_face_deform_readiness_cache: dict[int, dict[str, object]] = {}
|
self._local_face_deform_readiness_cache: dict[int, dict[str, object]] = {}
|
||||||
|
self._part_solid_count_cache: dict[int, int] = {}
|
||||||
self._open_shell_context_cache: dict[int, dict[str, object]] = {}
|
self._open_shell_context_cache: dict[int, dict[str, object]] = {}
|
||||||
self._edge_duplicate_key_ids_cache: dict[tuple[object, ...], list[int]] | None = None
|
self._edge_duplicate_key_ids_cache: dict[tuple[object, ...], list[int]] | None = None
|
||||||
self._same_domain_internal_edge_ids_cache: set[int] | None = None
|
self._same_domain_internal_edge_ids_cache: set[int] | None = None
|
||||||
@@ -233,6 +234,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
self._edge_first_level_topology_cache.clear()
|
self._edge_first_level_topology_cache.clear()
|
||||||
self._edge_first_level_fact_cache.clear()
|
self._edge_first_level_fact_cache.clear()
|
||||||
self._local_face_deform_readiness_cache.clear()
|
self._local_face_deform_readiness_cache.clear()
|
||||||
|
self._part_solid_count_cache.clear()
|
||||||
self._open_shell_context_cache.clear()
|
self._open_shell_context_cache.clear()
|
||||||
self._edge_duplicate_key_ids_cache = None
|
self._edge_duplicate_key_ids_cache = None
|
||||||
self._same_domain_internal_edge_ids_cache = None
|
self._same_domain_internal_edge_ids_cache = None
|
||||||
@@ -247,6 +249,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
solid_id = 0
|
solid_id = 0
|
||||||
for part in self.display_parts():
|
for part in self.display_parts():
|
||||||
part_solids = _explore(part.shape, TopAbs_SOLID)
|
part_solids = _explore(part.shape, TopAbs_SOLID)
|
||||||
|
self._part_solid_count_cache[part.id] = len(part_solids)
|
||||||
part_solid_edge_maps: list[tuple[int, TopTools_IndexedDataMapOfShapeListOfShape]] = []
|
part_solid_edge_maps: list[tuple[int, TopTools_IndexedDataMapOfShapeListOfShape]] = []
|
||||||
part_solid_entries: list[tuple[int, TopoDS_Shape]] = []
|
part_solid_entries: list[tuple[int, TopoDS_Shape]] = []
|
||||||
if part_solids:
|
if part_solids:
|
||||||
@@ -406,15 +409,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
info["feature_source_face_id"] = face_id
|
info["feature_source_face_id"] = face_id
|
||||||
info["feature_highlight_face_ids"] = (face_id,)
|
info["feature_highlight_face_ids"] = (face_id,)
|
||||||
info["feature_edit_actions"] = "拉伸/切除平面"
|
info["feature_edit_actions"] = "拉伸/切除平面"
|
||||||
if bool(info.get("has_inner_boundaries")):
|
|
||||||
info["local_face_deform_ready"] = False
|
|
||||||
info["local_face_deform_face_count"] = 1
|
|
||||||
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用拉伸/切除、孔或槽的专门修改入口。"
|
|
||||||
else:
|
|
||||||
info["local_face_deform_ready"] = True
|
|
||||||
info["local_face_deform_face_count"] = 1
|
|
||||||
info["local_face_deform_blocker"] = ""
|
|
||||||
info["local_face_deform_status"] = "deferred"
|
|
||||||
info.update(
|
info.update(
|
||||||
self._local_face_plane_size_info(
|
self._local_face_plane_size_info(
|
||||||
face_id,
|
face_id,
|
||||||
@@ -423,6 +417,15 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
center=_tuple_or_none(info.get("area_center")),
|
center=_tuple_or_none(info.get("area_center")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if bool(info.get("has_inner_boundaries")):
|
||||||
|
info["local_face_deform_ready"] = False
|
||||||
|
info["local_face_deform_face_count"] = 1
|
||||||
|
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用拉伸/切除、孔或槽的专门修改入口。"
|
||||||
|
else:
|
||||||
|
info.update(self._local_face_deform_readiness(face_id))
|
||||||
|
if bool(info.get("local_face_deform_ready")):
|
||||||
|
info["local_face_deform_status"] = "ready"
|
||||||
|
info.update(self._local_face_size_parameter_readiness(face_id, info))
|
||||||
elif surface_type == GeomAbs_Cylinder:
|
elif surface_type == GeomAbs_Cylinder:
|
||||||
cyl = surf.Cylinder()
|
cyl = surf.Cylinder()
|
||||||
axis = cyl.Axis()
|
axis = cyl.Axis()
|
||||||
@@ -604,6 +607,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
result: dict[str, object] = {
|
result: dict[str, object] = {
|
||||||
"same_domain_face_ids": tuple(side_face_ids),
|
"same_domain_face_ids": tuple(side_face_ids),
|
||||||
"same_domain_face_count": len(side_face_ids),
|
"same_domain_face_count": len(side_face_ids),
|
||||||
|
"feature_highlight_face_ids": tuple(side_face_ids),
|
||||||
"angular_span": combined_span,
|
"angular_span": combined_span,
|
||||||
"same_domain_angular_span": combined_span,
|
"same_domain_angular_span": combined_span,
|
||||||
"same_domain_note": same_domain_note,
|
"same_domain_note": same_domain_note,
|
||||||
@@ -748,6 +752,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
center=_tuple_or_none(info.get("area_center")),
|
center=_tuple_or_none(info.get("area_center")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
info.update(self._local_face_size_parameter_readiness(face_id, info))
|
||||||
try:
|
try:
|
||||||
info.update(self.face_first_level_topology(face_id))
|
info.update(self.face_first_level_topology(face_id))
|
||||||
info.update(self.face_first_level_facts(face_id, scope="face"))
|
info.update(self.face_first_level_facts(face_id, scope="face"))
|
||||||
@@ -2069,19 +2074,51 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
"local_face_width_direction": direction_groups[0]["direction"],
|
"local_face_width_direction": direction_groups[0]["direction"],
|
||||||
"local_face_height_direction": direction_groups[1]["direction"],
|
"local_face_height_direction": direction_groups[1]["direction"],
|
||||||
}
|
}
|
||||||
|
result.update(self._local_face_size_parameter_readiness(face_id, {**info, **result}))
|
||||||
|
prismatic_rebuild_blockers: list[str] = []
|
||||||
if extrusion_candidate:
|
if extrusion_candidate:
|
||||||
extrusion = _float_or_none(shell_info.get("shell_thickness_estimate"))
|
extrusion = _float_or_none(shell_info.get("shell_thickness_estimate"))
|
||||||
extrusion_confidence = "high" if len(connected_side_ids) == 4 else "medium"
|
extrusion_confidence = "high" if len(connected_side_ids) == 4 else "medium"
|
||||||
|
reference_face_id = opposite_face_id
|
||||||
|
part_id = self.face_part_ids[face_id] if 0 <= face_id < len(self.face_part_ids) else -1
|
||||||
|
part_solid_count = self._part_solid_count_cache.get(part_id, 0)
|
||||||
|
if feature_semantics not in {"additive-boss", "subtractive-pocket"}:
|
||||||
|
prismatic_rebuild_blockers.append("当前矩形平面还没有稳定判断为凸台或口袋。")
|
||||||
|
if len(connected_side_ids) != 4:
|
||||||
|
prismatic_rebuild_blockers.append("当前矩形特征需要四个侧壁都能识别后才开放长宽局部重建。")
|
||||||
|
if reference_face_id is None:
|
||||||
|
prismatic_rebuild_blockers.append("当前矩形特征缺少稳定参考面。")
|
||||||
|
if extrusion is None or extrusion <= 1e-9:
|
||||||
|
prismatic_rebuild_blockers.append("当前矩形特征缺少稳定高度/深度。")
|
||||||
|
if part_solid_count != 1:
|
||||||
|
prismatic_rebuild_blockers.append("矩形凸台/口袋长宽局部重建当前只对单 Solid 零件开放。")
|
||||||
result.update(
|
result.update(
|
||||||
{
|
{
|
||||||
"prismatic_extrusion_status": "candidate",
|
"prismatic_extrusion_status": "candidate",
|
||||||
"prismatic_extrusion_estimate": extrusion if extrusion is not None else "",
|
"prismatic_extrusion_estimate": extrusion if extrusion is not None else "",
|
||||||
"prismatic_reference_face_id": opposite_face_id,
|
"prismatic_reference_face_id": reference_face_id,
|
||||||
"prismatic_extrusion_confidence": extrusion_confidence,
|
"prismatic_extrusion_confidence": extrusion_confidence,
|
||||||
"confidence": extrusion_confidence,
|
"confidence": extrusion_confidence,
|
||||||
"prismatic_reference_source": "side-wall-topology" if topology_reference else "overlapping-plane",
|
"prismatic_reference_source": "side-wall-topology" if topology_reference else "overlapping-plane",
|
||||||
"prismatic_highlight_face_ids": tuple(sorted({face_id, *reference_face_ids, *connected_side_ids})),
|
"prismatic_highlight_face_ids": tuple(sorted({face_id, *reference_face_ids, *connected_side_ids})),
|
||||||
"prismatic_extrusion_note": "相对平面通过至少两个侧壁与当前矩形面相连。",
|
"prismatic_extrusion_note": "相对平面通过至少两个侧壁与当前矩形面相连。",
|
||||||
|
"part_solid_count": part_solid_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prismatic_rebuild_blockers.append("当前矩形平面还没有识别出稳定高度/深度,不能按完整凸台/口袋局部重建。")
|
||||||
|
if prismatic_rebuild_blockers:
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"rectangular_prismatic_size_rebuild_ready": False,
|
||||||
|
"rectangular_prismatic_size_rebuild_blocker": ";".join(prismatic_rebuild_blockers),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"rectangular_prismatic_size_rebuild_ready": True,
|
||||||
|
"rectangular_prismatic_size_rebuild_blocker": "",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
@@ -2588,6 +2625,22 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
**fillet_info,
|
**fillet_info,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if angular_span >= math.tau * 0.92:
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"slot_kind": "",
|
||||||
|
"slot_status": "",
|
||||||
|
"slot_risk": "",
|
||||||
|
"slot_blockers": "",
|
||||||
|
"slot_angular_span": "",
|
||||||
|
"slot_open_angle": "",
|
||||||
|
"slot_chord_width_estimate": "",
|
||||||
|
"slot_arc_length_estimate": "",
|
||||||
|
"slot_sagitta_depth_estimate": "",
|
||||||
|
"feature_slot_face_ids": (),
|
||||||
|
"feature_slot_boundary_face_ids": (),
|
||||||
|
}
|
||||||
|
)
|
||||||
scoped_readiness_info = dict(result)
|
scoped_readiness_info = dict(result)
|
||||||
scoped_readiness_info["angular_span"] = combined_angular_span
|
scoped_readiness_info["angular_span"] = combined_angular_span
|
||||||
scoped_readiness_info["height_estimate"] = axis_range["span"]
|
scoped_readiness_info["height_estimate"] = axis_range["span"]
|
||||||
@@ -3685,7 +3738,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
adjacent_face_ids=adjacent_face_ids,
|
adjacent_face_ids=adjacent_face_ids,
|
||||||
relation_scope="cylindrical-feature",
|
relation_scope="cylindrical-feature",
|
||||||
)
|
)
|
||||||
angular_span = feature.get("slot_angular_span", feature.get("angular_span", info.get("angular_span")))
|
angular_span = (
|
||||||
|
_float_or_none(feature.get("slot_angular_span"))
|
||||||
|
or _float_or_none(feature.get("same_domain_angular_span"))
|
||||||
|
or _float_or_none(feature.get("angular_span"))
|
||||||
|
or _float_or_none(info.get("same_domain_angular_span"))
|
||||||
|
or _float_or_none(info.get("angular_span"))
|
||||||
|
)
|
||||||
topology = {
|
topology = {
|
||||||
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
|
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
|
||||||
"topology_relation_depth": 1,
|
"topology_relation_depth": 1,
|
||||||
@@ -3913,6 +3972,123 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
"has_inner_boundaries": inner_boundary_wires > 0,
|
"has_inner_boundaries": inner_boundary_wires > 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _local_face_size_parameter_readiness(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||||
|
blockers: list[str] = []
|
||||||
|
if face_id < 0 or face_id >= len(self.faces):
|
||||||
|
blockers.append("Face ID 不存在。")
|
||||||
|
if str(info.get("surface") or "") != "plane":
|
||||||
|
blockers.append("面内长度/宽度只对平面矩形 Face 开放。")
|
||||||
|
if bool(info.get("has_inner_boundaries")):
|
||||||
|
blockers.append("当前 Face 有内孔/内边界,请优先使用孔或槽的专门修改入口。")
|
||||||
|
try:
|
||||||
|
boundary_wires = int(info.get("boundary_wires", 0) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
boundary_wires = 0
|
||||||
|
if boundary_wires and boundary_wires != 1:
|
||||||
|
blockers.append("当前 Face 不是单一外轮廓。")
|
||||||
|
|
||||||
|
width = _float_or_none(info.get("local_face_width"))
|
||||||
|
height = _float_or_none(info.get("local_face_height"))
|
||||||
|
width_direction = _tuple_or_none(info.get("local_face_width_direction"))
|
||||||
|
height_direction = _tuple_or_none(info.get("local_face_height_direction"))
|
||||||
|
center = _tuple_or_none(info.get("local_face_size_center")) or _tuple_or_none(info.get("area_center"))
|
||||||
|
if width is None or width <= 1e-9:
|
||||||
|
blockers.append("当前 Face 缺少稳定的面内长度。")
|
||||||
|
if height is None or height <= 1e-9:
|
||||||
|
blockers.append("当前 Face 缺少稳定的面内宽度。")
|
||||||
|
if width_direction is None or height_direction is None or center is None:
|
||||||
|
blockers.append("当前 Face 缺少稳定的面内方向或中心。")
|
||||||
|
|
||||||
|
edge_ids: list[int] = []
|
||||||
|
if 0 <= face_id < len(self.faces):
|
||||||
|
try:
|
||||||
|
edge_ids = self._face_boundary_edge_ids(face_id)
|
||||||
|
except Exception:
|
||||||
|
edge_ids = []
|
||||||
|
try:
|
||||||
|
boundary_edges = int(info.get("boundary_edges", len(edge_ids)) or len(edge_ids))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
boundary_edges = len(edge_ids)
|
||||||
|
rectangular_groups: list[dict[str, object]] = []
|
||||||
|
if boundary_edges != 4 or len(edge_ids) != 4:
|
||||||
|
blockers.append("面内长度/宽度当前只对四边矩形平面开放。")
|
||||||
|
else:
|
||||||
|
direction_groups: list[dict[str, object]] = []
|
||||||
|
for edge_id in edge_ids:
|
||||||
|
try:
|
||||||
|
curve = BRepAdaptor_Curve(self.edges[edge_id])
|
||||||
|
if curve.GetType() != GeomAbs_Line:
|
||||||
|
blockers.append("当前 Face 的边界包含非直线边。")
|
||||||
|
break
|
||||||
|
start = _point_tuple(curve.Value(curve.FirstParameter()))
|
||||||
|
end = _point_tuple(curve.Value(curve.LastParameter()))
|
||||||
|
vector = _tuple_sub(end, start)
|
||||||
|
length = math.sqrt(_tuple_dot(vector, vector))
|
||||||
|
direction = _tuple_normalized(vector)
|
||||||
|
except Exception:
|
||||||
|
blockers.append("当前 Face 的边界直线不能稳定读取。")
|
||||||
|
break
|
||||||
|
if direction is None or length <= 1e-9:
|
||||||
|
blockers.append("当前 Face 的边界存在退化直线边。")
|
||||||
|
break
|
||||||
|
matched_group = None
|
||||||
|
for group in direction_groups:
|
||||||
|
group_direction = _tuple_or_none(group.get("direction"))
|
||||||
|
if group_direction is not None and abs(_tuple_dot(direction, group_direction)) >= 0.999:
|
||||||
|
matched_group = group
|
||||||
|
break
|
||||||
|
if matched_group is None:
|
||||||
|
matched_group = {"direction": direction, "lengths": []}
|
||||||
|
direction_groups.append(matched_group)
|
||||||
|
matched_group["lengths"].append(length)
|
||||||
|
if len(direction_groups) != 2 or any(len(group["lengths"]) != 2 for group in direction_groups):
|
||||||
|
blockers.append("当前 Face 的四条边没有形成两组稳定平行对边。")
|
||||||
|
else:
|
||||||
|
first_direction = _tuple_or_none(direction_groups[0].get("direction"))
|
||||||
|
second_direction = _tuple_or_none(direction_groups[1].get("direction"))
|
||||||
|
if (
|
||||||
|
first_direction is None
|
||||||
|
or second_direction is None
|
||||||
|
or abs(_tuple_dot(first_direction, second_direction)) > 0.01
|
||||||
|
):
|
||||||
|
blockers.append("当前 Face 的两组对边不是稳定垂直关系。")
|
||||||
|
for group in direction_groups:
|
||||||
|
lengths = [float(item) for item in group["lengths"]]
|
||||||
|
average = sum(lengths) / max(len(lengths), 1)
|
||||||
|
if max(abs(item - average) for item in lengths) > max(average * 1e-4, 1e-7):
|
||||||
|
blockers.append("当前 Face 的相对边长度不一致。")
|
||||||
|
break
|
||||||
|
rectangular_groups = direction_groups
|
||||||
|
|
||||||
|
if blockers:
|
||||||
|
return {
|
||||||
|
"local_face_size_edit_ready": False,
|
||||||
|
"local_face_size_edit_blocker": ";".join(dict.fromkeys(blockers)),
|
||||||
|
}
|
||||||
|
result: dict[str, object] = {
|
||||||
|
"local_face_size_edit_ready": True,
|
||||||
|
"local_face_size_edit_blocker": "",
|
||||||
|
}
|
||||||
|
if rectangular_groups:
|
||||||
|
rectangular_groups.sort(
|
||||||
|
key=lambda group: sum(float(item) for item in group["lengths"]) / max(len(group["lengths"]), 1),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
length_group = rectangular_groups[0]
|
||||||
|
width_group = rectangular_groups[1]
|
||||||
|
length_values = [float(item) for item in length_group["lengths"]]
|
||||||
|
width_values = [float(item) for item in width_group["lengths"]]
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"local_face_width": sum(length_values) / len(length_values),
|
||||||
|
"local_face_height": sum(width_values) / len(width_values),
|
||||||
|
"local_face_width_direction": length_group["direction"],
|
||||||
|
"local_face_height_direction": width_group["direction"],
|
||||||
|
"local_face_size_center": center,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def _local_face_deform_readiness(self, face_id: int) -> dict[str, object]:
|
def _local_face_deform_readiness(self, face_id: int) -> dict[str, object]:
|
||||||
if face_id < 0 or face_id >= len(self.faces):
|
if face_id < 0 or face_id >= len(self.faces):
|
||||||
return {
|
return {
|
||||||
|
|||||||
+60
-20
@@ -3269,9 +3269,13 @@ class OperationMixin:
|
|||||||
outward_direction = _tuple_normalized(_tuple_or_none(info.get("push_pull_outward_direction")))
|
outward_direction = _tuple_normalized(_tuple_or_none(info.get("push_pull_outward_direction")))
|
||||||
current_plane_position = None
|
current_plane_position = None
|
||||||
target_plane_position = None
|
target_plane_position = None
|
||||||
|
current_plane_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
|
||||||
|
target_plane_center = None
|
||||||
if plane_origin is not None and outward_direction is not None:
|
if plane_origin is not None and outward_direction is not None:
|
||||||
current_plane_position = _tuple_dot(plane_origin, outward_direction)
|
current_plane_position = _tuple_dot(plane_origin, outward_direction)
|
||||||
target_plane_position = current_plane_position + float(distance)
|
target_plane_position = current_plane_position + float(distance)
|
||||||
|
if current_plane_center is not None:
|
||||||
|
target_plane_center = _tuple_add(current_plane_center, _tuple_scale(outward_direction, float(distance)))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": status,
|
"status": status,
|
||||||
@@ -3294,6 +3298,8 @@ class OperationMixin:
|
|||||||
"plane_direction": outward_direction,
|
"plane_direction": outward_direction,
|
||||||
"current_plane_position": current_plane_position,
|
"current_plane_position": current_plane_position,
|
||||||
"target_plane_position": target_plane_position,
|
"target_plane_position": target_plane_position,
|
||||||
|
"current_plane_center": current_plane_center,
|
||||||
|
"target_plane_center": target_plane_center,
|
||||||
"push_pull_inward_material_depth": inward_material_depth,
|
"push_pull_inward_material_depth": inward_material_depth,
|
||||||
"push_pull_inward_cut_ratio": inward_cut_ratio,
|
"push_pull_inward_cut_ratio": inward_cut_ratio,
|
||||||
"cylindrical_cap_extension_old_height": (
|
"cylindrical_cap_extension_old_height": (
|
||||||
@@ -8746,6 +8752,7 @@ class OperationMixin:
|
|||||||
metric = ""
|
metric = ""
|
||||||
target: float | tuple[float, ...] | None = None
|
target: float | tuple[float, ...] | None = None
|
||||||
tolerance = 1e-4
|
tolerance = 1e-4
|
||||||
|
center_tolerance: float | None = None
|
||||||
getter: Callable[[int], float | tuple[float, ...] | None] | None = None
|
getter: Callable[[int], float | tuple[float, ...] | None] | None = None
|
||||||
|
|
||||||
target_area = _float_or_none(plan.get("target_area"))
|
target_area = _float_or_none(plan.get("target_area"))
|
||||||
@@ -8794,6 +8801,8 @@ class OperationMixin:
|
|||||||
target = target_position
|
target = target_position
|
||||||
bbox_diagonal = _float_or_none(plan.get("bbox_diagonal")) or _shape_diagonal(self.shape)
|
bbox_diagonal = _float_or_none(plan.get("bbox_diagonal")) or _shape_diagonal(self.shape)
|
||||||
tolerance = max(bbox_diagonal * 1e-4, abs(target_position) * 1e-5, 1e-4)
|
tolerance = max(bbox_diagonal * 1e-4, abs(target_position) * 1e-5, 1e-4)
|
||||||
|
target_plane_center = _tuple_or_none(plan.get("target_plane_center"))
|
||||||
|
center_tolerance = max(bbox_diagonal * 0.15, tolerance * 20.0, 1e-3) if target_plane_center is not None else None
|
||||||
|
|
||||||
def plane_position_getter(face_id: int) -> float | None:
|
def plane_position_getter(face_id: int) -> float | None:
|
||||||
if face_id < 0 or face_id >= len(self.faces):
|
if face_id < 0 or face_id >= len(self.faces):
|
||||||
@@ -8890,6 +8899,46 @@ class OperationMixin:
|
|||||||
plan_face_id = _int_or_none(plan.get("face_id"))
|
plan_face_id = _int_or_none(plan.get("face_id"))
|
||||||
if plan_face_id is not None:
|
if plan_face_id is not None:
|
||||||
preferred_ids.append(plan_face_id)
|
preferred_ids.append(plan_face_id)
|
||||||
|
def candidate_center_error(face_id: int) -> float | None:
|
||||||
|
if metric != "plane_position":
|
||||||
|
return None
|
||||||
|
target_center = _tuple_or_none(plan.get("target_plane_center"))
|
||||||
|
if target_center is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
info = self.quick_face_info(face_id)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
actual_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
|
||||||
|
if actual_center is None:
|
||||||
|
return None
|
||||||
|
return _vector_length(_tuple_sub(actual_center, target_center))
|
||||||
|
|
||||||
|
def candidate_record(face_id: int, actual: object, error: float) -> dict[str, object]:
|
||||||
|
record: dict[str, object] = {
|
||||||
|
"face_id": face_id,
|
||||||
|
"metric": metric,
|
||||||
|
"actual": actual,
|
||||||
|
"target": target,
|
||||||
|
"error": error,
|
||||||
|
"tolerance": tolerance,
|
||||||
|
"scope": scope,
|
||||||
|
}
|
||||||
|
center_error = candidate_center_error(face_id)
|
||||||
|
if center_error is not None:
|
||||||
|
record["center_error"] = center_error
|
||||||
|
if center_tolerance is not None:
|
||||||
|
record["center_tolerance"] = center_tolerance
|
||||||
|
return record
|
||||||
|
|
||||||
|
def candidate_sort_key(record: dict[str, object]) -> tuple[int, float, float]:
|
||||||
|
center_error = _float_or_none(record.get("center_error"))
|
||||||
|
center_limit = _float_or_none(record.get("center_tolerance"))
|
||||||
|
center_penalty = 0
|
||||||
|
if center_error is not None and center_limit is not None and center_error > center_limit:
|
||||||
|
center_penalty = 1
|
||||||
|
return (center_penalty, float(record["error"]), center_error if center_error is not None else 0.0)
|
||||||
|
|
||||||
if preferred_ids:
|
if preferred_ids:
|
||||||
preferred_best: dict[str, object] | None = None
|
preferred_best: dict[str, object] | None = None
|
||||||
for face_id in preferred_ids:
|
for face_id in preferred_ids:
|
||||||
@@ -8902,16 +8951,9 @@ class OperationMixin:
|
|||||||
if actual is None:
|
if actual is None:
|
||||||
continue
|
continue
|
||||||
error = _result_value_error(actual, target)
|
error = _result_value_error(actual, target)
|
||||||
if preferred_best is None or error < float(preferred_best["error"]):
|
record = candidate_record(face_id, actual, error)
|
||||||
preferred_best = {
|
if preferred_best is None or candidate_sort_key(record) < candidate_sort_key(preferred_best):
|
||||||
"face_id": face_id,
|
preferred_best = record
|
||||||
"metric": metric,
|
|
||||||
"actual": actual,
|
|
||||||
"target": target,
|
|
||||||
"error": error,
|
|
||||||
"tolerance": tolerance,
|
|
||||||
"scope": scope,
|
|
||||||
}
|
|
||||||
if preferred_best is not None and float(preferred_best["error"]) <= tolerance:
|
if preferred_best is not None and float(preferred_best["error"]) <= tolerance:
|
||||||
return preferred_best
|
return preferred_best
|
||||||
|
|
||||||
@@ -8924,16 +8966,9 @@ class OperationMixin:
|
|||||||
if actual is None:
|
if actual is None:
|
||||||
continue
|
continue
|
||||||
error = _result_value_error(actual, target)
|
error = _result_value_error(actual, target)
|
||||||
if best is None or error < float(best["error"]):
|
record = candidate_record(face_id, actual, error)
|
||||||
best = {
|
if best is None or candidate_sort_key(record) < candidate_sort_key(best):
|
||||||
"face_id": face_id,
|
best = record
|
||||||
"metric": metric,
|
|
||||||
"actual": actual,
|
|
||||||
"target": target,
|
|
||||||
"error": error,
|
|
||||||
"tolerance": tolerance,
|
|
||||||
"scope": scope,
|
|
||||||
}
|
|
||||||
if best is not None:
|
if best is not None:
|
||||||
return best
|
return best
|
||||||
return {
|
return {
|
||||||
@@ -9011,6 +9046,11 @@ class OperationMixin:
|
|||||||
all_ids = filtered(range(len(self.faces)))
|
all_ids = filtered(range(len(self.faces)))
|
||||||
if not all_ids and solid_id is not None and solid_id >= 0:
|
if not all_ids and solid_id is not None and solid_id >= 0:
|
||||||
all_ids = filtered(range(len(self.faces)), require_solid=False)
|
all_ids = filtered(range(len(self.faces)), require_solid=False)
|
||||||
|
elif target_kind == "solid" and solid_id is not None and solid_id >= 0 and surface == "plane":
|
||||||
|
relaxed_ids = filtered(range(len(self.faces)), require_solid=False)
|
||||||
|
for face_id in relaxed_ids:
|
||||||
|
if face_id not in all_ids:
|
||||||
|
all_ids.append(face_id)
|
||||||
combined: list[int] = []
|
combined: list[int] = []
|
||||||
for face_id in [*primary_ids, *all_ids]:
|
for face_id in [*primary_ids, *all_ids]:
|
||||||
if face_id not in combined:
|
if face_id not in combined:
|
||||||
|
|||||||
@@ -0,0 +1,570 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from .isolated_edit_worker import _execute
|
||||||
|
from .model import StepModel
|
||||||
|
|
||||||
|
|
||||||
|
COMPONENT_SCHEMA = "step-editor-parametric-component-v1"
|
||||||
|
|
||||||
|
_COMPONENT_SEQUENCE_RE = re.compile(r"^(?P<index>\d{3,})_(?P<name>.+)$")
|
||||||
|
_ACTION_OPERATION_MAP = {
|
||||||
|
"push_pull_face": "push_pull_face",
|
||||||
|
"push_pull_face_keep_relations": "push_pull_face_keep_relations",
|
||||||
|
"move_selected_face_plane_position_local": "move_face_plane_offset_local",
|
||||||
|
"move_selected_face_plane_position_by_translation": "translate_face_plane_offset_owning",
|
||||||
|
"resize_face_width_local": "resize_face_size_local",
|
||||||
|
"resize_face_height_local": "resize_face_size_local",
|
||||||
|
"resize_face_width_keep_relations": "resize_face_size_local_keep_relations",
|
||||||
|
"resize_face_height_keep_relations": "resize_face_size_local_keep_relations",
|
||||||
|
"resize_face_width_owning_scale": "resize_face_size_owning_scale",
|
||||||
|
"resize_face_height_owning_scale": "resize_face_size_owning_scale",
|
||||||
|
"resize_shell_thickness": "resize_shell_thickness",
|
||||||
|
"resize_shell_thickness_owning_scale": "resize_shell_thickness_owning_scale",
|
||||||
|
"resize_hole": "resize_cylindrical_hole",
|
||||||
|
"resize_cylindrical_owning_scale": "resize_cylindrical_owning_scale",
|
||||||
|
"resize_hole_depth": "resize_cylindrical_depth",
|
||||||
|
"resize_hole_depth_owning_scale": "resize_cylindrical_depth_owning_scale",
|
||||||
|
"resize_slot_width": "resize_cylindrical_slot_width",
|
||||||
|
"resize_slot_depth": "resize_cylindrical_slot_depth",
|
||||||
|
"resize_slot_arc_length": "resize_cylindrical_slot_arc_length",
|
||||||
|
"resize_slot_angular_span": "resize_cylindrical_slot_angular_span",
|
||||||
|
"resize_slot_total_length": "resize_cylindrical_slot_total_length",
|
||||||
|
"resize_slot_center_distance": "resize_cylindrical_slot_center_distance",
|
||||||
|
"move_cylindrical_hole_axis": "move_cylindrical_hole_axis",
|
||||||
|
"move_cylindrical_slot_axis": "move_cylindrical_slot_axis",
|
||||||
|
"suppress_hole": "suppress_cylindrical_hole",
|
||||||
|
"resize_boss": "resize_cylindrical_boss",
|
||||||
|
"resize_boss_height": "resize_cylindrical_boss_height",
|
||||||
|
"resize_cylinder_height": "resize_cylindrical_height",
|
||||||
|
"resize_cylindrical_height_owning_scale": "resize_cylindrical_height_owning_scale",
|
||||||
|
"move_cylindrical_boss_axis": "move_cylindrical_boss_axis",
|
||||||
|
"resize_cone_reference_radius": "resize_cone_reference_radius",
|
||||||
|
"resize_cone_semi_angle": "resize_cone_semi_angle",
|
||||||
|
"resize_sphere_radius": "resize_sphere_radius",
|
||||||
|
"resize_torus_major_radius": "resize_torus_radius",
|
||||||
|
"resize_torus_minor_radius": "resize_torus_radius",
|
||||||
|
"resize_any_edge_length": "resize_general_edge_length",
|
||||||
|
"move_edge_start_point": "move_edge_endpoint",
|
||||||
|
"move_edge_end_point": "move_edge_endpoint",
|
||||||
|
"move_edge_center_point": "move_edge_center",
|
||||||
|
"move_circular_edge_axis_center": "move_circular_edge_axis_center",
|
||||||
|
"resize_ellipse_edge_major_radius": "resize_ellipse_edge_axis_radius",
|
||||||
|
"resize_ellipse_edge_minor_radius": "resize_ellipse_edge_axis_radius",
|
||||||
|
"resize_existing_fillet": "resize_existing_fillet",
|
||||||
|
"resize_existing_chamfer": "resize_existing_chamfer",
|
||||||
|
"fillet_edge": "fillet_edge",
|
||||||
|
"chamfer_edge": "chamfer_edge",
|
||||||
|
"chamfer_edge_asymmetric": "chamfer_edge_asymmetric",
|
||||||
|
"chamfer_edge_distance_angle": "chamfer_edge_distance_angle",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_component_name(value: object, fallback: str = "STEP_Parametric") -> str:
|
||||||
|
text = str(value or "").strip() or fallback
|
||||||
|
text = re.sub(r'[<>:"/\\|?*\x00-\x1f]+', "_", text)
|
||||||
|
text = re.sub(r"\s+", "_", text).strip(" ._")
|
||||||
|
return text or fallback
|
||||||
|
|
||||||
|
|
||||||
|
def default_component_root(project_root: Path | None = None) -> Path:
|
||||||
|
root = project_root or Path(__file__).resolve().parent.parent
|
||||||
|
return root / "nodes"
|
||||||
|
|
||||||
|
|
||||||
|
def next_component_dir(root: Path, component_name: object) -> Path:
|
||||||
|
base_name = sanitize_component_name(component_name)
|
||||||
|
max_index = -1
|
||||||
|
if root.is_dir():
|
||||||
|
for child in root.iterdir():
|
||||||
|
if not child.is_dir():
|
||||||
|
continue
|
||||||
|
match = _COMPONENT_SEQUENCE_RE.match(child.name)
|
||||||
|
if match:
|
||||||
|
max_index = max(max_index, int(match.group("index")))
|
||||||
|
return root / f"{max_index + 1:03d}_{base_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def component_name_from_step(step_path: object) -> str:
|
||||||
|
try:
|
||||||
|
stem = Path(str(step_path)).stem
|
||||||
|
except Exception:
|
||||||
|
stem = ""
|
||||||
|
return sanitize_component_name(f"{stem}_STEP参数化组件" if stem else "STEP参数化组件")
|
||||||
|
|
||||||
|
|
||||||
|
def numeric_text(value: object) -> str:
|
||||||
|
text = str(value if value is not None else "").strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def json_script_literal(value: object) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def _float_or_text(value: object) -> object:
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return value
|
||||||
|
text = str(value if value is not None else "").strip()
|
||||||
|
try:
|
||||||
|
return float(text)
|
||||||
|
except ValueError:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _as_list3(value: object) -> list[float] | None:
|
||||||
|
if isinstance(value, str):
|
||||||
|
chunks = [chunk.strip() for chunk in value.strip().strip("()[]").replace(";", ",").split(",") if chunk.strip()]
|
||||||
|
elif isinstance(value, (tuple, list)):
|
||||||
|
chunks = list(value)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
if len(chunks) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return [float(chunks[0]), float(chunks[1]), float(chunks[2])]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _target_object_id(spec: dict[str, object], selected_kind: str | None, selected_face_id: int | None, selected_edge_id: int | None) -> int | None:
|
||||||
|
if spec.get("source_face_id") not in {"", None}:
|
||||||
|
return int(spec["source_face_id"])
|
||||||
|
action = str(spec.get("action") or "")
|
||||||
|
kind = str(selected_kind or "")
|
||||||
|
if "edge" in action and selected_edge_id is not None and kind == "edge":
|
||||||
|
return int(selected_edge_id)
|
||||||
|
if selected_face_id is not None:
|
||||||
|
return int(selected_face_id)
|
||||||
|
if selected_edge_id is not None:
|
||||||
|
return int(selected_edge_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _target_kind_for_action(action: str, selected_kind: str | None) -> str:
|
||||||
|
if "edge" in action:
|
||||||
|
return "edge"
|
||||||
|
if selected_kind in {"face", "feature", "edge"}:
|
||||||
|
return str(selected_kind)
|
||||||
|
return "face"
|
||||||
|
|
||||||
|
|
||||||
|
def operation_for_action(action: object) -> str | None:
|
||||||
|
return _ACTION_OPERATION_MAP.get(str(action or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _axis_arg_for_face_size(action: str) -> str | None:
|
||||||
|
if "face_width" in action:
|
||||||
|
return "width"
|
||||||
|
if "face_height" in action:
|
||||||
|
return "height"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _mode_arg_for_torus(action: str) -> str | None:
|
||||||
|
if action == "resize_torus_major_radius":
|
||||||
|
return "major"
|
||||||
|
if action == "resize_torus_minor_radius":
|
||||||
|
return "minor"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _endpoint_arg_for_edge(action: str) -> str | None:
|
||||||
|
if action == "move_edge_start_point":
|
||||||
|
return "start"
|
||||||
|
if action == "move_edge_end_point":
|
||||||
|
return "end"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _ellipse_axis_arg(action: str) -> str | None:
|
||||||
|
if action == "resize_ellipse_edge_major_radius":
|
||||||
|
return "major"
|
||||||
|
if action == "resize_ellipse_edge_minor_radius":
|
||||||
|
return "minor"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def component_edit_config_from_spec(
|
||||||
|
*,
|
||||||
|
parameter_row: dict[str, str],
|
||||||
|
spec: dict[str, object],
|
||||||
|
selected_kind: str | None,
|
||||||
|
selected_face_id: int | None,
|
||||||
|
selected_edge_id: int | None,
|
||||||
|
step_path: Path | None,
|
||||||
|
) -> dict[str, object] | None:
|
||||||
|
action = str(spec.get("action") or "")
|
||||||
|
operation = operation_for_action(action)
|
||||||
|
target_id = _target_object_id(spec, selected_kind, selected_face_id, selected_edge_id)
|
||||||
|
if not operation or target_id is None:
|
||||||
|
return None
|
||||||
|
value_type = str(spec.get("value_type", "number"))
|
||||||
|
default_value = parameter_row.get("default", "")
|
||||||
|
target_value: object
|
||||||
|
if value_type == "vector3":
|
||||||
|
target_value = _as_list3(default_value) or _as_list3(spec.get("current_raw")) or default_value
|
||||||
|
elif value_type in {"number", "positive", "integer", "integer_or_empty"}:
|
||||||
|
target_value = _float_or_text(default_value)
|
||||||
|
else:
|
||||||
|
target_value = default_value
|
||||||
|
args: list[object] = [int(target_id)]
|
||||||
|
target_arg: object = {"param": parameter_row["name"]}
|
||||||
|
transform = str(spec.get("target_transform") or "")
|
||||||
|
if transform:
|
||||||
|
target_arg = {
|
||||||
|
"param": parameter_row["name"],
|
||||||
|
"transform": transform,
|
||||||
|
"context": spec.get("transform_context", {}),
|
||||||
|
}
|
||||||
|
if action == "suppress_hole":
|
||||||
|
target_value = ""
|
||||||
|
else:
|
||||||
|
args.append(target_arg)
|
||||||
|
axis_arg = _axis_arg_for_face_size(action)
|
||||||
|
if axis_arg is not None:
|
||||||
|
args.append(axis_arg)
|
||||||
|
elif action in {"resize_torus_major_radius", "resize_torus_minor_radius"}:
|
||||||
|
args.append(_mode_arg_for_torus(action))
|
||||||
|
elif action in {"move_edge_start_point", "move_edge_end_point"}:
|
||||||
|
args.insert(1, _endpoint_arg_for_edge(action))
|
||||||
|
elif action in {"resize_ellipse_edge_major_radius", "resize_ellipse_edge_minor_radius"}:
|
||||||
|
args.append(_ellipse_axis_arg(action))
|
||||||
|
elif action in {
|
||||||
|
"resize_hole_depth",
|
||||||
|
"resize_hole_depth_owning_scale",
|
||||||
|
"resize_slot_width",
|
||||||
|
"resize_slot_depth",
|
||||||
|
"resize_slot_arc_length",
|
||||||
|
"resize_slot_total_length",
|
||||||
|
"resize_slot_center_distance",
|
||||||
|
}:
|
||||||
|
manual_id = spec.get("manual_bottom_face_id")
|
||||||
|
if manual_id in {"", None}:
|
||||||
|
manual_id = spec.get("slot_pair_manual_face_id")
|
||||||
|
args.append("" if manual_id in {"", None} else manual_id)
|
||||||
|
return {
|
||||||
|
"parameter": parameter_row["name"],
|
||||||
|
"displayName": parameter_row.get("displayName", parameter_row["name"]),
|
||||||
|
"targetKind": _target_kind_for_action(action, selected_kind),
|
||||||
|
"targetId": int(target_id),
|
||||||
|
"uiAction": action,
|
||||||
|
"operation": operation,
|
||||||
|
"args": args,
|
||||||
|
"default": target_value,
|
||||||
|
"valueType": value_type,
|
||||||
|
"scope": spec.get("scope_key", spec.get("scope_default", "")),
|
||||||
|
"scopeLabel": spec.get("scope_label", spec.get("scope_text", "")),
|
||||||
|
"sourceStep": str(step_path or ""),
|
||||||
|
"parameterKey": spec.get("key", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_component_main_py(component: dict[str, object]) -> str:
|
||||||
|
project_root = str(Path(__file__).resolve().parent.parent)
|
||||||
|
component_name = sanitize_component_name(component.get("componentName") or component_name_from_step(component.get("sourceStep")))
|
||||||
|
output_parameter = {
|
||||||
|
"name": "output_step",
|
||||||
|
"displayName": "输出STEP",
|
||||||
|
"type": "file",
|
||||||
|
"ioRole": "output",
|
||||||
|
"default": "",
|
||||||
|
}
|
||||||
|
return f'''# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
STEP 参数化组件。
|
||||||
|
|
||||||
|
这个文件按 FlowEditor 节点脚本方式生成:
|
||||||
|
1. INPUT_PARAMETERS 是从软件“导出参数”勾选行直接嵌入的输入参数。
|
||||||
|
2. PARAMETERS 会额外加上输出 STEP 文件参数,供节点设计器生成输出端口。
|
||||||
|
3. execute(inputs, params, context) 是 FlowEditor 调用入口。
|
||||||
|
4. main() 只用于本地命令行调试。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PROJECT_ROOT = {json.dumps(project_root, ensure_ascii=False)}
|
||||||
|
if PROJECT_ROOT and PROJECT_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, PROJECT_ROOT)
|
||||||
|
|
||||||
|
from step_editor.parametric_component import run_embedded_component
|
||||||
|
|
||||||
|
INPUT_PARAMETERS = {json_script_literal(component.get("parameters", []))}
|
||||||
|
|
||||||
|
OUTPUT_PARAMETERS = [
|
||||||
|
{json_script_literal(output_parameter)}
|
||||||
|
]
|
||||||
|
|
||||||
|
PARAMETERS = INPUT_PARAMETERS + OUTPUT_PARAMETERS
|
||||||
|
|
||||||
|
COMPONENT = {json_script_literal(component)}
|
||||||
|
|
||||||
|
NODE_INFO = {{
|
||||||
|
"typeName": {json.dumps(component_name, ensure_ascii=False)},
|
||||||
|
"displayName": {json.dumps(component_name, ensure_ascii=False)},
|
||||||
|
"category": "几何参数化",
|
||||||
|
"icon": "icon.svg",
|
||||||
|
"parameters": PARAMETERS,
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
def _value_from_inputs(name, inputs, params, default=""):
|
||||||
|
value = inputs.get(name) if isinstance(inputs, dict) else None
|
||||||
|
if value in (None, "") and isinstance(params, dict):
|
||||||
|
value = params.get(name)
|
||||||
|
if value in (None, ""):
|
||||||
|
value = default
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _component_input_values(inputs, params):
|
||||||
|
values = {{}}
|
||||||
|
for item in INPUT_PARAMETERS:
|
||||||
|
name = item.get("name")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
values[name] = _value_from_inputs(name, inputs, params, item.get("default", ""))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _default_output_step(work_dir, output_dir):
|
||||||
|
output_root = output_dir or os.path.join(work_dir, "output")
|
||||||
|
os.makedirs(output_root, exist_ok=True)
|
||||||
|
return os.path.join(output_root, COMPONENT.get("outputName") or "modified.step")
|
||||||
|
|
||||||
|
|
||||||
|
def run(inputs=None, output_step=None, work_dir=None):
|
||||||
|
return run_embedded_component(COMPONENT, inputs=inputs, output_step=output_step, work_dir=work_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(inputs, params, context):
|
||||||
|
"""
|
||||||
|
FlowEditor 调用入口。
|
||||||
|
|
||||||
|
inputs:上游节点传入值,优先级高于 params。
|
||||||
|
params:节点属性面板参数。
|
||||||
|
context:FlowEditor 上下文,常见字段包括 work_dir / input_dir / output_dir。
|
||||||
|
"""
|
||||||
|
inputs = inputs or {{}}
|
||||||
|
params = params or {{}}
|
||||||
|
context = context or {{}}
|
||||||
|
work_dir = context.get("work_dir") or os.getcwd()
|
||||||
|
output_dir = context.get("output_dir") or os.path.join(work_dir, "output")
|
||||||
|
output_step = _default_output_step(work_dir, output_dir)
|
||||||
|
result = run(
|
||||||
|
inputs=_component_input_values(inputs, params),
|
||||||
|
output_step=output_step,
|
||||||
|
work_dir=work_dir,
|
||||||
|
)
|
||||||
|
if not result.get("ok"):
|
||||||
|
raise RuntimeError(result.get("error") or json.dumps(result, ensure_ascii=False))
|
||||||
|
return {{
|
||||||
|
"output_step": result.get("outputStep", output_step),
|
||||||
|
"outputStep": result.get("outputStep", output_step),
|
||||||
|
"sourceStep": result.get("sourceStep", ""),
|
||||||
|
"messages": result.get("messages", []),
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(description="Run generated STEP parametric component.")
|
||||||
|
parser.add_argument("--inputs", default="", help="JSON file or JSON object with input parameter values.")
|
||||||
|
parser.add_argument("--output-step", default="", help="Output STEP file path.")
|
||||||
|
parser.add_argument("--work-dir", default="", help="Runtime output directory.")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
inputs = args.inputs
|
||||||
|
if inputs:
|
||||||
|
candidate = Path(inputs)
|
||||||
|
if candidate.is_file():
|
||||||
|
inputs = json.loads(candidate.read_text(encoding="utf-8-sig"))
|
||||||
|
else:
|
||||||
|
inputs = json.loads(inputs)
|
||||||
|
else:
|
||||||
|
inputs = {{}}
|
||||||
|
result = run(inputs=inputs, output_step=args.output_step or None, work_dir=args.work_dir or None)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
return 0 if result.get("ok") else 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def export_parametric_component(
|
||||||
|
*,
|
||||||
|
parameters: list[dict[str, str]],
|
||||||
|
edits: list[dict[str, object]],
|
||||||
|
source_step: Path | None,
|
||||||
|
component_root: Path | None = None,
|
||||||
|
component_name: str | None = None,
|
||||||
|
) -> Path:
|
||||||
|
if not parameters:
|
||||||
|
raise ValueError("No input parameters selected.")
|
||||||
|
root = component_root or default_component_root()
|
||||||
|
target_dir = next_component_dir(root, component_name or component_name_from_step(source_step))
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
component = {
|
||||||
|
"schema": COMPONENT_SCHEMA,
|
||||||
|
"createdAt": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"componentName": component_name or component_name_from_step(source_step),
|
||||||
|
"sourceStep": str(source_step or ""),
|
||||||
|
"parameters": parameters,
|
||||||
|
"edits": edits,
|
||||||
|
"outputName": "modified.step",
|
||||||
|
}
|
||||||
|
target = target_dir / "main.py"
|
||||||
|
target.write_text(render_component_main_py(component), encoding="utf-8")
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def _input_values(inputs: object) -> dict[str, object]:
|
||||||
|
if not isinstance(inputs, dict):
|
||||||
|
return {}
|
||||||
|
if isinstance(inputs.get("inputs"), dict):
|
||||||
|
return dict(inputs["inputs"])
|
||||||
|
rows = inputs.get("parameters")
|
||||||
|
if isinstance(rows, list):
|
||||||
|
result: dict[str, object] = {}
|
||||||
|
for row in rows:
|
||||||
|
if isinstance(row, dict) and row.get("name"):
|
||||||
|
result[str(row["name"])] = row.get("value", row.get("default", ""))
|
||||||
|
return result
|
||||||
|
return dict(inputs)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_vector3(value: object) -> list[float]:
|
||||||
|
vector = _as_list3(value)
|
||||||
|
if vector is None:
|
||||||
|
raise ValueError(f"Expected 3D vector value, got {value!r}.")
|
||||||
|
return vector
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_arg_transform(value: object, transform: str, context: object) -> object:
|
||||||
|
context = context if isinstance(context, dict) else {}
|
||||||
|
if transform == "plane_target_position_to_offset":
|
||||||
|
current = float(context.get("current_plane_position"))
|
||||||
|
return float(value) - current
|
||||||
|
if transform == "radius_to_diameter":
|
||||||
|
return float(value) * 2.0
|
||||||
|
if transform == "diameter_to_radius":
|
||||||
|
return float(value) * 0.5
|
||||||
|
if transform == "degrees_to_radians":
|
||||||
|
import math
|
||||||
|
|
||||||
|
return math.radians(float(value))
|
||||||
|
if transform == "slot_open_angle_degrees_to_angular_span":
|
||||||
|
import math
|
||||||
|
|
||||||
|
return math.tau - math.radians(float(value))
|
||||||
|
if transform == "target_center_to_translation":
|
||||||
|
current = _parse_vector3(context.get("current_center"))
|
||||||
|
target = _parse_vector3(value)
|
||||||
|
return [target[index] - current[index] for index in range(3)]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_arg(value: object, values: dict[str, object], defaults: dict[str, object]) -> object:
|
||||||
|
if isinstance(value, dict) and "param" in value:
|
||||||
|
name = str(value.get("param") or "")
|
||||||
|
resolved = values.get(name, defaults.get(name, ""))
|
||||||
|
transform = str(value.get("transform") or "")
|
||||||
|
if transform:
|
||||||
|
return _apply_arg_transform(resolved, transform, value.get("context"))
|
||||||
|
return resolved
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def run_embedded_component(
|
||||||
|
component: dict[str, object],
|
||||||
|
*,
|
||||||
|
inputs: object | None = None,
|
||||||
|
output_step: str | Path | None = None,
|
||||||
|
work_dir: str | Path | None = None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
started = datetime.now().isoformat(timespec="seconds")
|
||||||
|
try:
|
||||||
|
source_step = Path(str(component.get("sourceStep") or "")).expanduser()
|
||||||
|
if not source_step.is_file():
|
||||||
|
return {"ok": False, "error": f"Source STEP does not exist: {source_step}", "startedAt": started}
|
||||||
|
output_path = Path(output_step) if output_step else None
|
||||||
|
if output_path is None:
|
||||||
|
output_root = Path(work_dir) if work_dir else Path.cwd()
|
||||||
|
output_path = output_root / str(component.get("outputName") or "modified.step")
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
parameters = [row for row in component.get("parameters", []) if isinstance(row, dict)]
|
||||||
|
defaults = {str(row.get("name") or ""): row.get("default", "") for row in parameters if row.get("name")}
|
||||||
|
values = _input_values(inputs)
|
||||||
|
model = StepModel.load(source_step)
|
||||||
|
messages: list[str] = []
|
||||||
|
for edit in component.get("edits", []):
|
||||||
|
if not isinstance(edit, dict):
|
||||||
|
continue
|
||||||
|
operation = str(edit.get("operation") or "")
|
||||||
|
args = [_resolve_arg(arg, values, defaults) for arg in list(edit.get("args") or [])]
|
||||||
|
messages.append(_execute(model, operation, args))
|
||||||
|
model.export_all(output_path)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"startedAt": started,
|
||||||
|
"finishedAt": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"sourceStep": str(source_step),
|
||||||
|
"outputStep": str(output_path),
|
||||||
|
"messages": messages,
|
||||||
|
"parameters": parameters,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"startedAt": started,
|
||||||
|
"finishedAt": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"error": str(exc),
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_inputs(path_or_json: str) -> object:
|
||||||
|
if not path_or_json:
|
||||||
|
return {}
|
||||||
|
candidate = Path(path_or_json)
|
||||||
|
if candidate.is_file():
|
||||||
|
return json.loads(candidate.read_text(encoding="utf-8-sig"))
|
||||||
|
return json.loads(path_or_json)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Run a STEP parametric component JSON.")
|
||||||
|
parser.add_argument("component", help="Component JSON file.")
|
||||||
|
parser.add_argument("--inputs", default="", help="JSON file or inline JSON object.")
|
||||||
|
parser.add_argument("--output-step", default="", help="Output STEP path.")
|
||||||
|
parser.add_argument("--work-dir", default="", help="Runtime work directory.")
|
||||||
|
parsed = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
component = json.loads(Path(parsed.component).read_text(encoding="utf-8-sig"))
|
||||||
|
result = run_embedded_component(
|
||||||
|
component,
|
||||||
|
inputs=_load_inputs(parsed.inputs),
|
||||||
|
output_step=parsed.output_step or None,
|
||||||
|
work_dir=parsed.work_dir or None,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
result = {"ok": False, "error": str(exc), "traceback": traceback.format_exc()}
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
return 0 if result.get("ok") else 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+27
-1
@@ -69,6 +69,32 @@ def _polydata_id_key(values: Iterable[int] | None) -> tuple[int, ...] | None:
|
|||||||
return tuple(sorted({int(value) for value in values}))
|
return tuple(sorted({int(value) for value in values}))
|
||||||
|
|
||||||
|
|
||||||
|
def _display_edge_samples(edge: TopoDS_Shape, deflection: float) -> list[tuple[float, float, float]]:
|
||||||
|
try:
|
||||||
|
curve = BRepAdaptor_Curve(edge)
|
||||||
|
curve_type = curve.GetType()
|
||||||
|
first = float(curve.FirstParameter())
|
||||||
|
last = float(curve.LastParameter())
|
||||||
|
except Exception:
|
||||||
|
return list(discretize_edge(edge, deflection))
|
||||||
|
|
||||||
|
if curve_type in {GeomAbs_Circle, GeomAbs_Ellipse} and math.isfinite(first) and math.isfinite(last):
|
||||||
|
span = abs(last - first)
|
||||||
|
if span > 1e-9:
|
||||||
|
min_segments = 24 if span >= math.tau * 0.75 else 8
|
||||||
|
segments = max(min_segments, int(math.ceil(span / math.radians(7.5))))
|
||||||
|
segments = min(max(segments, 2), 128)
|
||||||
|
samples: list[tuple[float, float, float]] = []
|
||||||
|
for index in range(segments + 1):
|
||||||
|
parameter = first + (last - first) * (index / segments)
|
||||||
|
point = curve.Value(parameter)
|
||||||
|
samples.append((float(point.X()), float(point.Y()), float(point.Z())))
|
||||||
|
if len(samples) >= 2:
|
||||||
|
return samples
|
||||||
|
|
||||||
|
return list(discretize_edge(edge, deflection))
|
||||||
|
|
||||||
|
|
||||||
class PolydataMixin:
|
class PolydataMixin:
|
||||||
def build_face_polydata(
|
def build_face_polydata(
|
||||||
self,
|
self,
|
||||||
@@ -272,7 +298,7 @@ class PolydataMixin:
|
|||||||
continue
|
continue
|
||||||
if edge_id in hidden_edge_ids:
|
if edge_id in hidden_edge_ids:
|
||||||
continue
|
continue
|
||||||
samples = discretize_edge(edge, deflection)
|
samples = _display_edge_samples(edge, deflection)
|
||||||
if len(samples) < 2:
|
if len(samples) < 2:
|
||||||
continue
|
continue
|
||||||
polyline = vtk.vtkPolyLine()
|
polyline = vtk.vtkPolyLine()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from .geometry_utils import (
|
|||||||
_tuple_sub,
|
_tuple_sub,
|
||||||
_vector_length,
|
_vector_length,
|
||||||
)
|
)
|
||||||
|
from .parametric_component import component_name_from_step, default_component_root, export_parametric_component
|
||||||
from .ui_helpers import * # noqa: F403
|
from .ui_helpers import * # noqa: F403
|
||||||
from .workers import EditWorker, LoadWorker, ScanWorker
|
from .workers import EditWorker, LoadWorker, ScanWorker
|
||||||
|
|
||||||
@@ -122,6 +123,22 @@ def _edit_timing_summary(timings: object, *, limit: int = 5) -> str:
|
|||||||
return ",".join(parts)
|
return ",".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _start_background_thread(thread: QThread, priority: QThread.Priority = QThread.Priority.LowPriority) -> None:
|
||||||
|
try:
|
||||||
|
thread.start(priority)
|
||||||
|
except TypeError:
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _isolated_process_creation_flags() -> int:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return 0
|
||||||
|
flags = 0
|
||||||
|
for name in ("CREATE_NO_WINDOW", "BELOW_NORMAL_PRIORITY_CLASS"):
|
||||||
|
flags |= int(getattr(subprocess, name, 0))
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
class WindowActionMixin:
|
class WindowActionMixin:
|
||||||
def _empty_edge_polydata(self):
|
def _empty_edge_polydata(self):
|
||||||
polydata = vtk.vtkPolyData()
|
polydata = vtk.vtkPolyData()
|
||||||
@@ -151,11 +168,49 @@ class WindowActionMixin:
|
|||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
QMessageBox.warning(self, "导出参数失败", f"无法写入 {output_path.name}:{exc}")
|
QMessageBox.warning(self, "导出参数失败", f"无法写入 {output_path.name}:{exc}")
|
||||||
return
|
return
|
||||||
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数到 {output_path.name}")
|
component_path: Path | None = None
|
||||||
|
component_warning = ""
|
||||||
|
try:
|
||||||
|
component_path = self._export_parametric_component_main(rows)
|
||||||
|
except Exception as exc:
|
||||||
|
component_warning = str(exc)
|
||||||
|
if component_path is not None:
|
||||||
|
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数,并生成组件 {component_path.parent.name}")
|
||||||
|
else:
|
||||||
|
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数到 {output_path.name}")
|
||||||
if hasattr(self, "set_plain_info"):
|
if hasattr(self, "set_plain_info"):
|
||||||
names = "、".join(str(row.get("displayName", "")) for row in rows[:8] if row.get("displayName"))
|
names = "、".join(str(row.get("displayName", "")) for row in rows[:8] if row.get("displayName"))
|
||||||
suffix = "……" if len(rows) > 8 else ""
|
suffix = "……" if len(rows) > 8 else ""
|
||||||
self.set_plain_info(f"已导出参数文件:{output_path}\n参数数量:{len(rows)}\n参数:{names}{suffix}")
|
lines = [
|
||||||
|
f"已导出参数文件:{output_path}",
|
||||||
|
f"参数数量:{len(rows)}",
|
||||||
|
f"参数:{names}{suffix}",
|
||||||
|
]
|
||||||
|
if component_path is not None:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"已生成参数化组件:{component_path.parent}",
|
||||||
|
f"组件入口:{component_path.name}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif component_warning:
|
||||||
|
lines.append(f"组件生成未完成:{component_warning}")
|
||||||
|
self.set_plain_info("\n".join(lines))
|
||||||
|
|
||||||
|
def _export_parametric_component_main(self, rows: list[dict[str, str]]) -> Path:
|
||||||
|
edits = self._selected_parameter_component_edits(rows) if hasattr(self, "_selected_parameter_component_edits") else []
|
||||||
|
if not edits:
|
||||||
|
raise ValueError("当前勾选参数还不能映射为可执行 STEP 编辑操作。")
|
||||||
|
source_step = self.step_path if isinstance(getattr(self, "step_path", None), Path) else None
|
||||||
|
if source_step is None or not source_step.is_file():
|
||||||
|
raise ValueError("当前模型缺少可复用的 STEP 文件路径,请先导入 STEP 模型。")
|
||||||
|
return export_parametric_component(
|
||||||
|
parameters=rows,
|
||||||
|
edits=edits,
|
||||||
|
source_step=source_step,
|
||||||
|
component_root=default_component_root(Path(__file__).resolve().parent.parent),
|
||||||
|
component_name=component_name_from_step(source_step),
|
||||||
|
)
|
||||||
|
|
||||||
def export_all(self) -> None:
|
def export_all(self) -> None:
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
@@ -488,7 +543,7 @@ class WindowActionMixin:
|
|||||||
def action():
|
def action():
|
||||||
if keep_relations:
|
if keep_relations:
|
||||||
return self.model.push_pull_face_keep_relations(face_id, distance)
|
return self.model.push_pull_face_keep_relations(face_id, distance)
|
||||||
return self.model.push_pull_face(face_id, distance)
|
return self.model.push_pull_face(face_id, distance, plan=dict(plan))
|
||||||
|
|
||||||
self._run_edit_action(
|
self._run_edit_action(
|
||||||
action,
|
action,
|
||||||
@@ -501,8 +556,11 @@ class WindowActionMixin:
|
|||||||
"distance_rule": "positive=outward fuse, negative=inward cut",
|
"distance_rule": "positive=outward fuse, negative=inward cut",
|
||||||
"surface": plan.get("surface"),
|
"surface": plan.get("surface"),
|
||||||
"outward_direction": plan.get("outward_direction"),
|
"outward_direction": plan.get("outward_direction"),
|
||||||
|
"plane_direction": plan.get("plane_direction"),
|
||||||
"current_plane_position": plan.get("current_plane_position"),
|
"current_plane_position": plan.get("current_plane_position"),
|
||||||
"target_plane_position": plan.get("target_plane_position"),
|
"target_plane_position": plan.get("target_plane_position"),
|
||||||
|
"current_plane_center": plan.get("current_plane_center"),
|
||||||
|
"target_plane_center": plan.get("target_plane_center"),
|
||||||
"direction_confidence": plan.get("direction_confidence"),
|
"direction_confidence": plan.get("direction_confidence"),
|
||||||
"direction_note": plan.get("direction_note"),
|
"direction_note": plan.get("direction_note"),
|
||||||
"resize_strategy": plan.get("resize_strategy"),
|
"resize_strategy": plan.get("resize_strategy"),
|
||||||
@@ -1413,7 +1471,11 @@ class WindowActionMixin:
|
|||||||
height_estimate = _float_or_none(info.get("hole_depth_estimate"))
|
height_estimate = _float_or_none(info.get("hole_depth_estimate"))
|
||||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||||
confidence = str(info.get("confidence", "low"))
|
confidence = str(info.get("confidence", "low"))
|
||||||
angular_span = _float_or_none(info.get("angular_span"))
|
angular_span = _float_or_none(info.get("same_domain_angular_span"))
|
||||||
|
if angular_span is None:
|
||||||
|
angular_span = _float_or_none(info.get("angular_span"))
|
||||||
|
if bool(info.get("is_full_cylinder")):
|
||||||
|
angular_span = math.tau
|
||||||
|
|
||||||
if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9:
|
if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9:
|
||||||
blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。")
|
blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。")
|
||||||
@@ -1588,6 +1650,7 @@ class WindowActionMixin:
|
|||||||
"中心(局部重建)",
|
"中心(局部重建)",
|
||||||
"中心(保持关系)",
|
"中心(保持关系)",
|
||||||
"圆柱高度调整",
|
"圆柱高度调整",
|
||||||
|
"圆柱孔径调整",
|
||||||
"高度(缩放特征)缩放所属对象",
|
"高度(缩放特征)缩放所属对象",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1603,6 +1666,7 @@ class WindowActionMixin:
|
|||||||
isolated_geometry_operations = {
|
isolated_geometry_operations = {
|
||||||
"push_pull_face",
|
"push_pull_face",
|
||||||
"push_pull_face_keep_relations",
|
"push_pull_face_keep_relations",
|
||||||
|
"translate_face_plane_offset_owning",
|
||||||
"move_face_plane_offset_local",
|
"move_face_plane_offset_local",
|
||||||
"resize_face_area_local",
|
"resize_face_area_local",
|
||||||
"resize_face_area",
|
"resize_face_area",
|
||||||
@@ -1615,6 +1679,8 @@ class WindowActionMixin:
|
|||||||
"resize_shell_thickness_owning_scale",
|
"resize_shell_thickness_owning_scale",
|
||||||
"resize_cylindrical_height",
|
"resize_cylindrical_height",
|
||||||
"resize_cylindrical_boss_height",
|
"resize_cylindrical_boss_height",
|
||||||
|
"resize_cylindrical_boss",
|
||||||
|
"move_cylindrical_boss_axis",
|
||||||
"resize_cylindrical_height_owning_scale",
|
"resize_cylindrical_height_owning_scale",
|
||||||
"resize_cone_reference_radius",
|
"resize_cone_reference_radius",
|
||||||
"resize_cone_semi_angle",
|
"resize_cone_semi_angle",
|
||||||
@@ -1832,6 +1898,8 @@ class WindowActionMixin:
|
|||||||
"does not have",
|
"does not have",
|
||||||
)
|
)
|
||||||
lower = text.lower()
|
lower = text.lower()
|
||||||
|
if "target check failed" in lower or "没有到达目标" in text:
|
||||||
|
return "结果未达到目标", "几何内核生成了结果,但目标位置或目标尺寸校验没有通过,模型已回滚。"
|
||||||
if any(marker.lower() in lower for marker in unsupported_markers):
|
if any(marker.lower() in lower for marker in unsupported_markers):
|
||||||
return "暂未实现", "当前版本暂未实现这类稳定修改。"
|
return "暂未实现", "当前版本暂未实现这类稳定修改。"
|
||||||
if any(marker.lower() in lower for marker in risk_markers):
|
if any(marker.lower() in lower for marker in risk_markers):
|
||||||
@@ -7391,7 +7459,7 @@ class WindowActionMixin:
|
|||||||
thread.finished.connect(self._forget_scan_thread)
|
thread.finished.connect(self._forget_scan_thread)
|
||||||
self.scan_thread = thread
|
self.scan_thread = thread
|
||||||
self.scan_worker = worker
|
self.scan_worker = worker
|
||||||
thread.start()
|
_start_background_thread(thread)
|
||||||
|
|
||||||
def _run_scan_task_sync(self, action) -> None:
|
def _run_scan_task_sync(self, action) -> None:
|
||||||
if not self.scan_in_progress:
|
if not self.scan_in_progress:
|
||||||
@@ -7745,7 +7813,7 @@ class WindowActionMixin:
|
|||||||
thread.finished.connect(self._forget_edit_thread)
|
thread.finished.connect(self._forget_edit_thread)
|
||||||
self.edit_thread = thread
|
self.edit_thread = thread
|
||||||
self.edit_worker = worker
|
self.edit_worker = worker
|
||||||
thread.start()
|
_start_background_thread(thread)
|
||||||
|
|
||||||
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
|
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
|
||||||
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
|
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
|
||||||
@@ -7932,6 +8000,7 @@ class WindowActionMixin:
|
|||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
errors="replace",
|
errors="replace",
|
||||||
|
creationflags=_isolated_process_creation_flags(),
|
||||||
)
|
)
|
||||||
self.active_isolated_edit_process = process
|
self.active_isolated_edit_process = process
|
||||||
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
||||||
@@ -8107,16 +8176,29 @@ class WindowActionMixin:
|
|||||||
or _unit_triple_or_none(parameters.get("outward_direction"))
|
or _unit_triple_or_none(parameters.get("outward_direction"))
|
||||||
)
|
)
|
||||||
if target_position is not None and plane_direction is not None:
|
if target_position is not None and plane_direction is not None:
|
||||||
|
part_id = self._edit_integrity_int_or_none(parameters.get("part_id"))
|
||||||
|
solid_id = self._edit_integrity_int_or_none(parameters.get("solid_id"))
|
||||||
candidate_ids.extend(
|
candidate_ids.extend(
|
||||||
self._face_ids_at_plane_position(
|
self._face_ids_at_plane_position(
|
||||||
model,
|
model,
|
||||||
target_position,
|
target_position,
|
||||||
plane_direction,
|
plane_direction,
|
||||||
_float_or_none(parameters.get("bbox_diagonal")),
|
_float_or_none(parameters.get("bbox_diagonal")),
|
||||||
self._edit_integrity_int_or_none(parameters.get("part_id")),
|
part_id,
|
||||||
self._edit_integrity_int_or_none(parameters.get("solid_id")),
|
solid_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if solid_id is not None and solid_id >= 0:
|
||||||
|
candidate_ids.extend(
|
||||||
|
self._face_ids_at_plane_position(
|
||||||
|
model,
|
||||||
|
target_position,
|
||||||
|
plane_direction,
|
||||||
|
_float_or_none(parameters.get("bbox_diagonal")),
|
||||||
|
part_id,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
)
|
||||||
for candidate_id in candidate_ids:
|
for candidate_id in candidate_ids:
|
||||||
if not (0 <= int(candidate_id) < len(model.faces)):
|
if not (0 <= int(candidate_id) < len(model.faces)):
|
||||||
continue
|
continue
|
||||||
@@ -8470,6 +8552,10 @@ class WindowActionMixin:
|
|||||||
for face_id in filtered(range(len(model.faces))):
|
for face_id in filtered(range(len(model.faces))):
|
||||||
if face_id not in result:
|
if face_id not in result:
|
||||||
result.append(face_id)
|
result.append(face_id)
|
||||||
|
if solid_id is not None and solid_id >= 0:
|
||||||
|
for face_id in filtered(range(len(model.faces)), require_solid=False):
|
||||||
|
if face_id not in result:
|
||||||
|
result.append(face_id)
|
||||||
return result
|
return result
|
||||||
if surface in {"plane", "cylinder", "cone", "sphere", "torus"} and has_target_value:
|
if surface in {"plane", "cylinder", "cone", "sphere", "torus"} and has_target_value:
|
||||||
result = filtered(range(len(model.faces)))
|
result = filtered(range(len(model.faces)))
|
||||||
@@ -8921,6 +9007,8 @@ class WindowActionMixin:
|
|||||||
if self.selected_kind is None:
|
if self.selected_kind is None:
|
||||||
timing_detail = f"\n\n性能耗时:{timing_text}" if timing_text else ""
|
timing_detail = f"\n\n性能耗时:{timing_text}" if timing_text else ""
|
||||||
self.set_plain_info(f"{record.detail}{timing_detail}\n\n{locator_note}")
|
self.set_plain_info(f"{record.detail}{timing_detail}\n\n{locator_note}")
|
||||||
|
if hasattr(self, "_after_property_edit_finished"):
|
||||||
|
self._after_property_edit_finished(success=True)
|
||||||
|
|
||||||
@Slot(str)
|
@Slot(str)
|
||||||
def _fail_edit_action(self, message: str) -> None:
|
def _fail_edit_action(self, message: str) -> None:
|
||||||
@@ -8940,6 +9028,8 @@ class WindowActionMixin:
|
|||||||
self.statusBar().showMessage(
|
self.statusBar().showMessage(
|
||||||
"后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "不能修改,模型未修改"
|
"后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "不能修改,模型未修改"
|
||||||
)
|
)
|
||||||
|
if hasattr(self, "_after_property_edit_finished"):
|
||||||
|
self._after_property_edit_finished(success=False)
|
||||||
if close_after_cancel:
|
if close_after_cancel:
|
||||||
if self.edit_thread is not None and self.edit_thread.isRunning():
|
if self.edit_thread is not None and self.edit_thread.isRunning():
|
||||||
self.edit_thread.quit()
|
self.edit_thread.quit()
|
||||||
|
|||||||
@@ -1640,7 +1640,8 @@ class WindowCoreMixin:
|
|||||||
if edge_ids:
|
if edge_ids:
|
||||||
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
|
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
|
||||||
elif self.selected_kind == "face" and self.selected_face_id is not None:
|
elif self.selected_kind == "face" and self.selected_face_id is not None:
|
||||||
face_polydata = self._cached_face_overlay_polydata(face_ids=[self.selected_face_id], smooth=False)
|
face_ids = self._selection_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
|
||||||
|
face_polydata = self._cached_face_overlay_polydata(face_ids=face_ids, smooth=False)
|
||||||
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
|
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
|
||||||
edge_polydata = self._cached_edge_overlay_polydata(self.selected_edge_id)
|
edge_polydata = self._cached_edge_overlay_polydata(self.selected_edge_id)
|
||||||
|
|
||||||
@@ -1675,7 +1676,8 @@ class WindowCoreMixin:
|
|||||||
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
|
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
|
||||||
self._highlight_faces(face_ids=face_ids)
|
self._highlight_faces(face_ids=face_ids)
|
||||||
elif self.selected_kind == "face" and self.selected_face_id is not None:
|
elif self.selected_kind == "face" and self.selected_face_id is not None:
|
||||||
self._highlight_faces(face_ids=[self.selected_face_id])
|
face_ids = self._selection_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
|
||||||
|
self._highlight_faces(face_ids=face_ids)
|
||||||
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
|
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
|
||||||
self._highlight_edge(self.selected_edge_id)
|
self._highlight_edge(self.selected_edge_id)
|
||||||
|
|
||||||
@@ -1871,6 +1873,7 @@ class WindowCoreMixin:
|
|||||||
self.model_actor.GetProperty().SetSpecular(0.25)
|
self.model_actor.GetProperty().SetSpecular(0.25)
|
||||||
self.model_actor.GetProperty().SetSpecularPower(18)
|
self.model_actor.GetProperty().SetSpecularPower(18)
|
||||||
self.model_actor.GetProperty().SetInterpolationToPhong()
|
self.model_actor.GetProperty().SetInterpolationToPhong()
|
||||||
|
self.model_actor.GetProperty().EdgeVisibilityOff()
|
||||||
backface_property = vtk.vtkProperty()
|
backface_property = vtk.vtkProperty()
|
||||||
backface_property.SetColor(0.58, 0.62, 0.64)
|
backface_property.SetColor(0.58, 0.62, 0.64)
|
||||||
backface_property.SetDiffuse(0.9)
|
backface_property.SetDiffuse(0.9)
|
||||||
@@ -2623,7 +2626,7 @@ class WindowCoreMixin:
|
|||||||
info.setdefault("feature_mode", "当前是几何候选判断,不等同于 CAD 历史特征")
|
info.setdefault("feature_mode", "当前是几何候选判断,不等同于 CAD 历史特征")
|
||||||
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
|
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
|
||||||
else:
|
else:
|
||||||
highlight_face_ids = [face_id]
|
highlight_face_ids = self._selection_same_domain_face_ids(face_id) or [face_id]
|
||||||
selection_fields = self._selection_identity_fields(
|
selection_fields = self._selection_identity_fields(
|
||||||
face_id,
|
face_id,
|
||||||
"特征来源 Face" if feature_mode else "Face",
|
"特征来源 Face" if feature_mode else "Face",
|
||||||
@@ -2643,6 +2646,21 @@ class WindowCoreMixin:
|
|||||||
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源 Face {logical_id}{raw_note}"
|
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源 Face {logical_id}{raw_note}"
|
||||||
self.statusBar().showMessage(self._selection_status(message, pick_position))
|
self.statusBar().showMessage(self._selection_status(message, pick_position))
|
||||||
|
|
||||||
|
def _selection_same_domain_face_ids(self, face_id: int) -> list[int]:
|
||||||
|
if self.model is None:
|
||||||
|
return [face_id]
|
||||||
|
try:
|
||||||
|
face_ids = self.model.face_region_ids(face_id)
|
||||||
|
except Exception:
|
||||||
|
face_ids = []
|
||||||
|
if not face_ids:
|
||||||
|
try:
|
||||||
|
face_ids = self.model.connected_same_domain_face_ids(face_id)
|
||||||
|
except Exception:
|
||||||
|
face_ids = []
|
||||||
|
face_ids = sorted({int(item) for item in face_ids if 0 <= int(item) < len(self.model.faces)})
|
||||||
|
return face_ids or [face_id]
|
||||||
|
|
||||||
def select_feature(self, face_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
|
def select_feature(self, face_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
return
|
return
|
||||||
@@ -2872,6 +2890,7 @@ class WindowCoreMixin:
|
|||||||
actor.GetProperty().SetSpecular(0.35)
|
actor.GetProperty().SetSpecular(0.35)
|
||||||
actor.GetProperty().SetInterpolationToPhong()
|
actor.GetProperty().SetInterpolationToPhong()
|
||||||
actor.GetProperty().SetLineWidth(2)
|
actor.GetProperty().SetLineWidth(2)
|
||||||
|
actor.GetProperty().EdgeVisibilityOff()
|
||||||
self._offset_overlay_actor_toward_camera(actor, scale=0.00035)
|
self._offset_overlay_actor_toward_camera(actor, scale=0.00035)
|
||||||
self.highlight_actor = actor
|
self.highlight_actor = actor
|
||||||
self.highlight_signature = signature
|
self.highlight_signature = signature
|
||||||
@@ -2930,11 +2949,13 @@ class WindowCoreMixin:
|
|||||||
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == target_id]
|
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == target_id]
|
||||||
self._highlight_hover_faces(face_ids=face_ids)
|
self._highlight_hover_faces(face_ids=face_ids)
|
||||||
elif kind == "feature":
|
elif kind == "feature":
|
||||||
self._highlight_hover_faces(face_ids=[target_id])
|
face_ids = self._selection_same_domain_face_ids(target_id) or [target_id]
|
||||||
|
self._highlight_hover_faces(face_ids=face_ids)
|
||||||
elif kind == "edge":
|
elif kind == "edge":
|
||||||
self._highlight_hover_edge(target_id)
|
self._highlight_hover_edge(target_id)
|
||||||
else:
|
else:
|
||||||
self._highlight_hover_faces(face_ids=[target_id])
|
face_ids = self._selection_same_domain_face_ids(target_id) or [target_id]
|
||||||
|
self._highlight_hover_faces(face_ids=face_ids)
|
||||||
self.hover_signature = signature
|
self.hover_signature = signature
|
||||||
self.render_window.Render()
|
self.render_window.Render()
|
||||||
|
|
||||||
@@ -2957,6 +2978,7 @@ class WindowCoreMixin:
|
|||||||
actor.GetProperty().SetInterpolationToPhong()
|
actor.GetProperty().SetInterpolationToPhong()
|
||||||
actor.GetProperty().LightingOff()
|
actor.GetProperty().LightingOff()
|
||||||
actor.GetProperty().SetLineWidth(2)
|
actor.GetProperty().SetLineWidth(2)
|
||||||
|
actor.GetProperty().EdgeVisibilityOff()
|
||||||
self._offset_overlay_actor_toward_camera(actor, scale=0.00055)
|
self._offset_overlay_actor_toward_camera(actor, scale=0.00055)
|
||||||
self.hover_face_actor = actor
|
self.hover_face_actor = actor
|
||||||
self.renderer.AddActor(actor)
|
self.renderer.AddActor(actor)
|
||||||
|
|||||||
+429
-166
@@ -26,6 +26,7 @@ from PySide6.QtGui import QColor
|
|||||||
|
|
||||||
from .constants import FACE_SELECTION_FEATURE_INFO_SURFACES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY
|
from .constants import FACE_SELECTION_FEATURE_INFO_SURFACES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY
|
||||||
from .model import StepModel
|
from .model import StepModel
|
||||||
|
from .parametric_component import component_edit_config_from_spec
|
||||||
from .records import OperationRecord
|
from .records import OperationRecord
|
||||||
from .ui_helpers import * # noqa: F403
|
from .ui_helpers import * # noqa: F403
|
||||||
from .widgets import NoWheelComboBox
|
from .widgets import NoWheelComboBox
|
||||||
@@ -172,6 +173,11 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
|
|||||||
surface = str(action_info.get("surface", "") or "")
|
surface = str(action_info.get("surface", "") or "")
|
||||||
feature_guess = str(action_info.get("feature_guess", "") or "")
|
feature_guess = str(action_info.get("feature_guess", "") or "")
|
||||||
angular_span = _float_or_none(action_info.get("angular_span"))
|
angular_span = _float_or_none(action_info.get("angular_span"))
|
||||||
|
face_size_keys = (
|
||||||
|
("local_face_width", "local_face_height")
|
||||||
|
if bool(action_info.get("local_face_size_edit_ready"))
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
|
||||||
if surface == "plane":
|
if surface == "plane":
|
||||||
if action_info.get("multistep_prismatic_status") == "blocked":
|
if action_info.get("multistep_prismatic_status") == "blocked":
|
||||||
@@ -179,7 +185,8 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
|
|||||||
if action_info.get("existing_chamfer_status") == "candidate":
|
if action_info.get("existing_chamfer_status") == "candidate":
|
||||||
return ("existing_chamfer_distance_estimate",)
|
return ("existing_chamfer_distance_estimate",)
|
||||||
if action_info.get("prismatic_profile_status") == "candidate":
|
if action_info.get("prismatic_profile_status") == "candidate":
|
||||||
keys = ["local_face_width", "local_face_height"]
|
keys = list(face_size_keys)
|
||||||
|
keys.append("face_target_normal_position")
|
||||||
if action_info.get("prismatic_extrusion_status") == "candidate":
|
if action_info.get("prismatic_extrusion_status") == "candidate":
|
||||||
keys.append("shell_thickness_estimate")
|
keys.append("shell_thickness_estimate")
|
||||||
if str(action_info.get("prismatic_feature_semantics") or "") in {"additive-boss", "subtractive-pocket"}:
|
if str(action_info.get("prismatic_feature_semantics") or "") in {"additive-boss", "subtractive-pocket"}:
|
||||||
@@ -187,15 +194,13 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
|
|||||||
return tuple(keys)
|
return tuple(keys)
|
||||||
if action_info.get("shell_region_status") == "candidate":
|
if action_info.get("shell_region_status") == "candidate":
|
||||||
return (
|
return (
|
||||||
"local_face_width",
|
*face_size_keys,
|
||||||
"local_face_height",
|
|
||||||
"face_center_position",
|
"face_center_position",
|
||||||
"face_target_normal_position",
|
"face_target_normal_position",
|
||||||
"shell_thickness_estimate",
|
"shell_thickness_estimate",
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
"local_face_width",
|
*face_size_keys,
|
||||||
"local_face_height",
|
|
||||||
"face_center_position",
|
"face_center_position",
|
||||||
"face_target_normal_position",
|
"face_target_normal_position",
|
||||||
)
|
)
|
||||||
@@ -340,11 +345,27 @@ class WindowStateMixin:
|
|||||||
title = f"{title_prefix} {logical_id}"
|
title = f"{title_prefix} {logical_id}"
|
||||||
if logical_id != int(face_id):
|
if logical_id != int(face_id):
|
||||||
title += f"(当前拓扑 Face {face_id})"
|
title += f"(当前拓扑 Face {face_id})"
|
||||||
return {
|
result = {
|
||||||
"selection_title": title,
|
"selection_title": title,
|
||||||
"selection_display_id": logical_id,
|
"selection_display_id": logical_id,
|
||||||
"selection_topological_face_id": int(face_id),
|
"selection_topological_face_id": int(face_id),
|
||||||
}
|
}
|
||||||
|
if self.model is not None:
|
||||||
|
try:
|
||||||
|
region_ids = self.model.face_region_ids(face_id)
|
||||||
|
except Exception:
|
||||||
|
region_ids = []
|
||||||
|
region_ids = sorted({int(item) for item in region_ids if 0 <= int(item) < len(self.model.faces)})
|
||||||
|
if len(region_ids) > 1:
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"face_region_logical_id": logical_id,
|
||||||
|
"face_region_ids": tuple(region_ids),
|
||||||
|
"face_region_count": len(region_ids),
|
||||||
|
"face_region_note": "STEP/B-Rep 将该局部面拆成多个同域拓扑 Face;界面按一个逻辑面区域显示,高亮包含全部碎片。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def _first_level_fact_selection_fields(self, face_id: int, scope: str = "auto") -> dict[str, object]:
|
def _first_level_fact_selection_fields(self, face_id: int, scope: str = "auto") -> dict[str, object]:
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
@@ -553,6 +574,21 @@ class WindowStateMixin:
|
|||||||
info.setdefault("feature_type", "可拉伸/切除平面候选")
|
info.setdefault("feature_type", "可拉伸/切除平面候选")
|
||||||
info.setdefault("feature_edit_actions", "拉伸/切除平面")
|
info.setdefault("feature_edit_actions", "拉伸/切除平面")
|
||||||
elif surface == "cylinder":
|
elif surface == "cylinder":
|
||||||
|
same_domain_ids = []
|
||||||
|
try:
|
||||||
|
same_domain_ids = self.model.connected_same_domain_face_ids(face_id) or [face_id]
|
||||||
|
except Exception:
|
||||||
|
same_domain_ids = [face_id]
|
||||||
|
same_domain_ids = sorted({int(item) for item in same_domain_ids if 0 <= int(item) < len(self.model.faces)})
|
||||||
|
if same_domain_ids:
|
||||||
|
info["same_domain_face_ids"] = tuple(same_domain_ids)
|
||||||
|
info["same_domain_face_count"] = len(same_domain_ids)
|
||||||
|
info["feature_highlight_face_ids"] = tuple(same_domain_ids)
|
||||||
|
if len(same_domain_ids) > 1:
|
||||||
|
info.setdefault(
|
||||||
|
"same_domain_note",
|
||||||
|
"当前圆柱面在 STEP/B-Rep 中被拆成多个同域拓扑 Face,界面按一个局部孔/槽区域高亮。",
|
||||||
|
)
|
||||||
feature_guess = str(info.get("feature_guess", "") or "")
|
feature_guess = str(info.get("feature_guess", "") or "")
|
||||||
angular_span = _float_or_none(info.get("angular_span"))
|
angular_span = _float_or_none(info.get("angular_span"))
|
||||||
if feature_guess == "hole/groove candidate":
|
if feature_guess == "hole/groove candidate":
|
||||||
@@ -768,7 +804,7 @@ class WindowStateMixin:
|
|||||||
if self.load_in_progress:
|
if self.load_in_progress:
|
||||||
self.statusBar().showMessage("STEP background loading is still running.")
|
self.statusBar().showMessage("STEP background loading is still running.")
|
||||||
return True
|
return True
|
||||||
if self.operation_in_progress:
|
if self.operation_in_progress and not bool(getattr(self, "_property_batch_running_action", False)):
|
||||||
self.statusBar().showMessage(message)
|
self.statusBar().showMessage(message)
|
||||||
return True
|
return True
|
||||||
if self.scan_in_progress:
|
if self.scan_in_progress:
|
||||||
@@ -2004,7 +2040,7 @@ class WindowStateMixin:
|
|||||||
)
|
)
|
||||||
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
|
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
|
||||||
input_item = self._property_table_item("", editable=False)
|
input_item = self._property_table_item("", editable=False)
|
||||||
input_item.setToolTip("勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。")
|
input_item.setToolTip("勾选后点击“导出参数”,会写入根目录 data.json,并把参数列表嵌入组件 main.py。")
|
||||||
row_items = (label_item, current_item, scope_item, target_item, input_item)
|
row_items = (label_item, current_item, scope_item, target_item, input_item)
|
||||||
self._style_property_row_items(row_items, editable=editable, spec=effective_spec)
|
self._style_property_row_items(row_items, editable=editable, spec=effective_spec)
|
||||||
for column, item in enumerate(row_items):
|
for column, item in enumerate(row_items):
|
||||||
@@ -2191,7 +2227,7 @@ class WindowStateMixin:
|
|||||||
checkbox.setChecked(False)
|
checkbox.setChecked(False)
|
||||||
checkbox.setEnabled(exportable)
|
checkbox.setEnabled(exportable)
|
||||||
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
|
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
|
||||||
tip = "勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。"
|
tip = "勾选后点击“导出参数”,会写入根目录 data.json,并把参数列表嵌入组件 main.py。"
|
||||||
if not exportable:
|
if not exportable:
|
||||||
tip = "当前行不是可导出的尺寸输入参数。"
|
tip = "当前行不是可导出的尺寸输入参数。"
|
||||||
checkbox.setToolTip(tip)
|
checkbox.setToolTip(tip)
|
||||||
@@ -2288,7 +2324,7 @@ class WindowStateMixin:
|
|||||||
finally:
|
finally:
|
||||||
checkbox.blockSignals(was_blocked)
|
checkbox.blockSignals(was_blocked)
|
||||||
tip = (
|
tip = (
|
||||||
"勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。"
|
"勾选后点击“导出参数”,会写入根目录 data.json,并把参数列表嵌入组件 main.py。"
|
||||||
if exportable
|
if exportable
|
||||||
else "当前行不是可导出的尺寸输入参数。"
|
else "当前行不是可导出的尺寸输入参数。"
|
||||||
)
|
)
|
||||||
@@ -2442,16 +2478,21 @@ class WindowStateMixin:
|
|||||||
prismatic_length = _float_or_none(action_info.get("prismatic_length"))
|
prismatic_length = _float_or_none(action_info.get("prismatic_length"))
|
||||||
prismatic_width = _float_or_none(action_info.get("prismatic_width"))
|
prismatic_width = _float_or_none(action_info.get("prismatic_width"))
|
||||||
prismatic_depth = _float_or_none(action_info.get("prismatic_extrusion_estimate"))
|
prismatic_depth = _float_or_none(action_info.get("prismatic_extrusion_estimate"))
|
||||||
|
prismatic_reference_face_id = _int_or_none(action_info.get("prismatic_reference_face_id"))
|
||||||
|
part_solid_count = _int_or_none(action_info.get("part_solid_count"))
|
||||||
prismatic_size_supported = bool(
|
prismatic_size_supported = bool(
|
||||||
embedded_prismatic
|
embedded_prismatic
|
||||||
and action_info.get("prismatic_profile_status") == "candidate"
|
and action_info.get("prismatic_profile_status") == "candidate"
|
||||||
and action_info.get("prismatic_extrusion_status") == "candidate"
|
and action_info.get("prismatic_extrusion_status") == "candidate"
|
||||||
|
and bool(action_info.get("local_face_size_edit_ready"))
|
||||||
and prismatic_length is not None
|
and prismatic_length is not None
|
||||||
and prismatic_length > 0
|
and prismatic_length > 0
|
||||||
and prismatic_width is not None
|
and prismatic_width is not None
|
||||||
and prismatic_width > 0
|
and prismatic_width > 0
|
||||||
and prismatic_depth is not None
|
and prismatic_depth is not None
|
||||||
and prismatic_depth > 0
|
and prismatic_depth > 0
|
||||||
|
and prismatic_reference_face_id is not None
|
||||||
|
and (part_solid_count is None or part_solid_count == 1)
|
||||||
and len(_int_values(action_info.get("prismatic_connected_side_face_ids"))) == 4
|
and len(_int_values(action_info.get("prismatic_connected_side_face_ids"))) == 4
|
||||||
)
|
)
|
||||||
dimensions: list[dict[str, object]] = []
|
dimensions: list[dict[str, object]] = []
|
||||||
@@ -2470,14 +2511,16 @@ class WindowStateMixin:
|
|||||||
dimension = dict(spec)
|
dimension = dict(spec)
|
||||||
dimension["parameter_role"] = "dimension"
|
dimension["parameter_role"] = "dimension"
|
||||||
if action_info.get("prismatic_profile_status") == "candidate":
|
if action_info.get("prismatic_profile_status") == "candidate":
|
||||||
if prismatic_size_key:
|
if prismatic_size_key and prismatic_size_supported:
|
||||||
if not prismatic_size_supported:
|
|
||||||
continue
|
|
||||||
modes = spec.get("scope_modes")
|
modes = spec.get("scope_modes")
|
||||||
local_mode = modes.get("local") if isinstance(modes, dict) else None
|
local_mode = modes.get("local") if isinstance(modes, dict) else None
|
||||||
local_mode = dict(local_mode) if isinstance(local_mode, dict) else {}
|
local_mode = dict(local_mode) if isinstance(local_mode, dict) else {}
|
||||||
is_width_axis = key == "local_face_width"
|
is_width_axis = key == "local_face_width"
|
||||||
current_limit = prismatic_width if is_width_axis else prismatic_length
|
current_limit = prismatic_width if is_width_axis else prismatic_length
|
||||||
|
size_blocker = str(
|
||||||
|
action_info.get("local_face_size_edit_blocker")
|
||||||
|
or "当前矩形特征的长宽、深度、参考面、单 Solid 条件或四个侧壁没有稳定识别,暂不开放长宽修改。"
|
||||||
|
)
|
||||||
local_mode.update(
|
local_mode.update(
|
||||||
{
|
{
|
||||||
"label": "局部重建",
|
"label": "局部重建",
|
||||||
@@ -2488,7 +2531,7 @@ class WindowStateMixin:
|
|||||||
"输入矩形凸台/口袋的目标长度或宽度;程序会先移除/补回旧矩形包络,"
|
"输入矩形凸台/口袋的目标长度或宽度;程序会先移除/补回旧矩形包络,"
|
||||||
"再重建目标矩形特征。"
|
"再重建目标矩形特征。"
|
||||||
),
|
),
|
||||||
"disabled_tip": "当前矩形特征的长宽、深度或四个侧壁没有稳定识别,暂不开放长宽修改。",
|
"disabled_tip": size_blocker,
|
||||||
"range_hint": "当前版本不在一次编辑中交换长度和宽度方向。",
|
"range_hint": "当前版本不在一次编辑中交换长度和宽度方向。",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -2752,6 +2795,8 @@ class WindowStateMixin:
|
|||||||
show_generic_face_edit_specs = bool(has_face and (is_plane or is_shell_candidate) and not is_existing_chamfer)
|
show_generic_face_edit_specs = bool(has_face and (is_plane or is_shell_candidate) and not is_existing_chamfer)
|
||||||
local_face_deform_ready = bool(action_info.get("local_face_deform_ready", True))
|
local_face_deform_ready = bool(action_info.get("local_face_deform_ready", True))
|
||||||
local_face_deform_blocker = str(action_info.get("local_face_deform_blocker") or "").strip()
|
local_face_deform_blocker = str(action_info.get("local_face_deform_blocker") or "").strip()
|
||||||
|
local_face_size_edit_ready = bool(action_info.get("local_face_size_edit_ready"))
|
||||||
|
local_face_size_edit_blocker = str(action_info.get("local_face_size_edit_blocker") or "").strip()
|
||||||
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
|
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
|
||||||
is_line_edge = has_edge and curve == "line"
|
is_line_edge = has_edge and curve == "line"
|
||||||
specs: list[dict[str, object]] = []
|
specs: list[dict[str, object]] = []
|
||||||
@@ -3553,147 +3598,150 @@ class WindowStateMixin:
|
|||||||
current_face_width = _float_or_none(action_info.get("local_face_width"))
|
current_face_width = _float_or_none(action_info.get("local_face_width"))
|
||||||
current_face_height = _float_or_none(action_info.get("local_face_height"))
|
current_face_height = _float_or_none(action_info.get("local_face_height"))
|
||||||
face_size_tip = (
|
face_size_tip = (
|
||||||
"这里的面内长度/面内宽度是选中 Face 在自身平面内两个主方向上的投影长度,"
|
"面内长度/面内宽度是矩形平面特征的两个驱动尺寸,"
|
||||||
"不是面积,也不是模型整体高度。局部重建会移动当前 Face 顶点,相邻面会按新顶点重建。"
|
"不是面积,也不是任意曲面的通用尺寸。局部重建会重建当前矩形特征,相邻面按新边界更新。"
|
||||||
)
|
)
|
||||||
face_size_owner_tip = (
|
face_size_owner_tip = (
|
||||||
"这里的面内长度/面内宽度是选中 Face 在自身平面内两个主方向上的投影长度,"
|
"面内长度/面内宽度是矩形平面特征的两个驱动尺寸,"
|
||||||
"不是面积,也不是模型整体高度。程序会沿该方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
"不是面积,也不是任意曲面的通用尺寸。程序会沿该方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
||||||
)
|
)
|
||||||
add_scoped_spec(
|
if local_face_size_edit_ready:
|
||||||
key="local_face_width",
|
add_scoped_spec(
|
||||||
label="面内长度",
|
key="local_face_width",
|
||||||
current_raw=current_face_width if current_face_width is not None else "",
|
label="面内长度",
|
||||||
target_text=numeric_text(current_face_width),
|
current_raw=current_face_width if current_face_width is not None else "",
|
||||||
scope_default="local",
|
target_text=numeric_text(current_face_width),
|
||||||
scope_modes={
|
scope_default="local",
|
||||||
"local": {
|
scope_modes={
|
||||||
"label": "局部重建",
|
"local": {
|
||||||
"action": "resize_face_width_local",
|
"label": "局部重建",
|
||||||
"target_attr": "face_width_input",
|
"action": "resize_face_width_local",
|
||||||
"enabled": bool(
|
"target_attr": "face_width_input",
|
||||||
local_face_deform_ready
|
"enabled": bool(
|
||||||
and current_face_width is not None
|
local_face_deform_ready
|
||||||
and current_face_width > 0
|
and current_face_width is not None
|
||||||
and current_face_center is not None
|
and current_face_width > 0
|
||||||
),
|
and current_face_center is not None
|
||||||
"enabled_tip": f"输入目标面内长度;{face_size_tip}",
|
),
|
||||||
"disabled_tip": face_local_disabled_tip(
|
"enabled_tip": f"输入目标面内长度;{face_size_tip}",
|
||||||
"只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改面内长度。"
|
"disabled_tip": face_local_disabled_tip(
|
||||||
),
|
local_face_size_edit_blocker
|
||||||
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}",
|
or "当前对象还没有识别为稳定矩形平面特征,不能修改面内长度。"
|
||||||
|
),
|
||||||
|
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}",
|
||||||
|
},
|
||||||
|
"keep_relations": {
|
||||||
|
"label": "保持关系",
|
||||||
|
"action": "resize_face_width_keep_relations",
|
||||||
|
"target_attr": "face_width_input",
|
||||||
|
"enabled": bool(
|
||||||
|
current_face_width is not None
|
||||||
|
and current_face_width > 0
|
||||||
|
and current_face_center is not None
|
||||||
|
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||||
|
and face_keep_relation_enabled
|
||||||
|
),
|
||||||
|
"enabled_tip": (
|
||||||
|
"输入目标面内长度;程序会沿该方向缩放所属特征或 Solid,"
|
||||||
|
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
|
||||||
|
),
|
||||||
|
"disabled_tip": (
|
||||||
|
face_keep_relation_disabled_tip
|
||||||
|
if not face_keep_relation_enabled
|
||||||
|
else "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能保持关系地修改面内长度。"
|
||||||
|
),
|
||||||
|
"range_hint": (
|
||||||
|
"保持关系会把面内长度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
|
||||||
|
f"{relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"owning": {
|
||||||
|
"label": "缩放特征",
|
||||||
|
"action": "resize_face_width_owning_scale",
|
||||||
|
"target_attr": "face_width_input",
|
||||||
|
"enabled": bool(
|
||||||
|
current_face_width is not None
|
||||||
|
and current_face_width > 0
|
||||||
|
and current_face_center is not None
|
||||||
|
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||||
|
),
|
||||||
|
"enabled_tip": f"输入目标面内长度;{face_size_owner_tip}",
|
||||||
|
"disabled_tip": "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
|
||||||
|
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_width, 0.2, 0.5, face_linear_hard_limit)}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep_relations": {
|
value_type="positive",
|
||||||
"label": "保持关系",
|
used=("local_face_width", "local_face_width_direction", "local_face_size_center"),
|
||||||
"action": "resize_face_width_keep_relations",
|
**face_scale_limits(current_face_width),
|
||||||
"target_attr": "face_width_input",
|
)
|
||||||
"enabled": bool(
|
add_scoped_spec(
|
||||||
current_face_width is not None
|
key="local_face_height",
|
||||||
and current_face_width > 0
|
label="面内宽度",
|
||||||
and current_face_center is not None
|
current_raw=current_face_height if current_face_height is not None else "",
|
||||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
target_text=numeric_text(current_face_height),
|
||||||
and face_keep_relation_enabled
|
scope_default="local",
|
||||||
),
|
scope_modes={
|
||||||
"enabled_tip": (
|
"local": {
|
||||||
"输入目标面内长度;程序会沿该方向缩放所属特征或 Solid,"
|
"label": "局部重建",
|
||||||
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
|
"action": "resize_face_height_local",
|
||||||
),
|
"target_attr": "face_height_input",
|
||||||
"disabled_tip": (
|
"enabled": bool(
|
||||||
face_keep_relation_disabled_tip
|
local_face_deform_ready
|
||||||
if not face_keep_relation_enabled
|
and current_face_height is not None
|
||||||
else "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能保持关系地修改面内长度。"
|
and current_face_height > 0
|
||||||
),
|
and current_face_center is not None
|
||||||
"range_hint": (
|
),
|
||||||
"保持关系会把面内长度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
|
"enabled_tip": f"输入目标面内宽度;{face_size_tip}",
|
||||||
f"{relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}"
|
"disabled_tip": face_local_disabled_tip(
|
||||||
),
|
local_face_size_edit_blocker
|
||||||
|
or "当前对象还没有识别为稳定矩形平面特征,不能修改面内宽度。"
|
||||||
|
),
|
||||||
|
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}",
|
||||||
|
},
|
||||||
|
"keep_relations": {
|
||||||
|
"label": "保持关系",
|
||||||
|
"action": "resize_face_height_keep_relations",
|
||||||
|
"target_attr": "face_height_input",
|
||||||
|
"enabled": bool(
|
||||||
|
current_face_height is not None
|
||||||
|
and current_face_height > 0
|
||||||
|
and current_face_center is not None
|
||||||
|
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||||
|
and face_keep_relation_enabled
|
||||||
|
),
|
||||||
|
"enabled_tip": (
|
||||||
|
"输入目标面内宽度;程序会沿该方向缩放所属特征或 Solid,"
|
||||||
|
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
|
||||||
|
),
|
||||||
|
"disabled_tip": (
|
||||||
|
face_keep_relation_disabled_tip
|
||||||
|
if not face_keep_relation_enabled
|
||||||
|
else "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能保持关系地修改面内宽度。"
|
||||||
|
),
|
||||||
|
"range_hint": (
|
||||||
|
"保持关系会把面内宽度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
|
||||||
|
f"{relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"owning": {
|
||||||
|
"label": "缩放特征",
|
||||||
|
"action": "resize_face_height_owning_scale",
|
||||||
|
"target_attr": "face_height_input",
|
||||||
|
"enabled": bool(
|
||||||
|
current_face_height is not None
|
||||||
|
and current_face_height > 0
|
||||||
|
and current_face_center is not None
|
||||||
|
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||||
|
),
|
||||||
|
"enabled_tip": f"输入目标面内宽度;{face_size_owner_tip}",
|
||||||
|
"disabled_tip": "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
|
||||||
|
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_height, 0.2, 0.5, face_linear_hard_limit)}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"owning": {
|
value_type="positive",
|
||||||
"label": "缩放特征",
|
used=("local_face_height", "local_face_height_direction", "local_face_size_center"),
|
||||||
"action": "resize_face_width_owning_scale",
|
**face_scale_limits(current_face_height),
|
||||||
"target_attr": "face_width_input",
|
)
|
||||||
"enabled": bool(
|
|
||||||
current_face_width is not None
|
|
||||||
and current_face_width > 0
|
|
||||||
and current_face_center is not None
|
|
||||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
|
||||||
),
|
|
||||||
"enabled_tip": f"输入目标面内长度;{face_size_owner_tip}",
|
|
||||||
"disabled_tip": "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
|
|
||||||
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_width, 0.2, 0.5, face_linear_hard_limit)}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
value_type="positive",
|
|
||||||
used=("local_face_width", "local_face_width_direction", "local_face_size_center"),
|
|
||||||
**face_scale_limits(current_face_width),
|
|
||||||
)
|
|
||||||
add_scoped_spec(
|
|
||||||
key="local_face_height",
|
|
||||||
label="面内宽度",
|
|
||||||
current_raw=current_face_height if current_face_height is not None else "",
|
|
||||||
target_text=numeric_text(current_face_height),
|
|
||||||
scope_default="local",
|
|
||||||
scope_modes={
|
|
||||||
"local": {
|
|
||||||
"label": "局部重建",
|
|
||||||
"action": "resize_face_height_local",
|
|
||||||
"target_attr": "face_height_input",
|
|
||||||
"enabled": bool(
|
|
||||||
local_face_deform_ready
|
|
||||||
and current_face_height is not None
|
|
||||||
and current_face_height > 0
|
|
||||||
and current_face_center is not None
|
|
||||||
),
|
|
||||||
"enabled_tip": f"输入目标面内宽度;{face_size_tip}",
|
|
||||||
"disabled_tip": face_local_disabled_tip(
|
|
||||||
"只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改面内宽度。"
|
|
||||||
),
|
|
||||||
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}",
|
|
||||||
},
|
|
||||||
"keep_relations": {
|
|
||||||
"label": "保持关系",
|
|
||||||
"action": "resize_face_height_keep_relations",
|
|
||||||
"target_attr": "face_height_input",
|
|
||||||
"enabled": bool(
|
|
||||||
current_face_height is not None
|
|
||||||
and current_face_height > 0
|
|
||||||
and current_face_center is not None
|
|
||||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
|
||||||
and face_keep_relation_enabled
|
|
||||||
),
|
|
||||||
"enabled_tip": (
|
|
||||||
"输入目标面内宽度;程序会沿该方向缩放所属特征或 Solid,"
|
|
||||||
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
|
|
||||||
),
|
|
||||||
"disabled_tip": (
|
|
||||||
face_keep_relation_disabled_tip
|
|
||||||
if not face_keep_relation_enabled
|
|
||||||
else "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能保持关系地修改面内宽度。"
|
|
||||||
),
|
|
||||||
"range_hint": (
|
|
||||||
"保持关系会把面内宽度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
|
|
||||||
f"{relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"owning": {
|
|
||||||
"label": "缩放特征",
|
|
||||||
"action": "resize_face_height_owning_scale",
|
|
||||||
"target_attr": "face_height_input",
|
|
||||||
"enabled": bool(
|
|
||||||
current_face_height is not None
|
|
||||||
and current_face_height > 0
|
|
||||||
and current_face_center is not None
|
|
||||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
|
||||||
),
|
|
||||||
"enabled_tip": f"输入目标面内宽度;{face_size_owner_tip}",
|
|
||||||
"disabled_tip": "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
|
|
||||||
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_height, 0.2, 0.5, face_linear_hard_limit)}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
value_type="positive",
|
|
||||||
used=("local_face_height", "local_face_height_direction", "local_face_size_center"),
|
|
||||||
**face_scale_limits(current_face_height),
|
|
||||||
)
|
|
||||||
add_scoped_spec(
|
add_scoped_spec(
|
||||||
key="face_center_position",
|
key="face_center_position",
|
||||||
label="中心",
|
label="中心",
|
||||||
@@ -3773,11 +3821,7 @@ class WindowStateMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_plane and not is_existing_chamfer:
|
if is_plane and not is_existing_chamfer:
|
||||||
plane_origin = _triple_or_none(action_info.get("plane_origin"))
|
plane_origin, plane_direction = self._plane_offset_info_for_property(action_info)
|
||||||
plane_direction = (
|
|
||||||
_triple_or_none(action_info.get("push_pull_outward_direction"))
|
|
||||||
or _triple_or_none(action_info.get("normal"))
|
|
||||||
)
|
|
||||||
current_plane_position = None
|
current_plane_position = None
|
||||||
if plane_origin is not None and plane_direction is not None:
|
if plane_origin is not None and plane_direction is not None:
|
||||||
direction_length = math.sqrt(
|
direction_length = math.sqrt(
|
||||||
@@ -3845,10 +3889,10 @@ class WindowStateMixin:
|
|||||||
"action": "push_pull_face",
|
"action": "push_pull_face",
|
||||||
"target_attr": "offset_input",
|
"target_attr": "offset_input",
|
||||||
"enabled": current_plane_position is not None,
|
"enabled": current_plane_position is not None,
|
||||||
"enabled_tip": "输入偏移的目标位置;程序会沿当前 Face 法向执行拉伸或切除。",
|
"enabled_tip": "输入偏移的目标位置;程序会沿判定的材料外侧法向执行拉伸或切除。",
|
||||||
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能按目标位置拉伸/切除。",
|
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能按目标位置拉伸/切除。",
|
||||||
"range_hint": (
|
"range_hint": (
|
||||||
"偏移是沿当前 Face 法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
|
"偏移是沿程序判定的材料外侧法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
|
||||||
"程序会把目标位置自动换算成本次拉伸/切除距离。"
|
"程序会把目标位置自动换算成本次拉伸/切除距离。"
|
||||||
f"{face_offset_hard_limit}"
|
f"{face_offset_hard_limit}"
|
||||||
),
|
),
|
||||||
@@ -3861,7 +3905,7 @@ class WindowStateMixin:
|
|||||||
"target_attr": "offset_input",
|
"target_attr": "offset_input",
|
||||||
"enabled": keep_relation_enabled,
|
"enabled": keep_relation_enabled,
|
||||||
"enabled_tip": (
|
"enabled_tip": (
|
||||||
"输入偏移的目标位置;程序会沿当前 Face 法向拉伸/切除,"
|
"输入偏移的目标位置;程序会沿判定的材料外侧法向拉伸/切除,"
|
||||||
"并要求一级相邻平面的平行/垂直关系可验证。"
|
"并要求一级相邻平面的平行/垂直关系可验证。"
|
||||||
),
|
),
|
||||||
"disabled_tip": keep_relation_disabled_tip,
|
"disabled_tip": keep_relation_disabled_tip,
|
||||||
@@ -3884,7 +3928,7 @@ class WindowStateMixin:
|
|||||||
and current_face_center is not None
|
and current_face_center is not None
|
||||||
),
|
),
|
||||||
"enabled_tip": (
|
"enabled_tip": (
|
||||||
"输入偏移的目标位置;程序只把当前 Face 沿法向移动到该目标值,"
|
"输入偏移的目标位置;程序只把当前 Face 沿判定的材料外侧法向移动到该目标值,"
|
||||||
"并让相邻平面按新顶点重建。"
|
"并让相邻平面按新顶点重建。"
|
||||||
),
|
),
|
||||||
"disabled_tip": face_local_disabled_tip(
|
"disabled_tip": face_local_disabled_tip(
|
||||||
@@ -3908,7 +3952,7 @@ class WindowStateMixin:
|
|||||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||||
),
|
),
|
||||||
"enabled_tip": (
|
"enabled_tip": (
|
||||||
"输入偏移的目标位置;程序会沿当前 Face 法向平移所属特征或 Solid,"
|
"输入偏移的目标位置;程序会沿判定的材料外侧法向平移所属特征或 Solid,"
|
||||||
"当前面形状和所属对象内部尺寸不变。"
|
"当前面形状和所属对象内部尺寸不变。"
|
||||||
),
|
),
|
||||||
"disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移移动所属特征。",
|
"disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移移动所属特征。",
|
||||||
@@ -5863,12 +5907,19 @@ class WindowStateMixin:
|
|||||||
changed = self._changed_property_rows()
|
changed = self._changed_property_rows()
|
||||||
enabled = bool(has_model and changed)
|
enabled = bool(has_model and changed)
|
||||||
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
|
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
|
||||||
if changed and len(changed) > 1:
|
if changed:
|
||||||
disabled_tip = "当前一次只执行一个几何修改;请只保留一行目标值不同,再点击参数化建模。"
|
count = len(changed)
|
||||||
|
enabled_tip = (
|
||||||
|
"应用当前被修改的 1 个参数。"
|
||||||
|
if count == 1
|
||||||
|
else f"按表格顺序依次应用当前被修改的 {count} 个参数;失败时会停止后续修改。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
enabled_tip = "应用当前被修改的参数。"
|
||||||
self._set_control_state(
|
self._set_control_state(
|
||||||
self.apply_property_button,
|
self.apply_property_button,
|
||||||
enabled and len(changed) == 1,
|
enabled,
|
||||||
"应用当前被修改的参数。",
|
enabled_tip,
|
||||||
disabled_tip,
|
disabled_tip,
|
||||||
)
|
)
|
||||||
self._update_parameter_export_state(has_model)
|
self._update_parameter_export_state(has_model)
|
||||||
@@ -5907,6 +5958,37 @@ class WindowStateMixin:
|
|||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
def _selected_parameter_component_edits(self, rows: list[dict[str, str]] | None = None) -> list[dict[str, object]]:
|
||||||
|
if not hasattr(self, "property_table"):
|
||||||
|
return []
|
||||||
|
specs = getattr(self, "property_editor_specs", [])
|
||||||
|
row_count = min(self.property_table.rowCount(), len(specs))
|
||||||
|
export_rows = list(rows or self._selected_parameter_export_rows())
|
||||||
|
edits: list[dict[str, object]] = []
|
||||||
|
export_index = 0
|
||||||
|
step_path = self.step_path if isinstance(getattr(self, "step_path", None), Path) else None
|
||||||
|
for row in range(row_count):
|
||||||
|
checkbox = self._property_input_checkbox(row)
|
||||||
|
if checkbox is None or not checkbox.isEnabled() or not checkbox.isChecked():
|
||||||
|
continue
|
||||||
|
if export_index >= len(export_rows):
|
||||||
|
break
|
||||||
|
spec = self._effective_property_spec(specs[row], row=row)
|
||||||
|
if str(spec.get("value_type", "number")) == "command":
|
||||||
|
continue
|
||||||
|
edit = component_edit_config_from_spec(
|
||||||
|
parameter_row=export_rows[export_index],
|
||||||
|
spec=spec,
|
||||||
|
selected_kind=str(getattr(self, "selected_kind", "") or ""),
|
||||||
|
selected_face_id=getattr(self, "selected_face_id", None),
|
||||||
|
selected_edge_id=getattr(self, "selected_edge_id", None),
|
||||||
|
step_path=step_path,
|
||||||
|
)
|
||||||
|
if edit is not None:
|
||||||
|
edits.append(edit)
|
||||||
|
export_index += 1
|
||||||
|
return edits
|
||||||
|
|
||||||
def _update_parameter_export_state(self, has_model: bool | None = None) -> None:
|
def _update_parameter_export_state(self, has_model: bool | None = None) -> None:
|
||||||
if not hasattr(self, "export_parameters_button"):
|
if not hasattr(self, "export_parameters_button"):
|
||||||
return
|
return
|
||||||
@@ -5922,7 +6004,7 @@ class WindowStateMixin:
|
|||||||
self._set_control_state(
|
self._set_control_state(
|
||||||
self.export_parameters_button,
|
self.export_parameters_button,
|
||||||
bool(has_model and selected_rows),
|
bool(has_model and selected_rows),
|
||||||
f"导出已勾选的 {len(selected_rows)} 个输入参数到 data.json。",
|
f"导出已勾选的 {len(selected_rows)} 个输入参数到 data.json,并生成参数化组件 main.py。",
|
||||||
disabled_tip,
|
disabled_tip,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5949,6 +6031,63 @@ class WindowStateMixin:
|
|||||||
changed.append((row, effective_spec, text))
|
changed.append((row, effective_spec, text))
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
def _property_batch_item_from_row(self, row: int, spec: dict[str, object], text: str) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"key": str(spec.get("key", "")),
|
||||||
|
"label": str(spec.get("label", "")),
|
||||||
|
"action": str(spec.get("action", "")),
|
||||||
|
"scope_key": str(spec.get("scope_key", self._property_scope_value(row, spec))),
|
||||||
|
"target_text": str(text),
|
||||||
|
"source_face_id": spec.get("source_face_id", self.selected_face_id),
|
||||||
|
"source_edge_id": spec.get("source_edge_id", self.selected_edge_id),
|
||||||
|
"selected_kind": self.selected_kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _property_batch_row_for_item(self, item: dict[str, object]) -> int | None:
|
||||||
|
specs = getattr(self, "property_editor_specs", [])
|
||||||
|
target_key = str(item.get("key", ""))
|
||||||
|
target_label = str(item.get("label", ""))
|
||||||
|
target_action = str(item.get("action", ""))
|
||||||
|
target_scope = str(item.get("scope_key", ""))
|
||||||
|
for row, spec in enumerate(specs):
|
||||||
|
if target_key and str(spec.get("key", "")) != target_key:
|
||||||
|
continue
|
||||||
|
if target_label and str(spec.get("label", "")) != target_label:
|
||||||
|
continue
|
||||||
|
modes = spec.get("scope_modes")
|
||||||
|
mode: dict[str, object] | None = None
|
||||||
|
if target_scope and isinstance(modes, dict):
|
||||||
|
candidate_mode = modes.get(target_scope)
|
||||||
|
if not isinstance(candidate_mode, dict):
|
||||||
|
continue
|
||||||
|
mode = dict(candidate_mode)
|
||||||
|
effective = self._effective_property_spec(spec, row=row)
|
||||||
|
if target_action and str(effective.get("action", "")) != target_action:
|
||||||
|
scoped_action = str((mode or {}).get("action", ""))
|
||||||
|
if scoped_action != target_action:
|
||||||
|
continue
|
||||||
|
if target_scope and not isinstance(modes, dict):
|
||||||
|
if str(effective.get("scope_key", "")) not in {"", target_scope}:
|
||||||
|
continue
|
||||||
|
return row
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _set_property_scope_value(self, row: int, scope_key: str) -> bool:
|
||||||
|
if not scope_key or not hasattr(self, "property_table"):
|
||||||
|
return True
|
||||||
|
widget = self.property_table.cellWidget(row, PROPERTY_SCOPE_COLUMN)
|
||||||
|
if not isinstance(widget, NoWheelComboBox):
|
||||||
|
return True
|
||||||
|
index = widget.findData(scope_key)
|
||||||
|
if index < 0:
|
||||||
|
index = widget.findText(scope_key)
|
||||||
|
if index < 0:
|
||||||
|
return False
|
||||||
|
if widget.currentIndex() == index:
|
||||||
|
return True
|
||||||
|
widget.setCurrentIndex(index)
|
||||||
|
return True
|
||||||
|
|
||||||
def _property_target_text(self, row: int) -> str:
|
def _property_target_text(self, row: int) -> str:
|
||||||
if not hasattr(self, "property_table"):
|
if not hasattr(self, "property_table"):
|
||||||
return ""
|
return ""
|
||||||
@@ -6133,11 +6272,100 @@ class WindowStateMixin:
|
|||||||
self.statusBar().showMessage("请先在当前选中对象表中修改一个可编辑目标值。")
|
self.statusBar().showMessage("请先在当前选中对象表中修改一个可编辑目标值。")
|
||||||
return
|
return
|
||||||
if len(changed) > 1:
|
if len(changed) > 1:
|
||||||
QMessageBox.information(self, "一次执行一个修改", "请只修改一行目标值,然后再点击参数化建模。")
|
self._start_property_batch_edit(changed)
|
||||||
return
|
return
|
||||||
row, _spec, _text = changed[0]
|
row, _spec, _text = changed[0]
|
||||||
self.apply_property_row_edit(row)
|
self.apply_property_row_edit(row)
|
||||||
|
|
||||||
|
def _start_property_batch_edit(self, changed: list[tuple[int, dict[str, object], str]]) -> None:
|
||||||
|
if self._edit_busy("请等待当前编辑完成后再执行批量参数化建模。"):
|
||||||
|
return
|
||||||
|
items = [self._property_batch_item_from_row(row, spec, text) for row, spec, text in changed]
|
||||||
|
self.property_batch_queue = items
|
||||||
|
self.property_batch_total = len(items)
|
||||||
|
self.property_batch_done = 0
|
||||||
|
self.property_batch_active = True
|
||||||
|
self.statusBar().showMessage(f"开始批量参数化建模:共 {len(items)} 个参数。")
|
||||||
|
self._run_next_property_batch_item()
|
||||||
|
|
||||||
|
def _run_next_property_batch_item(self) -> None:
|
||||||
|
queue = list(getattr(self, "property_batch_queue", []) or [])
|
||||||
|
if not queue:
|
||||||
|
total = int(getattr(self, "property_batch_total", 0) or 0)
|
||||||
|
self._clear_property_batch_state()
|
||||||
|
self.statusBar().showMessage(f"批量参数化建模完成:已应用 {total} 个参数。")
|
||||||
|
self._update_property_apply_state()
|
||||||
|
return
|
||||||
|
item = queue.pop(0)
|
||||||
|
self.property_batch_queue = queue
|
||||||
|
row = self._property_batch_row_for_item(item)
|
||||||
|
if row is None:
|
||||||
|
self._clear_property_batch_state()
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"批量参数化建模已停止",
|
||||||
|
f"找不到后续参数“{item.get('label', '')}”,模型拓扑可能已变化。已停止后续修改。",
|
||||||
|
)
|
||||||
|
self._update_property_apply_state()
|
||||||
|
return
|
||||||
|
if not self._set_property_scope_value(row, str(item.get("scope_key", ""))):
|
||||||
|
self._clear_property_batch_state()
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"批量参数化建模已停止",
|
||||||
|
f"参数“{item.get('label', '')}”的建模意图已经不可用。已停止后续修改。",
|
||||||
|
)
|
||||||
|
self._update_property_apply_state()
|
||||||
|
return
|
||||||
|
target_widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN)
|
||||||
|
if isinstance(target_widget, QLineEdit):
|
||||||
|
target_widget.setText(str(item.get("target_text", "")))
|
||||||
|
done = int(getattr(self, "property_batch_done", 0) or 0)
|
||||||
|
total = int(getattr(self, "property_batch_total", 0) or 0)
|
||||||
|
self.statusBar().showMessage(f"批量参数化建模:正在执行 {done + 1}/{total} - {item.get('label', '')}")
|
||||||
|
self._property_batch_running_action = True
|
||||||
|
self._property_batch_item_callback_seen = False
|
||||||
|
try:
|
||||||
|
self.apply_property_row_edit(row)
|
||||||
|
finally:
|
||||||
|
self._property_batch_running_action = False
|
||||||
|
if (
|
||||||
|
bool(getattr(self, "property_batch_active", False))
|
||||||
|
and not bool(getattr(self, "_property_batch_item_callback_seen", False))
|
||||||
|
and not bool(getattr(self, "operation_in_progress", False))
|
||||||
|
):
|
||||||
|
remaining = len(getattr(self, "property_batch_queue", []) or [])
|
||||||
|
done = int(getattr(self, "property_batch_done", 0) or 0)
|
||||||
|
self._clear_property_batch_state()
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"批量参数化建模已停止",
|
||||||
|
f"参数“{item.get('label', '')}”没有启动可执行修改。已完成 {done} 个,剩余 {remaining} 个未执行。",
|
||||||
|
)
|
||||||
|
self._update_property_apply_state()
|
||||||
|
|
||||||
|
def _clear_property_batch_state(self) -> None:
|
||||||
|
self.property_batch_queue = []
|
||||||
|
self.property_batch_total = 0
|
||||||
|
self.property_batch_done = 0
|
||||||
|
self.property_batch_active = False
|
||||||
|
self._property_batch_running_action = False
|
||||||
|
self._property_batch_item_callback_seen = False
|
||||||
|
|
||||||
|
def _after_property_edit_finished(self, *, success: bool) -> None:
|
||||||
|
if not bool(getattr(self, "property_batch_active", False)):
|
||||||
|
return
|
||||||
|
self._property_batch_item_callback_seen = True
|
||||||
|
if not success:
|
||||||
|
remaining = len(getattr(self, "property_batch_queue", []) or [])
|
||||||
|
done = int(getattr(self, "property_batch_done", 0) or 0)
|
||||||
|
self._clear_property_batch_state()
|
||||||
|
self.statusBar().showMessage(f"批量参数化建模已停止:已完成 {done} 个,剩余 {remaining} 个未执行。")
|
||||||
|
self._update_property_apply_state()
|
||||||
|
return
|
||||||
|
self.property_batch_done = int(getattr(self, "property_batch_done", 0) or 0) + 1
|
||||||
|
QTimer.singleShot(0, self._run_next_property_batch_item)
|
||||||
|
|
||||||
def _activate_property_source_feature(self, spec: dict[str, object]) -> None:
|
def _activate_property_source_feature(self, spec: dict[str, object]) -> None:
|
||||||
source_face_id = _int_or_none(spec.get("source_face_id"))
|
source_face_id = _int_or_none(spec.get("source_face_id"))
|
||||||
if source_face_id is None or source_face_id == self.selected_face_id:
|
if source_face_id is None or source_face_id == self.selected_face_id:
|
||||||
@@ -6453,6 +6681,41 @@ class WindowStateMixin:
|
|||||||
return current_length * target_radius / current_radius
|
return current_length * target_radius / current_radius
|
||||||
raise ValueError(f"这个属性使用了未知的目标换算方式:{transform}")
|
raise ValueError(f"这个属性使用了未知的目标换算方式:{transform}")
|
||||||
|
|
||||||
|
def _plane_offset_info_for_property(
|
||||||
|
self,
|
||||||
|
action_info: dict[str, object],
|
||||||
|
) -> tuple[tuple[float, float, float] | None, tuple[float, float, float] | None]:
|
||||||
|
plane_origin = _triple_or_none(action_info.get("plane_origin"))
|
||||||
|
plane_direction = _triple_or_none(action_info.get("push_pull_outward_direction"))
|
||||||
|
selected_face_id = _int_or_none(action_info.get("feature_source_face_id"))
|
||||||
|
if selected_face_id is None:
|
||||||
|
selected_face_id = _int_or_none(action_info.get("topological_face_id"))
|
||||||
|
if selected_face_id is None:
|
||||||
|
selected_face_id = _int_or_none(action_info.get("face_id"))
|
||||||
|
model_faces = getattr(self.model, "faces", None) if self.model is not None else None
|
||||||
|
if (
|
||||||
|
self.model is not None
|
||||||
|
and model_faces is not None
|
||||||
|
and selected_face_id is not None
|
||||||
|
and 0 <= selected_face_id < len(model_faces)
|
||||||
|
and (plane_origin is None or plane_direction is None)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
full_info = self.model.face_info(selected_face_id)
|
||||||
|
except Exception:
|
||||||
|
full_info = {}
|
||||||
|
if plane_origin is None:
|
||||||
|
plane_origin = _triple_or_none(full_info.get("plane_origin"))
|
||||||
|
if plane_direction is None:
|
||||||
|
plane_direction = _triple_or_none(full_info.get("push_pull_outward_direction"))
|
||||||
|
if plane_direction is None:
|
||||||
|
plane_direction = _triple_or_none(full_info.get("oriented_normal"))
|
||||||
|
if plane_direction is None:
|
||||||
|
plane_direction = _triple_or_none(action_info.get("oriented_normal"))
|
||||||
|
if plane_direction is None:
|
||||||
|
plane_direction = _triple_or_none(action_info.get("normal"))
|
||||||
|
return plane_origin, plane_direction
|
||||||
|
|
||||||
def _transform_property_vector3_target(
|
def _transform_property_vector3_target(
|
||||||
self,
|
self,
|
||||||
spec: dict[str, object],
|
spec: dict[str, object],
|
||||||
|
|||||||
Reference in New Issue
Block a user