feat: 完善特征编辑和选择稳定性
This commit is contained in:
@@ -8,10 +8,84 @@
|
|||||||
|
|
||||||
- 这是一个 STEP B-Rep 结果几何查看和受限局部编辑工具,不是完整参数化 CAD 内核。
|
- 这是一个 STEP B-Rep 结果几何查看和受限局部编辑工具,不是完整参数化 CAD 内核。
|
||||||
- 主入口是 `python main.py`;默认模型是 `assets/models/geom_extract.step`;立方体测试模型是 `assets/models/cube_10mm.step`。
|
- 主入口是 `python main.py`;默认模型是 `assets/models/geom_extract.step`;立方体测试模型是 `assets/models/cube_10mm.step`。
|
||||||
- 左侧操作面板按工作流排列:STEP 文件、模型结构树、鼠标选择模式、按 ID 选择、编辑、导出、显示、测量、可编辑对象、圆柱面候选、操作历史和对象信息。
|
- 左侧操作面板当前优先保留最小建模链路:STEP 文件、选择模式 / 按 ID 选择、当前选中对象、编辑、参数化建模和导出当前完整 STEP;部分辅助面板暂时收起,后续需要时再放回。
|
||||||
- 局部编辑会先做计划、风险提示、预览和后台执行;失败时会尽量回滚,成功后进入撤销/重做历史。
|
- 局部编辑会先做计划、风险提示、预览和后台执行;失败时会尽量回滚,成功后进入撤销/重做历史。
|
||||||
- 新开 Codex 聊天框继续开发时,只需要 Codex 阅读 README 末尾的“Codex 项目记忆”;普通开发者可以忽略那一节。
|
- 新开 Codex 聊天框继续开发时,只需要 Codex 阅读 README 末尾的“Codex 项目记忆”;普通开发者可以忽略那一节。
|
||||||
|
|
||||||
|
## 怎么运行
|
||||||
|
|
||||||
|
你第一次配置或运行这个项目时,可以按下面步骤来。命令默认在 Windows PowerShell 里执行。
|
||||||
|
|
||||||
|
1. 进入项目目录。如果你的仓库不在这个路径,把命令里的路径换成你的实际目录:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\admin\Desktop\python-occt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 如果你已经有 `pyocc` 环境,直接激活:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda activate pyocc
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 如果你是第一次配置环境,先用仓库里的 `environment.yml` 创建环境,再激活:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda env create -f environment.yml
|
||||||
|
conda activate pyocc
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 先跑一次烟测,确认入口和依赖能正常加载。这个命令不会进入交互窗口:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python main.py --smoke-test
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 启动程序并打开默认测试模型:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 启动时直接打开指定 STEP 文件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python main.py path\to\model.step
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 也可以先启动程序,在左侧 `STEP文件` 区域点击 `导入几何模型` 选择 `.step` / `.stp` 文件,再点击 `读取模型`。
|
||||||
|
|
||||||
|
8. 打开仓库自带的简单立方体测试模型:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python main.py assets\models\cube_10mm.step
|
||||||
|
```
|
||||||
|
|
||||||
|
9. 重新生成简单立方体测试模型:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\generate_cube_step.py
|
||||||
|
```
|
||||||
|
|
||||||
|
10. 验证立方体上的“只改一条 Edge 长度”基线:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\verify_edge_length_resize.py
|
||||||
|
```
|
||||||
|
|
||||||
|
11. 如果当前 PowerShell 没识别 `conda`,你可以直接使用完整路径运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Users\admin\miniforge3\Scripts\conda.exe run -n pyocc python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
开发时常用的本地检查:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m compileall -q main.py step_editor scripts
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
## 最终要实现什么
|
## 最终要实现什么
|
||||||
|
|
||||||
最终目标是做一个面向 STEP 模型的交互式工具:
|
最终目标是做一个面向 STEP 模型的交互式工具:
|
||||||
@@ -22,7 +96,7 @@
|
|||||||
- 用户可以选择零件、Solid、Face、Edge 和几何特征。
|
- 用户可以选择零件、Solid、Face、Edge 和几何特征。
|
||||||
- 用户可以做基础测量,例如把两个拾取点或对象中心设为 A/B 并计算距离。
|
- 用户可以做基础测量,例如把两个拾取点或对象中心设为 A/B 并计算距离。
|
||||||
- 程序可以识别孔、圆角、凸台、槽、壳体局部区域等候选特征。
|
- 程序可以识别孔、圆角、凸台、槽、壳体局部区域等候选特征。
|
||||||
- 用户可以修改可控的特征,比如平面推拉、孔径调整、槽/半孔宽度调整、盲孔深度调整、凸台直径调整、Edge倒圆、Edge倒角和修改Edge长度。
|
- 用户可以修改可控的特征,比如平面推拉和目标法向位置、孔径/半径调整、完整圆柱孔轴心坐标修改、槽/半孔宽度、深度、圆弧长度、圆弧角度、开口角度和槽孔总长度调整、盲孔/盲槽深度调整、凸台直径/半径/高度/轴心坐标调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度调整、圆锥半角、环面直径、Edge倒圆、Edge倒角、圆Edge半径/直径、修改Edge长度和直线Edge起点/终点坐标。
|
||||||
- 用户可以撤销/重做修改,避免一步编辑做坏。
|
- 用户可以撤销/重做修改,避免一步编辑做坏。
|
||||||
- 用户可以导出修改后的完整模型。
|
- 用户可以导出修改后的完整模型。
|
||||||
- 如果 STEP 文件里存在多个互不关联的零件,用户可以只导出选中的某个零件。
|
- 如果 STEP 文件里存在多个互不关联的零件,用户可以只导出选中的某个零件。
|
||||||
@@ -44,6 +118,23 @@
|
|||||||
- 能撤销/重做编辑。
|
- 能撤销/重做编辑。
|
||||||
- 先实现少量可控编辑操作,用于验证后续特征编辑路线。
|
- 先实现少量可控编辑操作,用于验证后续特征编辑路线。
|
||||||
|
|
||||||
|
## 当前可修改范围
|
||||||
|
|
||||||
|
这张表描述的是当前版本已经接入到界面或编辑链路里的能力。它们都是基于 STEP 的 B-Rep 结果几何做局部重建,不等于原 CAD 软件里的完整建模历史参数。
|
||||||
|
|
||||||
|
| 对象 / 特征 | 现在可以修改 | 暂不能保证或还不能修改 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 平面 Face | `Face目标法向位置`、`Face本次偏移量`;识别到相对平面时可以改薄壁厚度。 | `Face面积` 目前只读;自由曲面、复杂壳体局部区域和带复杂边界的面不能直接按面积修改。 |
|
||||||
|
| 圆柱孔 / 圆柱面 | 孔或圆柱直径、半径;完整圆柱孔可以改轴心坐标;完整孔可以尝试封堵。 | 复杂孔、螺纹孔、锥孔、不完整圆柱孔和拓扑不稳定的孔可能失败并回滚。 |
|
||||||
|
| 盲孔 / 盲槽 | 识别到疑似底面 Face 时,可以改 `盲孔/盲槽深度`。 | STEP 不保存真实孔深历史;找不到可靠底面或底面被复杂拓扑切碎时,深度只能只读或需要手动辅助。 |
|
||||||
|
| 槽 / 半孔 | 可以改槽/半孔宽度、深度、圆弧长度、圆弧角度和开口角度;长圆槽识别到成对半圆端时,可以改槽孔总长度和中心距,也可以手动填写配对端 Face ID。 | 复杂草图槽、非圆柱槽、槽的整体迁移/轴心移动、链式槽和跨多个不规则面的槽还不是稳定能力。 |
|
||||||
|
| 圆柱凸台 / 外圆 | 完整圆柱凸台可以改直径、半径、高度和轴心坐标;普通完整圆柱也有高度兜底修改。 | 复杂凸台、异形凸台、多台阶凸台和不完整外圆不能保证稳定重建。 |
|
||||||
|
| 已有圆角 / 倒圆面 | 部分规则圆柱倒圆面可以改半径或圆弧长度,使用先移除旧圆角再重建新圆角的路线。 | 复杂 blend、支撑面不明确、圆角链或恢复锐边失败时会阻止或回滚。 |
|
||||||
|
| Edge | 直线 Edge 可以尝试改目标长度、起点坐标、中心坐标和终点坐标;直线 Edge 可以加倒圆、对称倒角、不等距倒角和距离+角度倒角;圆形/圆弧 Edge 可以尝试改半径或直径。 | “通用任意 Edge”仍是受限实现;复杂曲线、B-spline、和无法确定局部变形区域的 Edge 可能走高风险 fallback 或失败。 |
|
||||||
|
| 圆锥 / 球 / 环面 | 圆锥可以改参考半径、直径或半角;球可以改半径或直径;环面可以改主/副半径或直径。 | 这些属于几何缩放或局部重建能力,不是原始特征历史恢复;复杂相邻拓扑可能失败。 |
|
||||||
|
| Solid / 特征整体 | 可以对选中 Solid 或当前单一模型做平移、旋转和按包围盒尺寸 / 体积 / 表面积的缩放类修改。 | 还没有装配约束、零件间关联约束或完整多零件装配编辑。 |
|
||||||
|
| 只读信息 | 面积、体积、包围盒、曲面类型、相邻关系、候选特征说明等会展示出来帮助判断。 | 标成只读的值不会被 `参数化建模` 修改;显示、测量和导出不是建模操作。 |
|
||||||
|
|
||||||
## 现在已经实现了什么
|
## 现在已经实现了什么
|
||||||
|
|
||||||
当前已经实现:
|
当前已经实现:
|
||||||
@@ -124,14 +215,16 @@
|
|||||||
- `深度扫描` 会尝试列出更多候选,适合默认列表里没有找到目标对象时使用。
|
- `深度扫描` 会尝试列出更多候选,适合默认列表里没有找到目标对象时使用。
|
||||||
- 可推拉的平面Face。
|
- 可推拉的平面Face。
|
||||||
- 可调整孔径的圆柱Face候选。
|
- 可调整孔径的圆柱Face候选。
|
||||||
- 可调整槽/半孔宽度的局部圆柱孔/槽候选。
|
- 可调整槽/半孔宽度、深度和圆弧长度的局部圆柱孔/槽候选。
|
||||||
- 可调整盲孔深度的圆柱Face候选。
|
- 可调整盲孔/盲槽深度的圆柱Face候选。
|
||||||
|
- 可调整圆柱凸台直径和高度的凸台候选。
|
||||||
- 可添加新圆角的直线Edge候选。
|
- 可添加新圆角的直线Edge候选。
|
||||||
- 可添加倒角的直线Edge候选。
|
- 可添加倒角的直线Edge候选。
|
||||||
- 可尝试直接调整长度的直线Edge候选,简单平面多面体会优先局部边形变。
|
- 可尝试直接调整长度的直线Edge候选,简单平面多面体会优先局部边形变。
|
||||||
|
- 可调整圆Edge半径/直径的圆形或圆弧Edge候选。
|
||||||
- 表格会显示当前值、状态、风险、置信度和说明。
|
- 表格会显示当前值、状态、风险、置信度和说明。
|
||||||
- 盲孔深度候选会在列表阶段先确认疑似底面,列表里的深度值优先使用底面Face轴向位置计算。
|
- 盲孔/盲槽深度候选会在列表阶段先确认疑似底面,列表里的深度值优先使用底面Face轴向位置计算。
|
||||||
- 点击表格行会直接选中对应Face或Edge,方便继续执行推拉、孔径调整、槽/半孔宽度调整、盲孔深度调整、Edge倒圆、Edge倒角或修改Edge长度。
|
- 点击表格行会直接选中对应Face或Edge,方便继续执行推拉、孔径调整、槽/半孔宽度/深度/圆弧长度调整、凸台高度调整、盲孔/盲槽深度调整、Edge倒圆、Edge倒角或修改Edge长度。
|
||||||
- 支持列出圆柱面候选特征。
|
- 支持列出圆柱面候选特征。
|
||||||
- 圆柱面候选会显示在表格中,点击候选行可以直接选中并高亮对应 Face。
|
- 圆柱面候选会显示在表格中,点击候选行可以直接选中并高亮对应 Face。
|
||||||
- 圆柱面候选可以按类型筛选:全部、孔/槽候选、圆角候选、凸柱/外圆候选、未明确分类。
|
- 圆柱面候选可以按类型筛选:全部、孔/槽候选、圆角候选、凸柱/外圆候选、未明确分类。
|
||||||
@@ -172,6 +265,22 @@
|
|||||||
- 只对已识别为 `partial-cylindrical-groove` 的部分圆柱孔/槽候选开放。
|
- 只对已识别为 `partial-cylindrical-groove` 的部分圆柱孔/槽候选开放。
|
||||||
- 输入目标槽宽后,会按当前圆弧角度把槽宽换算为目标圆柱直径,再复用圆柱孔/槽重建路线执行。
|
- 输入目标槽宽后,会按当前圆弧角度把槽宽换算为目标圆柱直径,再复用圆柱孔/槽重建路线执行。
|
||||||
- 这不是完整 CAD 槽参数编辑;复杂槽、非圆柱槽或圆弧角度不稳定时仍可能失败并回滚。
|
- 这不是完整 CAD 槽参数编辑;复杂槽、非圆柱槽或圆弧角度不稳定时仍可能失败并回滚。
|
||||||
|
- 当前版本支持槽/半孔深度调整:
|
||||||
|
- 只对已识别为 `partial-cylindrical-groove` 的部分圆柱孔/槽候选开放。
|
||||||
|
- 输入目标槽深后,会按当前圆弧角度把槽深换算为目标圆柱直径,再复用圆柱孔/槽重建路线执行。
|
||||||
|
- 这仍是 B-Rep 几何近似,不是完整 CAD 槽深参数编辑。
|
||||||
|
- 当前版本支持槽/半孔圆弧长度调整:
|
||||||
|
- 只对已识别为 `partial-cylindrical-groove` 的部分圆柱孔/槽候选开放。
|
||||||
|
- 输入目标圆弧长度后,会按当前圆弧角度把圆弧长度换算为目标圆柱直径,再复用圆柱孔/槽重建路线执行。
|
||||||
|
- 这仍是 B-Rep 几何近似,不是完整 CAD 槽参数编辑。
|
||||||
|
- 当前版本支持槽/半孔圆弧角度和开口角度调整:
|
||||||
|
- 只对已识别为 `partial-cylindrical-groove` 的部分圆柱孔/槽候选开放。
|
||||||
|
- 输入目标圆弧角度时,程序会保持当前圆柱半径,先补旧扇形槽,再按目标角度重切局部扇形槽。
|
||||||
|
- 输入目标开口角度时,会先换算为 `2π - 开口角` 的圆弧角度后执行。
|
||||||
|
- 如果检测到成对长圆槽端,当前角度修改仍只重建所选局部扇形槽。
|
||||||
|
- 当前版本支持长圆槽/槽孔总长度和中心距调整:
|
||||||
|
- 自动识别到成对半圆槽端时,程序会保持槽宽不变,先补旧长圆槽体,再切出目标总长度。
|
||||||
|
- 如果自动配对失败,可以在当前选中对象表中填写 `槽孔配对端Face ID`,后续槽宽、槽深、圆弧长度和总长度调整会优先尝试使用这个手动配对端。
|
||||||
- 当前版本支持薄壁/壳体局部厚度调整:
|
- 当前版本支持薄壁/壳体局部厚度调整:
|
||||||
- 对已找到投影重叠相对平面的平面候选开放。
|
- 对已找到投影重叠相对平面的平面候选开放。
|
||||||
- 输入目标厚度后,程序会把厚度变化换算成当前平面区域的推拉距离,保留相对平面不动。
|
- 输入目标厚度后,程序会把厚度变化换算成当前平面区域的推拉距离,保留相对平面不动。
|
||||||
@@ -183,7 +292,7 @@
|
|||||||
- 会识别已有圆角Face和相邻支撑Face,并在特征模式下一起高亮。
|
- 会识别已有圆角Face和相邻支撑Face,并在特征模式下一起高亮。
|
||||||
- `可编辑对象` 会把已有圆角候选单独列为 `修改已有圆角半径`,点击后会选中该圆角 Face,并把圆角半径输入框预填为当前估算半径的参考目标值。
|
- `可编辑对象` 会把已有圆角候选单独列为 `修改已有圆角半径`,点击后会选中该圆角 Face,并把圆角半径输入框预填为当前估算半径的参考目标值。
|
||||||
- 选中已有圆角候选时,不会再误把它填入 `孔直径`,避免把圆角当孔径修改。
|
- 选中已有圆角候选时,不会再误把它填入 `孔直径`,避免把圆角当孔径修改。
|
||||||
- 当前版本的已有圆角半径修改会先尝试移除圆角Face,再在恢复出的锐边上重新倒圆;只对较规整的圆柱圆角候选开放,复杂 blend 可能失败并回滚。
|
- 当前版本的已有圆角半径修改会先尝试移除圆角Face,再在恢复出的锐边上重新倒圆;也可以在当前选中对象表中输入已有圆角圆弧长度,程序会按当前圆弧角度换算成目标半径。只对较规整的圆柱圆角候选开放,复杂 blend 可能失败并回滚。
|
||||||
- 当前版本支持圆柱孔封堵:
|
- 当前版本支持圆柱孔封堵:
|
||||||
- 只对 `hole/groove candidate` 且角度跨度接近完整圆柱的孔开放。
|
- 只对 `hole/groove candidate` 且角度跨度接近完整圆柱的孔开放。
|
||||||
- 不对半孔/槽开放,避免用完整圆柱补料时把不该补的位置也补上。
|
- 不对半孔/槽开放,避免用完整圆柱补料时把不该补的位置也补上。
|
||||||
@@ -197,6 +306,11 @@
|
|||||||
- 目标直径变小时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建,避免环形 cutter 在部分场景下把实体切空。
|
- 目标直径变小时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建,避免环形 cutter 在部分场景下把实体切空。
|
||||||
- 会先显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。
|
- 会先显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。
|
||||||
- 当前默认模型可能没有明确凸台候选;包含完整圆柱凸台的 STEP 文件可以使用这个能力。
|
- 当前默认模型可能没有明确凸台候选;包含完整圆柱凸台的 STEP 文件可以使用这个能力。
|
||||||
|
- 当前版本支持圆柱凸台高度和轴心坐标调整:
|
||||||
|
- 只对已识别为完整圆柱凸台、并找到可推拉端盖 Face 的候选开放。
|
||||||
|
- 输入目标高度后,程序会选择凸台端盖并把高度变化换算成端盖推拉距离。
|
||||||
|
- 输入目标轴心坐标后,程序会先移除旧凸台包络,再按同直径在目标轴心补出凸台。
|
||||||
|
- 这不是 CAD 历史里的拉伸特征高度参数;端盖识别不稳定或推拉计划被阻止时会拒绝执行。
|
||||||
- 当前版本支持直线Edge添加圆角:
|
- 当前版本支持直线Edge添加圆角:
|
||||||
- 只对直线Edge开放。
|
- 只对直线Edge开放。
|
||||||
- 会先检查边长、相邻Face数量和目标半径相对边长的比例。
|
- 会先检查边长、相邻Face数量和目标半径相对边长的比例。
|
||||||
@@ -207,14 +321,18 @@
|
|||||||
- 当前版本支持直线Edge添加倒角:
|
- 当前版本支持直线Edge添加倒角:
|
||||||
- 只对直线Edge开放。
|
- 只对直线Edge开放。
|
||||||
- 会先检查边长、相邻Face数量和倒角距离相对边长的比例。
|
- 会先检查边长、相邻Face数量和倒角距离相对边长的比例。
|
||||||
|
- 当前选中对象表支持 `新增倒角距离` 的对称距离倒角、`新增不等距倒角D1/D2` 的两侧不同距离倒角,以及 `新增距离+角度倒角D/角度`。
|
||||||
- 橙色半透明预览表示目标Edge和输入倒角距离的大致范围。
|
- 橙色半透明预览表示目标Edge和输入倒角距离的大致范围。
|
||||||
- 真实编辑使用 OCCT `BRepFilletAPI_MakeChamfer` 在后台执行。
|
- 真实编辑使用 OCCT `BRepFilletAPI_MakeChamfer` 在后台执行。
|
||||||
- STEP 没有建模历史,某些边仍可能被 OCCT 拒绝;失败时会自动尝试恢复编辑前状态。
|
- STEP 没有建模历史,某些边仍可能被 OCCT 拒绝;失败时会自动尝试恢复编辑前状态。
|
||||||
- 当前版本支持修改Edge长度:
|
- 当前版本支持修改Edge长度:
|
||||||
- 选中Edge后,`Edge目标长度` 会自动填入当前Edge长度。
|
- 选中Edge后,`Edge目标长度` 会自动填入当前Edge长度。
|
||||||
|
- 对圆形/圆弧Edge,当前选中对象表会额外提供圆Edge半径和圆Edge直径可编辑行,内部会换算为等比例Edge目标长度。
|
||||||
- 可以选择Edge长度基准:自动、中心、固定起点、固定终点。
|
- 可以选择Edge长度基准:自动、中心、固定起点、固定终点。
|
||||||
|
- 对直线Edge,当前选中对象表会额外提供 `Edge起点坐标`、`Edge中心坐标` 和 `Edge终点坐标`,可以直接输入目标 X/Y/Z 坐标来移动单个端点或整条边。
|
||||||
- 对正方体这类全平面多面体,直线Edge会优先走“局部边形变”:只移动被选Edge的一个端点或两个端点,并重建周边平面。
|
- 对正方体这类全平面多面体,直线Edge会优先走“局部边形变”:只移动被选Edge的一个端点或两个端点,并重建周边平面。
|
||||||
- 局部边形变不会把整个端面拉走,因此正方体不会默认变成长方体;相邻面会按被移动的边自然变成梯形或拆成三角面。
|
- 局部边形变不会把整个端面拉走,因此正方体不会默认变成长方体;相邻面会按被移动的边自然变成梯形或拆成三角面。
|
||||||
|
- 直线Edge端点/中心坐标修改复用同一套局部边形变;当前只对简单全平面多面体开放,复杂曲面、带内孔Face或大型复杂Solid会阻止执行。
|
||||||
- `自动` 默认固定起点、移动终点;`固定起点` / `固定终点` 会明确控制哪一端不动;`中心` 会让两端各移动一半。
|
- `自动` 默认固定起点、移动终点;`固定起点` / `固定终点` 会明确控制哪一端不动;`中心` 会让两端各移动一半。
|
||||||
- 如果模型不是简单全平面多面体,直线Edge会继续尝试寻找端点附近、方向匹配的平面端面,并把“改边长”转换成该端面的平面推拉操作。
|
- 如果模型不是简单全平面多面体,直线Edge会继续尝试寻找端点附近、方向匹配的平面端面,并把“改边长”转换成该端面的平面推拉操作。
|
||||||
- 圆形/圆弧Edge会优先检查相邻圆柱面;如果能复用孔/槽或凸台直径编辑,就把Edge目标长度按比例换算成目标圆柱直径再执行局部编辑。
|
- 圆形/圆弧Edge会优先检查相邻圆柱面;如果能复用孔/槽或凸台直径编辑,就把Edge目标长度按比例换算成目标圆柱直径再执行局部编辑。
|
||||||
@@ -243,6 +361,7 @@
|
|||||||
- 如果后台编辑失败,程序会自动尝试恢复到编辑前快照,避免出现“提示失败但模型已经被部分改动”的状态。
|
- 如果后台编辑失败,程序会自动尝试恢复到编辑前快照,避免出现“提示失败但模型已经被部分改动”的状态。
|
||||||
- 当前版本支持圆柱孔/圆柱面扩大切削。
|
- 当前版本支持圆柱孔/圆柱面扩大切削。
|
||||||
- 当前版本支持圆柱孔/圆柱面缩小孔径:先补料,再用目标直径重切。
|
- 当前版本支持圆柱孔/圆柱面缩小孔径:先补料,再用目标直径重切。
|
||||||
|
- 当前版本支持完整圆柱孔轴心坐标修改:在当前选中对象表里修改 `孔轴心坐标`,程序会先补旧孔,再按同直径在目标轴心位置重切新孔;半孔/开放槽暂不放行这条路线,避免整圆柱工具误补材料。
|
||||||
- 圆柱孔/圆柱面孔径调整现在会先生成切削计划:
|
- 圆柱孔/圆柱面孔径调整现在会先生成切削计划:
|
||||||
- 目标直径必须大于 0;如果目标直径与当前直径几乎相同,会直接阻止。
|
- 目标直径必须大于 0;如果目标直径与当前直径几乎相同,会直接阻止。
|
||||||
- 会计算直径变化比例;变化超过当前直径 35% 会提高到中风险,超过 100% 会提高到高风险。
|
- 会计算直径变化比例;变化超过当前直径 35% 会提高到中风险,超过 100% 会提高到高风险。
|
||||||
@@ -277,7 +396,7 @@
|
|||||||
- 鼠标拾取模型时会显示三维拾取点坐标。
|
- 鼠标拾取模型时会显示三维拾取点坐标。
|
||||||
- 编辑历史会记录目标对象和当时的拾取点。
|
- 编辑历史会记录目标对象和当时的拾取点。
|
||||||
- 支持把本软件加载后的编辑历史导出为 JSON v2;除摘要和详情外,也会保存 `operation_name`、`target`、`parameters` 和 `result_message`,为后续安全复放做准备。
|
- 支持把本软件加载后的编辑历史导出为 JSON v2;除摘要和详情外,也会保存 `operation_name`、`target`、`parameters` 和 `result_message`,为后续安全复放做准备。
|
||||||
- 点击操作历史记录时,程序会尝试重新高亮目标对象,并显示当时的拾取点标记。
|
- 点击操作历史记录时,程序会尝试重新高亮目标对象,并在详情里保留当时的拾取点坐标;3D 视图里不会再额外显示拾取点小球。
|
||||||
- 对非 `hole/groove candidate` 的圆柱切削会先弹出确认,避免误切外圆角或凸柱。
|
- 对非 `hole/groove candidate` 的圆柱切削会先弹出确认,避免误切外圆角或凸柱。
|
||||||
- 提供 `--smoke-test` 启动检查,用于验证 Qt + VTK 窗口组件能正常初始化。
|
- 提供 `--smoke-test` 启动检查,用于验证 Qt + VTK 窗口组件能正常初始化。
|
||||||
|
|
||||||
@@ -302,7 +421,7 @@ vertices: 3262
|
|||||||
|
|
||||||
- 复杂特征识别:已经有平面、圆柱、边、孔/槽候选、槽/半孔候选、已有圆角候选、圆柱凸台候选等基础候选列表,但还不是完整 CAD 语义级识别。
|
- 复杂特征识别:已经有平面、圆柱、边、孔/槽候选、槽/半孔候选、已有圆角候选、圆柱凸台候选等基础候选列表,但还不是完整 CAD 语义级识别。
|
||||||
- 盲孔/盲槽深度:已经能对部分可识别候选做深度估算和编辑,并会优先使用疑似底面Face的轴向位置计算当前深度;但还不能保证所有模型都能恢复真实 CAD 语义深度。
|
- 盲孔/盲槽深度:已经能对部分可识别候选做深度估算和编辑,并会优先使用疑似底面Face的轴向位置计算当前深度;但还不能保证所有模型都能恢复真实 CAD 语义深度。
|
||||||
- 槽、凸台、壳体局部区域:已经有圆柱孔、槽/半孔候选结构化识别、槽/半孔宽度调整、完整圆柱凸台直径修改,以及平面薄壁/壳体相对面厚度估算和当前版本厚度调整;复杂槽、复杂凸台和完整壳体区域仍需要继续做。
|
- 槽、凸台、壳体局部区域:已经有圆柱孔、槽/半孔候选结构化识别、槽/半孔宽度/深度/圆弧长度/圆弧角度/开口角度调整、长圆槽总长度/中心距调整和手动配对端 Face ID、完整圆柱凸台直径/半径/高度/轴心坐标修改、普通完整圆柱高度兜底修改,以及平面薄壁/壳体相对面厚度估算和当前版本厚度调整;复杂槽、复杂凸台和完整壳体区域仍需要继续做。
|
||||||
- 已有圆角/倒圆面半径:已经有当前版本的受限修改实现,使用 defeature + refillet 路线,但复杂 blend、支撑面不明确或恢复锐边失败时仍会阻止或回滚。
|
- 已有圆角/倒圆面半径:已经有当前版本的受限修改实现,使用 defeature + refillet 路线,但复杂 blend、支撑面不明确或恢复锐边失败时仍会阻止或回滚。
|
||||||
- 局部偏差分析:已经有编辑前后体积、面积、包围盒、距离热力图和差异报告,但还不是工程公差级的局部区域偏差报告。
|
- 局部偏差分析:已经有编辑前后体积、面积、包围盒、距离热力图和差异报告,但还不是工程公差级的局部区域偏差报告。
|
||||||
- 错误恢复和模型修复:已经有编辑前快照、失败回滚、基础 B-Rep 校验、同域面合并、布尔结果自动修复,以及一键修复当前模型或选中零件/solid,但还不是完整的模型修复流水线。
|
- 错误恢复和模型修复:已经有编辑前快照、失败回滚、基础 B-Rep 校验、同域面合并、布尔结果自动修复,以及一键修复当前模型或选中零件/solid,但还不是完整的模型修复流水线。
|
||||||
@@ -318,75 +437,12 @@ vertices: 3262
|
|||||||
建议下一步按这个顺序推进:
|
建议下一步按这个顺序推进:
|
||||||
|
|
||||||
1. 继续补齐更多最小可用特征入口,优先覆盖复杂槽、复杂凸台、完整壳体区域和装配用例。
|
1. 继续补齐更多最小可用特征入口,优先覆盖复杂槽、复杂凸台、完整壳体区域和装配用例。
|
||||||
2. 继续验证平面推拉、孔径调整、槽/半孔宽度调整、薄壁厚度调整、盲孔/盲槽深度调整、圆柱凸台直径调整和直接边长调整,记录哪些对象稳定、哪些对象会失败。
|
2. 继续验证平面推拉、孔径/半径调整、槽/半孔宽度/深度/圆弧长度调整、薄壁厚度调整、盲孔/盲槽深度调整、圆柱凸台直径/半径/高度/轴心坐标调整和直接边长调整,记录哪些对象稳定、哪些对象会失败。
|
||||||
3. 改进盲孔/槽底面识别,减少“看起来像 blind 但找不到可靠底面Face”的候选。
|
3. 改进盲孔/槽底面识别,减少“看起来像 blind 但找不到可靠底面Face”的候选。
|
||||||
4. 继续加固已有圆角半径修改:扩大可成功场景,记录哪些圆角能 defeature + refillet,哪些需要更复杂的重建策略。
|
4. 继续加固已有圆角半径修改:扩大可成功场景,记录哪些圆角能 defeature + refillet,哪些需要更复杂的重建策略。
|
||||||
5. 完善本软件编辑历史的导入/复放能力;当前 JSON v2 已保存机器可读的动作名和参数,但自动复放还需要按操作类型逐步加安全规则。
|
5. 完善本软件编辑历史的导入/复放能力;当前 JSON v2 已保存机器可读的动作名和参数,但自动复放还需要按操作类型逐步加安全规则。
|
||||||
6. 继续做性能和稳定性优化,重点是扫描分批刷新、局部显示重建缓存,以及高风险 OCC 操作隔离。
|
6. 继续做性能和稳定性优化,重点是扫描分批刷新、局部显示重建缓存,以及高风险 OCC 操作隔离。
|
||||||
|
|
||||||
## 怎么运行
|
|
||||||
|
|
||||||
先打开 PowerShell,进入项目目录:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd C:\Users\admin\Desktop\python-occt
|
|
||||||
```
|
|
||||||
|
|
||||||
激活环境:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
conda activate pyocc
|
|
||||||
```
|
|
||||||
|
|
||||||
如果是在一台新电脑上第一次配置环境,可以使用仓库里的 `environment.yml` 创建环境:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
conda env create -f environment.yml
|
|
||||||
conda activate pyocc
|
|
||||||
```
|
|
||||||
|
|
||||||
运行默认模型:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
打开指定 STEP 文件:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python main.py path\to\model.step
|
|
||||||
```
|
|
||||||
|
|
||||||
打开简单立方体测试模型:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python main.py assets\models\cube_10mm.step
|
|
||||||
```
|
|
||||||
|
|
||||||
重新生成简单立方体测试模型:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python scripts\generate_cube_step.py
|
|
||||||
```
|
|
||||||
|
|
||||||
如果当前 PowerShell 没识别 `conda`,可以直接使用完整路径运行:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
C:\Users\admin\miniforge3\Scripts\conda.exe run -n pyocc python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
启动烟测,不进入交互窗口:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python main.py --smoke-test
|
|
||||||
```
|
|
||||||
|
|
||||||
验证立方体上的“只改一条 Edge 长度”基线:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python scripts\verify_edge_length_resize.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 怎么使用
|
## 怎么使用
|
||||||
|
|
||||||
打开程序后,右侧是 3D 模型窗口,左侧是操作面板。
|
打开程序后,右侧是 3D 模型窗口,左侧是操作面板。
|
||||||
@@ -459,10 +515,10 @@ python scripts\verify_edge_length_resize.py
|
|||||||
- 当前默认显示一批代表性的可编辑对象,详细几何判断会尽量延后到选中对象或执行编辑前,避免扫描过慢。
|
- 当前默认显示一批代表性的可编辑对象,详细几何判断会尽量延后到选中对象或执行编辑前,避免扫描过慢。
|
||||||
- `推拉平面` 行表示这个Face是平面,可以配合 `Face偏移` 和 `推拉平面` 使用。
|
- `推拉平面` 行表示这个Face是平面,可以配合 `Face偏移` 和 `推拉平面` 使用。
|
||||||
- `调整圆柱孔径` 行表示这个 Face 是圆柱面候选,可以配合 `孔直径` 和 `调整圆柱孔径` 使用。
|
- `调整圆柱孔径` 行表示这个 Face 是圆柱面候选,可以配合 `孔直径` 和 `调整圆柱孔径` 使用。
|
||||||
- `调整槽/半孔宽度` 行表示这个Face是槽/半孔候选,可以配合 `槽/半孔宽度` 和 `调整槽/半孔宽度` 使用。
|
- `调整槽/半孔宽度` / `调整槽/半孔深度` / `调整槽/半孔圆弧长度` 行表示这个Face是槽/半孔候选,可以在当前选中对象表里修改槽宽、槽深或圆弧长度。
|
||||||
- `调整圆柱凸台直径` 行表示这个Face是较明确的完整圆柱凸台候选,可以配合 `凸台直径` 和 `调整圆柱凸台直径` 使用。
|
- `调整圆柱凸台直径` / `调整圆柱凸台高度` 行表示这个Face是较明确的完整圆柱凸台候选,可以在当前选中对象表里修改凸台直径、半径、高度或轴心坐标。
|
||||||
- `封堵圆柱孔` 行表示这个Face是接近完整圆柱的孔候选,可以点击后直接使用 `封堵圆柱孔`。
|
- `封堵圆柱孔` 行表示这个Face是接近完整圆柱的孔候选,可以点击后直接使用 `封堵圆柱孔`。
|
||||||
- `调整盲孔深度` 行表示这个Face是较明确的盲孔/盲槽候选,可以配合 `孔深度` 和 `调整盲孔深度` 使用。
|
- `调整盲孔/盲槽深度` 行表示这个Face是较明确的盲孔/盲槽候选,可以在当前选中对象表里修改盲孔/盲槽深度。
|
||||||
- `给Edge添加圆角` 行表示这个对象是直线Edge,可以配合 `圆角半径` 和 `给Edge添加圆角` 使用。
|
- `给Edge添加圆角` 行表示这个对象是直线Edge,可以配合 `圆角半径` 和 `给Edge添加圆角` 使用。
|
||||||
- `给Edge添加倒角` 行表示这个对象是直线Edge,可以配合 `倒角距离` 和 `给Edge添加倒角` 使用。
|
- `给Edge添加倒角` 行表示这个对象是直线Edge,可以配合 `倒角距离` 和 `给Edge添加倒角` 使用。
|
||||||
- `修改已有圆角半径` 行表示这个Face是已有圆角/倒圆候选,点击后会切到 `特征` 模式并预填圆角半径参考值;真正执行需要再点击编辑区里的 `修改已有圆角半径` 按钮并确认。
|
- `修改已有圆角半径` 行表示这个Face是已有圆角/倒圆候选,点击后会切到 `特征` 模式并预填圆角半径参考值;真正执行需要再点击编辑区里的 `修改已有圆角半径` 按钮并确认。
|
||||||
@@ -546,6 +602,23 @@ python scripts\verify_edge_length_resize.py
|
|||||||
- 输入目标槽宽后点击 `调整槽/半孔宽度`。
|
- 输入目标槽宽后点击 `调整槽/半孔宽度`。
|
||||||
- 程序会用当前圆弧角度把目标槽宽换算成目标圆柱直径,再复用圆柱孔/槽调整流程。
|
- 程序会用当前圆弧角度把目标槽宽换算成目标圆柱直径,再复用圆柱孔/槽调整流程。
|
||||||
- 这一步仍是 B-Rep 几何近似;复杂槽、非圆柱槽或圆弧角度不稳定时可能失败并回滚。
|
- 这一步仍是 B-Rep 几何近似;复杂槽、非圆柱槽或圆弧角度不稳定时可能失败并回滚。
|
||||||
|
- `槽/半孔深度`:
|
||||||
|
- 先选择一个槽/半孔候选 Face。
|
||||||
|
- 在当前选中对象表里修改 `槽/半孔深度`,点击该行的 `可修改` 或点击 `参数化建模`。
|
||||||
|
- 程序会按当前圆弧角度把目标槽深换算成目标圆柱直径,再复用圆柱孔/槽调整流程。
|
||||||
|
- `槽/半孔圆弧长度`:
|
||||||
|
- 先选择一个槽/半孔候选 Face。
|
||||||
|
- 在当前选中对象表里修改 `槽/半孔圆弧长度`,点击该行的 `可修改` 或点击 `参数化建模`。
|
||||||
|
- 程序会按当前圆弧角度把目标圆弧长度换算成目标圆柱直径,再复用圆柱孔/槽调整流程。
|
||||||
|
- `槽/半孔圆弧角度` / `槽/半孔开口角度`:
|
||||||
|
- 先选择一个槽/半孔候选 Face。
|
||||||
|
- 在当前选中对象表里修改角度值,角度输入单位是度。
|
||||||
|
- 圆弧角度会直接换算成局部扇形槽目标角度;开口角度会换算成 `2π - 开口角`。
|
||||||
|
- 程序会保持当前圆柱半径,先补旧扇形槽,再按目标角度重切局部扇形槽。
|
||||||
|
- `槽孔配对端Face ID` / `槽孔总长度` / `槽孔中心距`:
|
||||||
|
- 对长圆槽或成对半圆槽,程序会自动寻找另一个槽端。
|
||||||
|
- 如果自动配对失败,可以先在 `槽孔配对端Face ID` 行填入另一个半圆端的 Face ID 并点击 `设置`。
|
||||||
|
- 修改 `槽孔总长度` 或 `槽孔中心距` 时,会保持槽宽不变并重建完整长圆槽体。
|
||||||
- `薄壁厚度` + `调整薄壁厚度`:
|
- `薄壁厚度` + `调整薄壁厚度`:
|
||||||
- 先选择一个平面 Face,建议从 `可编辑对象` 中点击 `调整薄壁厚度` 行进入。
|
- 先选择一个平面 Face,建议从 `可编辑对象` 中点击 `调整薄壁厚度` 行进入。
|
||||||
- 选中符合条件的薄壁/壳体平面后,`薄壁厚度` 会自动填入一个比当前估算厚度略大的建议值。
|
- 选中符合条件的薄壁/壳体平面后,`薄壁厚度` 会自动填入一个比当前估算厚度略大的建议值。
|
||||||
@@ -560,6 +633,14 @@ python scripts\verify_edge_length_resize.py
|
|||||||
- 目标直径小于当前直径时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建。
|
- 目标直径小于当前直径时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建。
|
||||||
- 点击后会显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。
|
- 点击后会显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。
|
||||||
- 这不是 CAD 历史里的“修改凸台特征参数”,而是基于当前 B-Rep 的受限几何布尔编辑。
|
- 这不是 CAD 历史里的“修改凸台特征参数”,而是基于当前 B-Rep 的受限几何布尔编辑。
|
||||||
|
- `凸台高度`:
|
||||||
|
- 先选择一个完整圆柱凸台 Face。
|
||||||
|
- 在当前选中对象表里修改 `凸台高度`,点击该行的 `可修改` 或点击 `参数化建模`。
|
||||||
|
- 程序会尝试找到凸台外端盖 Face,并通过推拉端盖改变高度。
|
||||||
|
- `凸台轴心坐标`:
|
||||||
|
- 先选择一个完整圆柱凸台 Face。
|
||||||
|
- 在当前选中对象表里修改 `凸台轴心坐标`,点击该行的 `应用` 或点击 `参数化建模`。
|
||||||
|
- 程序会切掉旧凸台包络,再按原直径和原高度范围在目标轴心位置补出新凸台;目标位置离基体太远时风险会升高。
|
||||||
- `封堵圆柱孔`:
|
- `封堵圆柱孔`:
|
||||||
- 先选择一个完整圆柱孔 Face,建议从 `可编辑对象` 中点击 `封堵圆柱孔` 行进入。
|
- 先选择一个完整圆柱孔 Face,建议从 `可编辑对象` 中点击 `封堵圆柱孔` 行进入。
|
||||||
- 点击 `封堵圆柱孔` 后,程序会先生成补料计划。
|
- 点击 `封堵圆柱孔` 后,程序会先生成补料计划。
|
||||||
@@ -591,6 +672,7 @@ python scripts\verify_edge_length_resize.py
|
|||||||
- 先切换到 `特征` 选择模式并选择一个已有圆角/倒圆候选 Face,或从 `可编辑对象` 中点击 `修改已有圆角半径` 行进入。
|
- 先切换到 `特征` 选择模式并选择一个已有圆角/倒圆候选 Face,或从 `可编辑对象` 中点击 `修改已有圆角半径` 行进入。
|
||||||
- 选中已有圆角候选后,`圆角半径` 会自动填入一个基于当前估算半径的参考目标值。
|
- 选中已有圆角候选后,`圆角半径` 会自动填入一个基于当前估算半径的参考目标值。
|
||||||
- 输入目标半径后点击 `修改已有圆角半径`。
|
- 输入目标半径后点击 `修改已有圆角半径`。
|
||||||
|
- 也可以在当前选中对象表里修改 `已有圆角圆弧长度`,程序会按当前圆弧角度换算成目标半径后执行同一条修改路线。
|
||||||
- 程序会先检查该Face是否为 `round/fillet candidate`、是否识别到至少两个支撑Face、目标半径是否有效,以及当前零件是否为单Solid。
|
- 程序会先检查该Face是否为 `round/fillet candidate`、是否识别到至少两个支撑Face、目标半径是否有效,以及当前零件是否为单Solid。
|
||||||
- 通过检查后,会显示蓝色半透明预览,表示将尝试移除并重建的已有圆角面。
|
- 通过检查后,会显示蓝色半透明预览,表示将尝试移除并重建的已有圆角面。
|
||||||
- 真实编辑会先用 OCCT defeaturing 移除已有圆角面,再从恢复出的候选锐边中寻找可重新倒圆的边并调用 OCCT 倒圆 API。
|
- 真实编辑会先用 OCCT defeaturing 移除已有圆角面,再从恢复出的候选锐边中寻找可重新倒圆的边并调用 OCCT 倒圆 API。
|
||||||
@@ -599,19 +681,26 @@ python scripts\verify_edge_length_resize.py
|
|||||||
- 先切换到 `Edge` 选择模式并选择一条直线Edge,或从 `可编辑对象` 中点击 `给Edge添加倒角` 行进入。
|
- 先切换到 `Edge` 选择模式并选择一条直线Edge,或从 `可编辑对象` 中点击 `给Edge添加倒角` 行进入。
|
||||||
- 选中直线Edge后,`倒角距离` 会自动填入一个较小的建议距离。
|
- 选中直线Edge后,`倒角距离` 会自动填入一个较小的建议距离。
|
||||||
- 输入目标倒角距离后点击 `给Edge添加倒角`。
|
- 输入目标倒角距离后点击 `给Edge添加倒角`。
|
||||||
|
- 如果需要两侧不同距离,在当前选中对象表里填写 `新增不等距倒角D1/D2`,格式为 `D1, D2`。
|
||||||
|
- 如果需要距离+角度倒角,在当前选中对象表里填写 `新增距离+角度倒角D/角度`,格式为 `距离, 角度`,角度单位是度。
|
||||||
|
- `倒角参考Face ID` 决定 D1/D2 的方向;默认使用当前Edge的第一个相邻Face,也可以改成另一个相邻Face后点击 `设置`。
|
||||||
- 程序会先检查倒角距离是否大于 0、是否相对边长过大,以及该Edge是否检测到至少两个相邻Face。
|
- 程序会先检查倒角距离是否大于 0、是否相对边长过大,以及该Edge是否检测到至少两个相邻Face。
|
||||||
- 点击后会显示橙色半透明圆管预览,用于提示目标边和倒角距离范围。
|
- 点击后会显示橙色半透明圆管预览,用于提示目标边和倒角距离范围。
|
||||||
- 真实编辑会在后台调用 OCCT 倒角 API,完成后刷新模型。
|
- 真实编辑会在后台调用 OCCT 倒角 API,完成后刷新模型。
|
||||||
- 当前只支持对称距离倒角,不支持两侧不同距离或距离+角度倒角。
|
- 当前支持对称距离倒角、两侧不同距离倒角和距离+角度倒角。
|
||||||
- 由于 STEP 不带建模历史,某些边会被 OCCT 判断为不适合倒角;失败时程序会提示并尝试恢复编辑前状态。
|
- 由于 STEP 不带建模历史,某些边会被 OCCT 判断为不适合倒角;失败时程序会提示并尝试恢复编辑前状态。
|
||||||
- `Edge目标长度` + `修改Edge长度`:
|
- `Edge目标长度` + `修改Edge长度`:
|
||||||
- 先切换到 `Edge` 选择模式并选择一条Edge,或从 `可编辑对象` 中点击Edge长度相关行进入。
|
- 先切换到 `Edge` 选择模式并选择一条Edge,或从 `可编辑对象` 中点击Edge长度相关行进入。
|
||||||
- 选中Edge后,`Edge目标长度` 会自动填入当前Edge长度。
|
- 选中Edge后,`Edge目标长度` 会自动填入当前Edge长度。
|
||||||
- 右侧Edge长度基准下拉框可以选择 `自动`、`中心`、`固定起点` 或 `固定终点`。
|
- 右侧Edge长度基准下拉框可以选择 `自动`、`中心`、`固定起点` 或 `固定终点`。
|
||||||
- 把它改成目标长度后点击 `修改Edge长度`。
|
- 把它改成目标长度后点击 `修改Edge长度`。
|
||||||
|
- 如果选中的是直线Edge,也可以在当前选中对象表中直接修改 `Edge起点坐标`、`Edge中心坐标` 或 `Edge终点坐标`,格式是 `X, Y, Z`,然后点击该行的 `可修改` 按钮。
|
||||||
|
- 修改 `Edge中心坐标` 会保持这条直线Edge长度不变,两个端点一起移动;修改起点或终点则只移动对应端点。
|
||||||
- 对 `assets/models/cube_10mm.step` 这类全平面多面体,直线Edge会优先只移动被选Edge的端点并重建周边平面。
|
- 对 `assets/models/cube_10mm.step` 这类全平面多面体,直线Edge会优先只移动被选Edge的端点并重建周边平面。
|
||||||
- 这时正方体不会默认变成长方体;相邻面会跟着被移动的边自然变成梯形,必要时非共面的面会拆成三角面。
|
- 这时正方体不会默认变成长方体;相邻面会跟着被移动的边自然变成梯形,必要时非共面的面会拆成三角面。
|
||||||
- 如果确认窗口里的策略是 `local-edge-only-deform`,说明走的就是“只变这条边”的局部形变路径。
|
- 如果确认窗口里的策略是 `local-edge-only-deform`,说明走的就是“只变这条边”的局部形变路径。
|
||||||
|
- 如果确认窗口里的策略是 `local-edge-endpoint-deform`,说明走的是“移动起点/终点坐标”的局部形变路径。
|
||||||
|
- 如果确认窗口里的策略是 `local-edge-center-deform`,说明走的是“移动整条Edge中心坐标”的局部形变路径。
|
||||||
- 如果模型不是简单全平面多面体,直线Edge会继续尝试端面推拉后备路径,并计算需要移动的端面Face和推拉距离。
|
- 如果模型不是简单全平面多面体,直线Edge会继续尝试端面推拉后备路径,并计算需要移动的端面Face和推拉距离。
|
||||||
- 圆形/圆弧Edge会优先尝试复用相邻圆柱面的孔/槽或凸台直径编辑,并在确认窗口显示换算出来的目标直径。
|
- 圆形/圆弧Edge会优先尝试复用相邻圆柱面的孔/槽或凸台直径编辑,并在确认窗口显示换算出来的目标直径。
|
||||||
- 如果圆边没有可复用圆柱面,但能读取圆心和轴线,会走 `围绕圆边轴线径向缩放` 后备策略,尽量不改变轴向厚度/高度;执行后部分解析几何可能变成 B-spline。
|
- 如果圆边没有可复用圆柱面,但能读取圆心和轴线,会走 `围绕圆边轴线径向缩放` 后备策略,尽量不改变轴向厚度/高度;执行后部分解析几何可能变成 B-spline。
|
||||||
@@ -653,7 +742,7 @@ python scripts\verify_edge_length_resize.py
|
|||||||
- `导出差异报告` 会把当前选中的历史记录导出为 `.txt`,里面包含操作详情、拓扑变化、几何变化和热力图统计。
|
- `导出差异报告` 会把当前选中的历史记录导出为 `.txt`,里面包含操作详情、拓扑变化、几何变化和热力图统计。
|
||||||
- `导出编辑历史` 会把当前会话中的本软件编辑记录导出为 `.json`,用于留档和后续复盘;当前格式是 `step-editor-operation-history-v2`,包含机器可读的动作名、目标和参数。
|
- `导出编辑历史` 会把当前会话中的本软件编辑记录导出为 `.json`,用于留档和后续复盘;当前格式是 `step-editor-operation-history-v2`,包含机器可读的动作名、目标和参数。
|
||||||
- 当前热力图是“编辑后模型顶点到编辑前模型表面”的距离估算,不是完整 CAD 公差报告。
|
- 当前热力图是“编辑后模型顶点到编辑前模型表面”的距离估算,不是完整 CAD 公差报告。
|
||||||
- 点击某条历史记录时,程序也会尝试重新高亮当时编辑的目标Face,并用青色小点标出当时的拾取位置。
|
- 点击某条历史记录时,程序也会尝试重新高亮当时编辑的目标Face,并在详情里显示当时的拾取位置;3D 视图里不会额外画拾取点小球。
|
||||||
- 由于布尔编辑后拓扑 ID 可能重新分配,历史里的Face ID只能作为定位参考,不能视为稳定的 CAD 建模历史 ID。
|
- 由于布尔编辑后拓扑 ID 可能重新分配,历史里的Face ID只能作为定位参考,不能视为稳定的 CAD 建模历史 ID。
|
||||||
- 撤销后对应记录会从列表里退回。
|
- 撤销后对应记录会从列表里退回。
|
||||||
- 重做后对应记录会重新显示。
|
- 重做后对应记录会重新显示。
|
||||||
@@ -799,7 +888,7 @@ git diff --check
|
|||||||
- STEP 后台加载:首次先显示粗略网格,再异步切到精细显示,并可隐藏同域内部边。
|
- STEP 后台加载:首次先显示粗略网格,再异步切到精细显示,并可隐藏同域内部边。
|
||||||
- 选择:支持零件、Solid、Face、Edge、特征。鼠标悬停红色预高亮,选中黄色高亮。
|
- 选择:支持零件、Solid、Face、Edge、特征。鼠标悬停红色预高亮,选中黄色高亮。
|
||||||
- 同域面:Face/特征会尽量把共面或同轴同半径的连续碎面合成逻辑区域。
|
- 同域面:Face/特征会尽量把共面或同轴同半径的连续碎面合成逻辑区域。
|
||||||
- 编辑:平面推拉、孔径、槽/半孔宽度、盲孔/盲槽深度、凸台直径、已有圆角半径当前版本修改、Edge圆角、Edge倒角、薄壁厚度、修改Edge长度、零件/Solid 平移和旋转。
|
- 编辑:平面推拉和目标法向位置、孔径/半径、槽/半孔宽度、深度、圆弧长度、圆弧角度、开口角度、槽孔总长度和中心距、盲孔/盲槽深度、凸台直径/半径/高度/轴心坐标、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度当前版本修改、圆锥半角、环面直径、Edge圆角、Edge倒角、圆Edge半径/直径、薄壁厚度、修改Edge长度、直线Edge起点/中心/终点坐标、零件/Solid 平移和旋转。Face 面积当前作为只读属性展示,不再伪装成直接可修改参数。
|
||||||
- 恢复:编辑前快照、后台执行、失败回滚、撤销/重做、差异预览、热力图和差异报告。
|
- 恢复:编辑前快照、后台执行、失败回滚、撤销/重做、差异预览、热力图和差异报告。
|
||||||
- 导出:完整模型、零件、Solid、Face、特征区域和 Edge,导出前做基础质量检查。
|
- 导出:完整模型、零件、Solid、Face、特征区域和 Edge,导出前做基础质量检查。
|
||||||
|
|
||||||
@@ -810,9 +899,10 @@ git diff --check
|
|||||||
当前策略顺序:
|
当前策略顺序:
|
||||||
|
|
||||||
1. `local-edge-only-deform`:直线Edge位于简单全平面多面体时,优先只移动被选Edge的端点并重建所属Solid/零件。对立方体测试模型,默认固定起点移动终点;相邻面会自然变成梯形,必要时非共面四边面拆三角面,而不是把整个正方体缩放成长方体。
|
1. `local-edge-only-deform`:直线Edge位于简单全平面多面体时,优先只移动被选Edge的端点并重建所属Solid/零件。对立方体测试模型,默认固定起点移动终点;相邻面会自然变成梯形,必要时非共面四边面拆三角面,而不是把整个正方体缩放成长方体。
|
||||||
2. `move-edge-end-plane-by-push-pull`:如果不适合局部形变,尝试找端点附近、方向匹配的平面端面,把改边长转成端面推拉。
|
2. `local-edge-endpoint-deform`:直线Edge起点/终点坐标直接修改,同样只在简单全平面多面体上移动单个端点并重建周边平面。
|
||||||
3. `resize-adjacent-cylinder-from-circular-edge-length`:圆形/圆弧Edge尝试复用相邻圆柱孔/槽或凸台直径编辑。
|
3. `move-edge-end-plane-by-push-pull`:如果不适合局部形变,尝试找端点附近、方向匹配的平面端面,把改边长转成端面推拉。
|
||||||
4. `scale-owning-shape-from-edge`:圆边、椭圆边和可采样为平面的非直线Edge会优先用 `radial-affine` 围绕曲线平面法向做径向缩放;其他无明确局部路径的Edge才按Edge轴向或整体做几何缩放 fallback,风险高,会影响其它尺寸。
|
4. `resize-adjacent-cylinder-from-circular-edge-length`:圆形/圆弧Edge尝试复用相邻圆柱孔/槽或凸台直径编辑。
|
||||||
|
5. `scale-owning-shape-from-edge`:圆边、椭圆边和可采样为平面的非直线Edge会优先用 `radial-affine` 围绕曲线平面法向做径向缩放;其他无明确局部路径的Edge才按Edge轴向或整体做几何缩放 fallback,风险高,会影响其它尺寸。
|
||||||
|
|
||||||
确认窗口和操作历史里要尽量记录策略、目标范围、目标Edge长度、基准、移动端点、重建面数和风险,方便用户判断结果是否符合预期。
|
确认窗口和操作历史里要尽量记录策略、目标范围、目标Edge长度、基准、移动端点、重建面数和风险,方便用户判断结果是否符合预期。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .app import StepEditorWindow, main
|
|
||||||
from .model import StepModel
|
from .model import StepModel
|
||||||
|
|
||||||
__all__ = ["StepEditorWindow", "StepModel", "main"]
|
__all__ = ["StepEditorWindow", "StepModel", "main"]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
if name in {"StepEditorWindow", "main"}:
|
||||||
|
from .app import StepEditorWindow, main
|
||||||
|
|
||||||
|
return {"StepEditorWindow": StepEditorWindow, "main": main}[name]
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|||||||
+223
-37
@@ -103,11 +103,19 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.hover_face_actor = None
|
self.hover_face_actor = None
|
||||||
self.hover_edge_actor = None
|
self.hover_edge_actor = None
|
||||||
self.hover_signature: tuple[str, int] | None = None
|
self.hover_signature: tuple[str, int] | None = None
|
||||||
self.hover_interval_ms = 85
|
self.hover_interval_ms = 140
|
||||||
self.hover_move_threshold_px = 5
|
self.hover_move_threshold_px = 8
|
||||||
self.pending_hover_position: tuple[int, int] | None = None
|
self.pending_hover_position: tuple[int, int] | None = None
|
||||||
self.last_hover_pick_position: tuple[int, int] | None = None
|
self.last_hover_pick_position: tuple[int, int] | None = None
|
||||||
|
self.camera_interaction_active = False
|
||||||
|
self.edge_actor_visible_before_interaction = True
|
||||||
|
self.model_interpolation_before_interaction: int | None = None
|
||||||
self.pointer_button_down = False
|
self.pointer_button_down = False
|
||||||
|
self.left_button_press_position: tuple[int, int] | None = None
|
||||||
|
self.left_button_dragged = False
|
||||||
|
self.left_click_drag_threshold_px = 6
|
||||||
|
self.skip_next_vtk_left_press = False
|
||||||
|
self.skip_next_vtk_left_release = False
|
||||||
self.pick_marker_actor = None
|
self.pick_marker_actor = None
|
||||||
self.edit_preview_actor = None
|
self.edit_preview_actor = None
|
||||||
self.edit_preview_actors: list[object] = []
|
self.edit_preview_actors: list[object] = []
|
||||||
@@ -143,6 +151,11 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.cylinder_candidate_cache: list[dict[str, object]] = []
|
self.cylinder_candidate_cache: list[dict[str, object]] = []
|
||||||
self.cylinder_candidates_loaded = False
|
self.cylinder_candidates_loaded = False
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
|
self.edit_busy_timer = QTimer(self)
|
||||||
|
self.edit_busy_timer.timeout.connect(self._pulse_edit_busy_status)
|
||||||
|
self.edit_busy_base_text = ""
|
||||||
|
self.edit_busy_started_at: datetime | None = None
|
||||||
|
self.edit_busy_phase = 0
|
||||||
self.edit_thread: QThread | None = None
|
self.edit_thread: QThread | None = None
|
||||||
self.edit_worker: EditWorker | None = None
|
self.edit_worker: EditWorker | None = None
|
||||||
self.pending_edit_context: dict[str, object] | None = None
|
self.pending_edit_context: dict[str, object] | None = None
|
||||||
@@ -158,6 +171,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.load_refine_worker: LoadWorker | None = None
|
self.load_refine_worker: LoadWorker | None = None
|
||||||
self.pending_load_path: Path | None = None
|
self.pending_load_path: Path | None = None
|
||||||
self.initial_load_deflection = 2.0
|
self.initial_load_deflection = 2.0
|
||||||
|
self.edit_result_deflection = 1.6
|
||||||
self.last_id_kind = "Face"
|
self.last_id_kind = "Face"
|
||||||
self.property_editor_updating = False
|
self.property_editor_updating = False
|
||||||
self.property_editor_specs: list[dict[str, object]] = []
|
self.property_editor_specs: list[dict[str, object]] = []
|
||||||
@@ -394,23 +408,76 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
QPushButton#propertyRowEditButton {
|
QPushButton#propertyRowEditButton {
|
||||||
background: #fff3d9;
|
background: #ea580c;
|
||||||
border: 1px solid #d97706;
|
border: 1px solid #c2410c;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: #92400e;
|
color: #ffffff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
}
|
}
|
||||||
QPushButton#propertyRowEditButton:hover {
|
QPushButton#propertyRowEditButton:hover {
|
||||||
background: #ffe6b8;
|
background: #f97316;
|
||||||
border-color: #b45309;
|
border-color: #9a3412;
|
||||||
|
}
|
||||||
|
QPushButton#propertyRowEditButton:pressed {
|
||||||
|
background: #9a3412;
|
||||||
|
border-color: #7c2d12;
|
||||||
|
color: #ffffff;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
QPushButton#propertyRowEditButton[changed="true"] {
|
||||||
|
background: #dcfce7;
|
||||||
|
border: 1px solid #16a34a;
|
||||||
|
color: #14532d;
|
||||||
|
}
|
||||||
|
QPushButton#propertyRowEditButton[changed="true"]:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
border-color: #15803d;
|
||||||
}
|
}
|
||||||
QPushButton#propertyRowEditButton:disabled {
|
QPushButton#propertyRowEditButton:disabled {
|
||||||
background: #eef2f6;
|
background: #eef2f6;
|
||||||
border: 1px dashed #bcc7d4;
|
border: 1px dashed #bcc7d4;
|
||||||
color: #8f99a8;
|
color: #8f99a8;
|
||||||
}
|
}
|
||||||
|
QLineEdit#propertyTargetEditor {
|
||||||
|
background: #fff7ed;
|
||||||
|
border: 2px solid #f97316;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #111827;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0px 5px;
|
||||||
|
selection-background-color: #bfdbfe;
|
||||||
|
}
|
||||||
|
QLineEdit#propertyTargetEditor:hover {
|
||||||
|
background: #ffedd5;
|
||||||
|
border-color: #ea580c;
|
||||||
|
}
|
||||||
|
QLineEdit#propertyTargetEditor:focus {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2px solid #2563eb;
|
||||||
|
padding: 0px 4px;
|
||||||
|
}
|
||||||
|
QPushButton#propertyExpandBar {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #d7dee8;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 600;
|
||||||
|
min-height: 14px;
|
||||||
|
max-height: 20px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
QPushButton#propertyExpandBar:hover {
|
||||||
|
background: #eef6ff;
|
||||||
|
border-color: #9dbbe0;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
QPushButton#propertyExpandBar:pressed {
|
||||||
|
background: #dbeafe;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
QLineEdit:disabled,
|
QLineEdit:disabled,
|
||||||
QComboBox:disabled,
|
QComboBox:disabled,
|
||||||
QPlainTextEdit:disabled {
|
QPlainTextEdit:disabled {
|
||||||
@@ -743,14 +810,24 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
|
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
|
||||||
object_edit_layout.addWidget(self.property_table)
|
object_edit_layout.addWidget(self.property_table)
|
||||||
self.property_expand_button = QPushButton("展开全部属性")
|
self.property_expand_button = QPushButton("展开全部属性")
|
||||||
|
self.property_expand_button.setObjectName("propertyExpandBar")
|
||||||
help_tip(self.property_expand_button, "当前选中对象属性较多时,默认只显示前 6 行;点击这里展开或收起属性表。")
|
help_tip(self.property_expand_button, "当前选中对象属性较多时,默认只显示前 6 行;点击这里展开或收起属性表。")
|
||||||
self.property_expand_button.clicked.connect(self.toggle_property_table_expanded)
|
self.property_expand_button.clicked.connect(self.toggle_property_table_expanded)
|
||||||
self.property_expand_button.setVisible(False)
|
self.property_expand_button.setVisible(False)
|
||||||
|
self.property_expand_button.setMaximumHeight(22)
|
||||||
|
self.property_expand_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
object_edit_layout.addWidget(self.property_expand_button)
|
object_edit_layout.addWidget(self.property_expand_button)
|
||||||
|
property_action_row = QHBoxLayout()
|
||||||
|
property_action_row.setContentsMargins(0, 0, 0, 0)
|
||||||
self.apply_property_button = QPushButton("参数化建模")
|
self.apply_property_button = QPushButton("参数化建模")
|
||||||
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)
|
||||||
object_edit_layout.addWidget(self.apply_property_button)
|
self.quick_export_all_button = QPushButton("导出当前完整STEP")
|
||||||
|
help_tip(self.quick_export_all_button, "把当前编辑后的完整模型导出为 STEP 文件。导出前会做基础质量检查。")
|
||||||
|
self.quick_export_all_button.clicked.connect(self.export_all)
|
||||||
|
property_action_row.addWidget(self.apply_property_button)
|
||||||
|
property_action_row.addWidget(self.quick_export_all_button)
|
||||||
|
object_edit_layout.addLayout(property_action_row)
|
||||||
|
|
||||||
edit_box = QGroupBox(panel)
|
edit_box = QGroupBox(panel)
|
||||||
edit_box.setObjectName("editSection")
|
edit_box.setObjectName("editSection")
|
||||||
@@ -770,6 +847,18 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.hole_diameter_input = QLineEdit("")
|
self.hole_diameter_input = QLineEdit("")
|
||||||
help_tip(self.hole_diameter_input, "圆柱孔/槽的目标直径。选中候选后会自动填一个参考值,可以手动改。")
|
help_tip(self.hole_diameter_input, "圆柱孔/槽的目标直径。选中候选后会自动填一个参考值,可以手动改。")
|
||||||
edit_layout.addWidget(self.hole_diameter_input, 2, 1)
|
edit_layout.addWidget(self.hole_diameter_input, 2, 1)
|
||||||
|
self.hole_center_x_input = QLineEdit("", edit_box)
|
||||||
|
self.hole_center_y_input = QLineEdit("", edit_box)
|
||||||
|
self.hole_center_z_input = QLineEdit("", edit_box)
|
||||||
|
for hole_center_input in (
|
||||||
|
self.hole_center_x_input,
|
||||||
|
self.hole_center_y_input,
|
||||||
|
self.hole_center_z_input,
|
||||||
|
):
|
||||||
|
hole_center_input.setVisible(False)
|
||||||
|
help_tip(self.hole_center_x_input, "圆柱孔轴心目标 X 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
|
||||||
|
help_tip(self.hole_center_y_input, "圆柱孔轴心目标 Y 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
|
||||||
|
help_tip(self.hole_center_z_input, "圆柱孔轴心目标 Z 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
|
||||||
self.resize_button = QPushButton("调整圆柱孔径")
|
self.resize_button = QPushButton("调整圆柱孔径")
|
||||||
help_tip(self.resize_button, "修改孔或圆柱槽的直径。扩大时切削,缩小时会先补料再重切。")
|
help_tip(self.resize_button, "修改孔或圆柱槽的直径。扩大时切削,缩小时会先补料再重切。")
|
||||||
self.resize_button.clicked.connect(self.resize_hole)
|
self.resize_button.clicked.connect(self.resize_hole)
|
||||||
@@ -784,21 +873,75 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.resize_slot_button.clicked.connect(self.resize_slot_width)
|
self.resize_slot_button.clicked.connect(self.resize_slot_width)
|
||||||
edit_layout.addWidget(self.resize_slot_button, 5, 0, 1, 2)
|
edit_layout.addWidget(self.resize_slot_button, 5, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("凸台直径"), 6, 0)
|
edit_layout.addWidget(QLabel("槽/半孔深度"), 6, 0)
|
||||||
|
self.slot_depth_input = QLineEdit("")
|
||||||
|
help_tip(self.slot_depth_input, "槽或半孔的目标凹入深度。程序会按当前圆弧角度换算成对应圆柱直径来执行。")
|
||||||
|
edit_layout.addWidget(self.slot_depth_input, 6, 1)
|
||||||
|
self.resize_slot_depth_button = QPushButton("调整槽/半孔深度")
|
||||||
|
help_tip(self.resize_slot_depth_button, "修改已识别槽/半孔候选的凹入深度。当前版本是几何近似,失败会回滚。")
|
||||||
|
self.resize_slot_depth_button.clicked.connect(self.resize_slot_depth)
|
||||||
|
edit_layout.addWidget(self.resize_slot_depth_button, 7, 0, 1, 2)
|
||||||
|
self.slot_arc_length_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_arc_length_input.setVisible(False)
|
||||||
|
help_tip(self.slot_arc_length_input, "槽或半孔的目标圆弧长度。当前选中对象表会使用这个隐藏输入执行圆弧长度修改。")
|
||||||
|
self.slot_angular_span_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_angular_span_input.setVisible(False)
|
||||||
|
help_tip(self.slot_angular_span_input, "槽或半孔的目标圆弧角度。当前选中对象表会使用这个隐藏输入执行角度修改。")
|
||||||
|
self.slot_pair_face_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_pair_face_input.setVisible(False)
|
||||||
|
help_tip(self.slot_pair_face_input, "长圆槽/槽孔的另一个半圆端 Face ID。自动配对不准时可在当前选中对象表里手动设置。")
|
||||||
|
self.slot_total_length_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_total_length_input.setVisible(False)
|
||||||
|
help_tip(self.slot_total_length_input, "长圆槽/槽孔的目标总长度。只有识别到成对半圆槽端时才会启用。")
|
||||||
|
self.slot_center_x_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_center_x_input.setVisible(False)
|
||||||
|
self.slot_center_y_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_center_y_input.setVisible(False)
|
||||||
|
self.slot_center_z_input = QLineEdit("", edit_box)
|
||||||
|
self.slot_center_z_input.setVisible(False)
|
||||||
|
help_tip(self.slot_center_x_input, "槽/半孔目标轴心坐标 X。当前选中对象表会使用这个隐藏输入执行轴心移动。")
|
||||||
|
help_tip(self.slot_center_y_input, "槽/半孔目标轴心坐标 Y。当前选中对象表会使用这个隐藏输入执行轴心移动。")
|
||||||
|
help_tip(self.slot_center_z_input, "槽/半孔目标轴心坐标 Z。当前选中对象表会使用这个隐藏输入执行轴心移动。")
|
||||||
|
|
||||||
|
edit_layout.addWidget(QLabel("凸台直径"), 8, 0)
|
||||||
self.boss_diameter_input = QLineEdit("")
|
self.boss_diameter_input = QLineEdit("")
|
||||||
help_tip(self.boss_diameter_input, "圆柱凸台的目标直径。选中凸台候选后会自动填一个参考值。")
|
help_tip(self.boss_diameter_input, "圆柱凸台的目标直径。选中凸台候选后会自动填一个参考值。")
|
||||||
edit_layout.addWidget(self.boss_diameter_input, 6, 1)
|
edit_layout.addWidget(self.boss_diameter_input, 8, 1)
|
||||||
|
self.boss_height_input = QLineEdit("", edit_box)
|
||||||
|
self.boss_height_input.setVisible(False)
|
||||||
|
help_tip(self.boss_height_input, "圆柱凸台的目标高度。当前选中对象表会使用这个隐藏输入执行高度修改。")
|
||||||
|
self.boss_center_x_input = QLineEdit("", edit_box)
|
||||||
|
self.boss_center_x_input.setVisible(False)
|
||||||
|
self.boss_center_y_input = QLineEdit("", edit_box)
|
||||||
|
self.boss_center_y_input.setVisible(False)
|
||||||
|
self.boss_center_z_input = QLineEdit("", edit_box)
|
||||||
|
self.boss_center_z_input.setVisible(False)
|
||||||
|
help_tip(self.boss_center_x_input, "圆柱凸台目标轴心坐标 X。当前选中对象表会使用这个隐藏输入执行轴心移动。")
|
||||||
|
help_tip(self.boss_center_y_input, "圆柱凸台目标轴心坐标 Y。当前选中对象表会使用这个隐藏输入执行轴心移动。")
|
||||||
|
help_tip(self.boss_center_z_input, "圆柱凸台目标轴心坐标 Z。当前选中对象表会使用这个隐藏输入执行轴心移动。")
|
||||||
|
self.cone_reference_radius_input = QLineEdit("", edit_box)
|
||||||
|
self.cone_reference_radius_input.setVisible(False)
|
||||||
|
help_tip(self.cone_reference_radius_input, "圆锥面的目标参考半径。当前选中对象表会使用这个隐藏输入执行圆锥面修改。")
|
||||||
|
self.sphere_radius_input = QLineEdit("", edit_box)
|
||||||
|
self.sphere_radius_input.setVisible(False)
|
||||||
|
help_tip(self.sphere_radius_input, "球面的目标半径。当前选中对象表会使用这个隐藏输入执行球面修改。")
|
||||||
|
self.torus_radius_input = QLineEdit("", edit_box)
|
||||||
|
self.torus_radius_input.setVisible(False)
|
||||||
|
help_tip(self.torus_radius_input, "环面的目标半径。当前选中对象表会使用这个隐藏输入执行环面修改。")
|
||||||
|
self.face_area_input = QLineEdit("", edit_box)
|
||||||
|
self.face_area_input.setVisible(False)
|
||||||
|
help_tip(self.face_area_input, "复杂曲面的目标Face面积。当前选中对象表会使用这个隐藏输入执行兜底面积缩放。")
|
||||||
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
|
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
|
||||||
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
|
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
|
||||||
self.resize_boss_button.clicked.connect(self.resize_boss)
|
self.resize_boss_button.clicked.connect(self.resize_boss)
|
||||||
edit_layout.addWidget(self.resize_boss_button, 7, 0, 1, 2)
|
edit_layout.addWidget(self.resize_boss_button, 9, 0, 1, 2)
|
||||||
|
|
||||||
self.suppress_button = QPushButton("封堵圆柱孔")
|
self.suppress_button = QPushButton("封堵圆柱孔")
|
||||||
help_tip(self.suppress_button, "用补料体填住完整圆柱孔。适合通孔/盲孔,不适合半孔或槽。")
|
help_tip(self.suppress_button, "用补料体填住完整圆柱孔。适合通孔/盲孔,不适合半孔或槽。")
|
||||||
self.suppress_button.clicked.connect(self.suppress_hole)
|
self.suppress_button.clicked.connect(self.suppress_hole)
|
||||||
edit_layout.addWidget(self.suppress_button, 8, 0, 1, 2)
|
edit_layout.addWidget(self.suppress_button, 10, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("孔深度"), 9, 0)
|
edit_layout.addWidget(QLabel("孔深度"), 11, 0)
|
||||||
depth_inputs = QWidget()
|
depth_inputs = QWidget()
|
||||||
depth_layout = QHBoxLayout(depth_inputs)
|
depth_layout = QHBoxLayout(depth_inputs)
|
||||||
depth_layout.setContentsMargins(0, 0, 0, 0)
|
depth_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -810,41 +953,72 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
self.hole_bottom_face_input.textChanged.connect(lambda _text: self._update_action_states())
|
self.hole_bottom_face_input.textChanged.connect(lambda _text: self._update_action_states())
|
||||||
depth_layout.addWidget(self.hole_depth_input, stretch=2)
|
depth_layout.addWidget(self.hole_depth_input, stretch=2)
|
||||||
depth_layout.addWidget(self.hole_bottom_face_input, stretch=1)
|
depth_layout.addWidget(self.hole_bottom_face_input, stretch=1)
|
||||||
edit_layout.addWidget(depth_inputs, 9, 1)
|
edit_layout.addWidget(depth_inputs, 11, 1)
|
||||||
self.resize_depth_button = QPushButton("调整盲孔深度")
|
self.resize_depth_button = QPushButton("调整盲孔/盲槽深度")
|
||||||
help_tip(self.resize_depth_button, "加深或变浅盲孔/盲槽。需要识别到底面,或手动填写底面 Face ID。")
|
help_tip(self.resize_depth_button, "加深或变浅盲孔/盲槽。需要识别到底面,或手动填写底面 Face ID。")
|
||||||
self.resize_depth_button.clicked.connect(self.resize_hole_depth)
|
self.resize_depth_button.clicked.connect(self.resize_hole_depth)
|
||||||
edit_layout.addWidget(self.resize_depth_button, 10, 0, 1, 2)
|
edit_layout.addWidget(self.resize_depth_button, 12, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("圆角半径"), 11, 0)
|
edit_layout.addWidget(QLabel("圆角半径"), 13, 0)
|
||||||
self.edge_fillet_radius_input = QLineEdit("")
|
self.edge_fillet_radius_input = QLineEdit("")
|
||||||
help_tip(self.edge_fillet_radius_input, "新圆角或已有圆角的目标半径。选中Edge/圆角候选后会自动填参考值。")
|
help_tip(self.edge_fillet_radius_input, "新圆角或已有圆角的目标半径。选中Edge/圆角候选后会自动填参考值。")
|
||||||
edit_layout.addWidget(self.edge_fillet_radius_input, 11, 1)
|
edit_layout.addWidget(self.edge_fillet_radius_input, 13, 1)
|
||||||
self.fillet_edge_button = QPushButton("给Edge添加圆角")
|
self.fillet_edge_button = QPushButton("给Edge添加圆角")
|
||||||
help_tip(self.fillet_edge_button, "给当前直线Edge新增圆角。不是修改已有圆角;已有圆角请用下面那个按钮。")
|
help_tip(self.fillet_edge_button, "给当前直线Edge新增圆角。不是修改已有圆角;已有圆角请用下面那个按钮。")
|
||||||
self.fillet_edge_button.clicked.connect(self.fillet_edge)
|
self.fillet_edge_button.clicked.connect(self.fillet_edge)
|
||||||
edit_layout.addWidget(self.fillet_edge_button, 12, 0, 1, 2)
|
edit_layout.addWidget(self.fillet_edge_button, 14, 0, 1, 2)
|
||||||
|
|
||||||
self.resize_existing_fillet_button = QPushButton("修改已有圆角半径")
|
self.resize_existing_fillet_button = QPushButton("修改已有圆角半径")
|
||||||
help_tip(self.resize_existing_fillet_button, "尝试修改已识别圆角面的半径。会先移除原圆角再重建,复杂圆角可能失败并回滚。")
|
help_tip(self.resize_existing_fillet_button, "尝试修改已识别圆角面的半径。会先移除原圆角再重建,复杂圆角可能失败并回滚。")
|
||||||
self.resize_existing_fillet_button.clicked.connect(self.resize_existing_fillet)
|
self.resize_existing_fillet_button.clicked.connect(self.resize_existing_fillet)
|
||||||
edit_layout.addWidget(self.resize_existing_fillet_button, 13, 0, 1, 2)
|
edit_layout.addWidget(self.resize_existing_fillet_button, 15, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("倒角距离"), 14, 0)
|
edit_layout.addWidget(QLabel("倒角距离"), 16, 0)
|
||||||
self.edge_chamfer_distance_input = QLineEdit("")
|
self.edge_chamfer_distance_input = QLineEdit("")
|
||||||
help_tip(self.edge_chamfer_distance_input, "给Edge添加倒角时使用的距离。选中直线Edge后会填一个较小参考值。")
|
help_tip(self.edge_chamfer_distance_input, "给Edge添加倒角时使用的距离。选中直线Edge后会填一个较小参考值。")
|
||||||
edit_layout.addWidget(self.edge_chamfer_distance_input, 14, 1)
|
self.edge_chamfer_distance1_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_chamfer_distance2_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_chamfer_angle_distance_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_chamfer_angle_degrees_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_chamfer_reference_face_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_chamfer_distance1_input.setVisible(False)
|
||||||
|
self.edge_chamfer_distance2_input.setVisible(False)
|
||||||
|
self.edge_chamfer_angle_distance_input.setVisible(False)
|
||||||
|
self.edge_chamfer_angle_degrees_input.setVisible(False)
|
||||||
|
self.edge_chamfer_reference_face_input.setVisible(False)
|
||||||
|
edit_layout.addWidget(self.edge_chamfer_distance_input, 16, 1)
|
||||||
self.chamfer_edge_button = QPushButton("给Edge添加倒角")
|
self.chamfer_edge_button = QPushButton("给Edge添加倒角")
|
||||||
help_tip(self.chamfer_edge_button, "给当前直线Edge新增对称倒角。会先预览,再后台执行,失败会回滚。")
|
help_tip(self.chamfer_edge_button, "给当前直线Edge新增对称倒角。会先预览,再后台执行,失败会回滚。")
|
||||||
self.chamfer_edge_button.clicked.connect(self.chamfer_edge)
|
self.chamfer_edge_button.clicked.connect(self.chamfer_edge)
|
||||||
edit_layout.addWidget(self.chamfer_edge_button, 15, 0, 1, 2)
|
edit_layout.addWidget(self.chamfer_edge_button, 17, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("Edge目标长度"), 16, 0)
|
edit_layout.addWidget(QLabel("Edge目标长度"), 18, 0)
|
||||||
edge_length_inputs = QWidget()
|
edge_length_inputs = QWidget()
|
||||||
edge_length_layout = QHBoxLayout(edge_length_inputs)
|
edge_length_layout = QHBoxLayout(edge_length_inputs)
|
||||||
edge_length_layout.setContentsMargins(0, 0, 0, 0)
|
edge_length_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.edge_target_length_input = QLineEdit("")
|
self.edge_target_length_input = QLineEdit("")
|
||||||
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度再执行。")
|
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度再执行。")
|
||||||
|
self.edge_start_x_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_start_y_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_start_z_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_center_x_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_center_y_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_center_z_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_end_x_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_end_y_input = QLineEdit("", edit_box)
|
||||||
|
self.edge_end_z_input = QLineEdit("", edit_box)
|
||||||
|
for endpoint_input in (
|
||||||
|
self.edge_start_x_input,
|
||||||
|
self.edge_start_y_input,
|
||||||
|
self.edge_start_z_input,
|
||||||
|
self.edge_center_x_input,
|
||||||
|
self.edge_center_y_input,
|
||||||
|
self.edge_center_z_input,
|
||||||
|
self.edge_end_x_input,
|
||||||
|
self.edge_end_y_input,
|
||||||
|
self.edge_end_z_input,
|
||||||
|
):
|
||||||
|
endpoint_input.setVisible(False)
|
||||||
self.edge_length_anchor_combo = NoWheelComboBox()
|
self.edge_length_anchor_combo = NoWheelComboBox()
|
||||||
self.edge_length_anchor_combo.addItem("自动", "auto")
|
self.edge_length_anchor_combo.addItem("自动", "auto")
|
||||||
self.edge_length_anchor_combo.addItem("中心", "center")
|
self.edge_length_anchor_combo.addItem("中心", "center")
|
||||||
@@ -858,31 +1032,31 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
)
|
)
|
||||||
edge_length_layout.addWidget(self.edge_target_length_input, stretch=2)
|
edge_length_layout.addWidget(self.edge_target_length_input, stretch=2)
|
||||||
edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1)
|
edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1)
|
||||||
edit_layout.addWidget(edge_length_inputs, 16, 1)
|
edit_layout.addWidget(edge_length_inputs, 18, 1)
|
||||||
self.resize_edge_length_button = QPushButton("修改Edge长度")
|
self.resize_edge_length_button = QPushButton("修改Edge长度")
|
||||||
help_tip(
|
help_tip(
|
||||||
self.resize_edge_length_button,
|
self.resize_edge_length_button,
|
||||||
"修改当前Edge长度。直线Edge优先局部形变;圆、椭圆和平面曲线会优先用局部/径向策略,必要时再用后备缩放。",
|
"修改当前Edge长度。直线Edge优先局部形变;圆、椭圆和平面曲线会优先用局部/径向策略,必要时再用后备缩放。",
|
||||||
)
|
)
|
||||||
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
|
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
|
||||||
edit_layout.addWidget(self.resize_edge_length_button, 17, 0, 1, 2)
|
edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("薄壁厚度"), 18, 0)
|
edit_layout.addWidget(QLabel("薄壁厚度"), 20, 0)
|
||||||
self.shell_thickness_input = QLineEdit("")
|
self.shell_thickness_input = QLineEdit("")
|
||||||
help_tip(
|
help_tip(
|
||||||
self.shell_thickness_input,
|
self.shell_thickness_input,
|
||||||
"薄壁/壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
|
"薄壁/壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
|
||||||
)
|
)
|
||||||
edit_layout.addWidget(self.shell_thickness_input, 18, 1)
|
edit_layout.addWidget(self.shell_thickness_input, 20, 1)
|
||||||
self.resize_shell_thickness_button = QPushButton("调整薄壁厚度")
|
self.resize_shell_thickness_button = QPushButton("调整薄壁厚度")
|
||||||
help_tip(
|
help_tip(
|
||||||
self.resize_shell_thickness_button,
|
self.resize_shell_thickness_button,
|
||||||
"移动当前平面区域来达到目标厚度。当前版本基于相对平面估算,执行前会预览并可回滚。",
|
"移动当前平面区域来达到目标厚度。当前版本基于相对平面估算,执行前会预览并可回滚。",
|
||||||
)
|
)
|
||||||
self.resize_shell_thickness_button.clicked.connect(self.resize_shell_thickness)
|
self.resize_shell_thickness_button.clicked.connect(self.resize_shell_thickness)
|
||||||
edit_layout.addWidget(self.resize_shell_thickness_button, 19, 0, 1, 2)
|
edit_layout.addWidget(self.resize_shell_thickness_button, 21, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("平移 X/Y/Z"), 20, 0)
|
edit_layout.addWidget(QLabel("平移 X/Y/Z"), 22, 0)
|
||||||
translate_inputs = QWidget()
|
translate_inputs = QWidget()
|
||||||
translate_layout = QHBoxLayout(translate_inputs)
|
translate_layout = QHBoxLayout(translate_inputs)
|
||||||
translate_layout.setContentsMargins(0, 0, 0, 0)
|
translate_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -898,17 +1072,17 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
translate_layout.addWidget(self.translate_x_input)
|
translate_layout.addWidget(self.translate_x_input)
|
||||||
translate_layout.addWidget(self.translate_y_input)
|
translate_layout.addWidget(self.translate_y_input)
|
||||||
translate_layout.addWidget(self.translate_z_input)
|
translate_layout.addWidget(self.translate_z_input)
|
||||||
edit_layout.addWidget(translate_inputs, 20, 1)
|
edit_layout.addWidget(translate_inputs, 22, 1)
|
||||||
self.translate_part_button = QPushButton("平移选中零件")
|
self.translate_part_button = QPushButton("平移选中零件")
|
||||||
help_tip(self.translate_part_button, "按上面的 X/Y/Z 距离移动当前零件。会写入历史,可撤销。")
|
help_tip(self.translate_part_button, "按上面的 X/Y/Z 距离移动当前零件。会写入历史,可撤销。")
|
||||||
self.translate_part_button.clicked.connect(self.translate_selected_part)
|
self.translate_part_button.clicked.connect(self.translate_selected_part)
|
||||||
self.translate_solid_button = QPushButton("平移选中Solid")
|
self.translate_solid_button = QPushButton("平移选中Solid")
|
||||||
help_tip(self.translate_solid_button, "按上面的 X/Y/Z 距离移动当前Solid。单Solid零件中相当于移动整个零件。")
|
help_tip(self.translate_solid_button, "按上面的 X/Y/Z 距离移动当前Solid。单Solid零件中相当于移动整个零件。")
|
||||||
self.translate_solid_button.clicked.connect(self.translate_selected_solid)
|
self.translate_solid_button.clicked.connect(self.translate_selected_solid)
|
||||||
edit_layout.addWidget(self.translate_part_button, 21, 0, 1, 2)
|
edit_layout.addWidget(self.translate_part_button, 23, 0, 1, 2)
|
||||||
edit_layout.addWidget(self.translate_solid_button, 22, 0, 1, 2)
|
edit_layout.addWidget(self.translate_solid_button, 24, 0, 1, 2)
|
||||||
|
|
||||||
edit_layout.addWidget(QLabel("旋转轴/角度"), 23, 0)
|
edit_layout.addWidget(QLabel("旋转轴/角度"), 25, 0)
|
||||||
rotate_inputs = QWidget()
|
rotate_inputs = QWidget()
|
||||||
rotate_layout = QHBoxLayout(rotate_inputs)
|
rotate_layout = QHBoxLayout(rotate_inputs)
|
||||||
rotate_layout.setContentsMargins(0, 0, 0, 0)
|
rotate_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -921,15 +1095,27 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
|||||||
help_tip(self.rotate_angle_input, "旋转角度,单位是度。正负号决定旋转方向。")
|
help_tip(self.rotate_angle_input, "旋转角度,单位是度。正负号决定旋转方向。")
|
||||||
rotate_layout.addWidget(self.rotate_axis_combo)
|
rotate_layout.addWidget(self.rotate_axis_combo)
|
||||||
rotate_layout.addWidget(self.rotate_angle_input)
|
rotate_layout.addWidget(self.rotate_angle_input)
|
||||||
edit_layout.addWidget(rotate_inputs, 23, 1)
|
edit_layout.addWidget(rotate_inputs, 25, 1)
|
||||||
|
self.scale_target_diagonal_input = QLineEdit("", edit_box)
|
||||||
|
self.scale_target_diagonal_input.setVisible(False)
|
||||||
|
help_tip(self.scale_target_diagonal_input, "零件或Solid的目标包围盒对角线。当前选中对象表会使用这个隐藏输入执行整体等比缩放。")
|
||||||
|
self.scale_x_size_input = QLineEdit("", edit_box)
|
||||||
|
self.scale_x_size_input.setVisible(False)
|
||||||
|
help_tip(self.scale_x_size_input, "零件或Solid的目标 X 向包围盒尺寸。")
|
||||||
|
self.scale_y_size_input = QLineEdit("", edit_box)
|
||||||
|
self.scale_y_size_input.setVisible(False)
|
||||||
|
help_tip(self.scale_y_size_input, "零件或Solid的目标 Y 向包围盒尺寸。")
|
||||||
|
self.scale_z_size_input = QLineEdit("", edit_box)
|
||||||
|
self.scale_z_size_input.setVisible(False)
|
||||||
|
help_tip(self.scale_z_size_input, "零件或Solid的目标 Z 向包围盒尺寸。")
|
||||||
self.rotate_part_button = QPushButton("旋转选中零件")
|
self.rotate_part_button = QPushButton("旋转选中零件")
|
||||||
help_tip(self.rotate_part_button, "绕所选轴旋转当前零件。会写入历史,可撤销。")
|
help_tip(self.rotate_part_button, "绕所选轴旋转当前零件。会写入历史,可撤销。")
|
||||||
self.rotate_part_button.clicked.connect(self.rotate_selected_part)
|
self.rotate_part_button.clicked.connect(self.rotate_selected_part)
|
||||||
self.rotate_solid_button = QPushButton("旋转选中Solid")
|
self.rotate_solid_button = QPushButton("旋转选中Solid")
|
||||||
help_tip(self.rotate_solid_button, "绕所选轴旋转当前Solid。单Solid零件中相当于旋转整个零件。")
|
help_tip(self.rotate_solid_button, "绕所选轴旋转当前Solid。单Solid零件中相当于旋转整个零件。")
|
||||||
self.rotate_solid_button.clicked.connect(self.rotate_selected_solid)
|
self.rotate_solid_button.clicked.connect(self.rotate_selected_solid)
|
||||||
edit_layout.addWidget(self.rotate_part_button, 24, 0, 1, 2)
|
edit_layout.addWidget(self.rotate_part_button, 26, 0, 1, 2)
|
||||||
edit_layout.addWidget(self.rotate_solid_button, 25, 0, 1, 2)
|
edit_layout.addWidget(self.rotate_solid_button, 27, 0, 1, 2)
|
||||||
|
|
||||||
panel_layout.addWidget(self.object_edit_box)
|
panel_layout.addWidget(self.object_edit_box)
|
||||||
if ENABLE_EXPORT_PANEL:
|
if ENABLE_EXPORT_PANEL:
|
||||||
|
|||||||
+1361
-12
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
|||||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Defeaturing
|
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Defeaturing
|
||||||
from OCC.Core.BRepBndLib import brepbndlib
|
from OCC.Core.BRepBndLib import brepbndlib
|
||||||
from OCC.Core.BOPAlgo import BOPAlgo_GlueFull
|
from OCC.Core.BOPAlgo import BOPAlgo_GlueFull
|
||||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
|
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_GTransform, BRepBuilderAPI_Transform
|
||||||
from OCC.Core.BRepCheck import BRepCheck_Analyzer
|
from OCC.Core.BRepCheck import BRepCheck_Analyzer
|
||||||
from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
|
from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
|
||||||
from OCC.Core.BRepGProp import brepgprop
|
from OCC.Core.BRepGProp import brepgprop
|
||||||
@@ -34,7 +34,7 @@ from OCC.Core.TopExp import TopExp_Explorer
|
|||||||
from OCC.Core.TopLoc import TopLoc_Location
|
from OCC.Core.TopLoc import TopLoc_Location
|
||||||
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Shape, topods
|
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Shape, topods
|
||||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||||
from OCC.Core.gp import gp_Ax1, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
|
from OCC.Core.gp import gp_Ax1, gp_Dir, gp_GTrsf, gp_Pnt, gp_Trsf, gp_Vec, gp_XYZ
|
||||||
from OCC.Extend.TopologyUtils import TopologyExplorer
|
from OCC.Extend.TopologyUtils import TopologyExplorer
|
||||||
|
|
||||||
from .constants import CURVE_TYPES, ORIENTATION_TYPES
|
from .constants import CURVE_TYPES, ORIENTATION_TYPES
|
||||||
@@ -341,6 +341,58 @@ def _rotated_shape(
|
|||||||
return BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
return BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
||||||
|
|
||||||
|
|
||||||
|
def _scaled_shape(shape: TopoDS_Shape, scale: float, center: tuple[float, float, float]) -> TopoDS_Shape:
|
||||||
|
if abs(scale - 1.0) <= 1e-12:
|
||||||
|
return shape
|
||||||
|
if scale <= 0:
|
||||||
|
raise ValueError("Scale factor must be greater than 0.")
|
||||||
|
trsf = gp_Trsf()
|
||||||
|
trsf.SetScale(gp_Pnt(*center), float(scale))
|
||||||
|
return BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
||||||
|
|
||||||
|
|
||||||
|
def _axis_scaled_shape(
|
||||||
|
shape: TopoDS_Shape,
|
||||||
|
axis_name: str,
|
||||||
|
scale: float,
|
||||||
|
center: tuple[float, float, float],
|
||||||
|
) -> TopoDS_Shape:
|
||||||
|
if abs(scale - 1.0) <= 1e-12:
|
||||||
|
return shape
|
||||||
|
if scale <= 0:
|
||||||
|
raise ValueError("Scale factor must be greater than 0.")
|
||||||
|
axis = axis_name.upper()
|
||||||
|
axis_index = {"X": 0, "Y": 1, "Z": 2}.get(axis)
|
||||||
|
if axis_index is None:
|
||||||
|
raise ValueError("Scale axis must be X, Y or Z.")
|
||||||
|
matrix = [
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
]
|
||||||
|
matrix[axis_index][axis_index] = float(scale)
|
||||||
|
cx, cy, cz = center
|
||||||
|
moved_center = (
|
||||||
|
matrix[0][0] * cx + matrix[0][1] * cy + matrix[0][2] * cz,
|
||||||
|
matrix[1][0] * cx + matrix[1][1] * cy + matrix[1][2] * cz,
|
||||||
|
matrix[2][0] * cx + matrix[2][1] * cy + matrix[2][2] * cz,
|
||||||
|
)
|
||||||
|
translation = (cx - moved_center[0], cy - moved_center[1], cz - moved_center[2])
|
||||||
|
transform = gp_GTrsf()
|
||||||
|
for row in range(3):
|
||||||
|
for column in range(3):
|
||||||
|
transform.SetValue(row + 1, column + 1, matrix[row][column])
|
||||||
|
transform.SetTranslationPart(gp_XYZ(*translation))
|
||||||
|
builder = BRepBuilderAPI_GTransform(shape, transform, True)
|
||||||
|
builder.Build()
|
||||||
|
if not builder.IsDone():
|
||||||
|
raise RuntimeError("Axis scale transform failed.")
|
||||||
|
result = builder.Shape()
|
||||||
|
if result.IsNull():
|
||||||
|
raise RuntimeError("Axis scale transform produced an empty shape.")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _axis_dir_from_name(axis_name: str) -> gp_Dir:
|
def _axis_dir_from_name(axis_name: str) -> gp_Dir:
|
||||||
axis = axis_name.upper()
|
axis = axis_name.upper()
|
||||||
if axis == "X":
|
if axis == "X":
|
||||||
@@ -365,10 +417,23 @@ def _tuple_or_none(value: object) -> tuple[float, float, float] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _float_or_none(value: object) -> float | None:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _tuple_sub(left: tuple[float, float, float], right: tuple[float, float, float]) -> tuple[float, float, float]:
|
def _tuple_sub(left: tuple[float, float, float], right: tuple[float, float, float]) -> tuple[float, float, float]:
|
||||||
return (left[0] - right[0], left[1] - right[1], left[2] - right[2])
|
return (left[0] - right[0], left[1] - right[1], left[2] - right[2])
|
||||||
|
|
||||||
|
|
||||||
|
def _tuple_add(left: tuple[float, float, float], right: tuple[float, float, float]) -> tuple[float, float, float]:
|
||||||
|
return (left[0] + right[0], left[1] + right[1], left[2] + right[2])
|
||||||
|
|
||||||
|
|
||||||
def _tuple_scale(values: tuple[float, float, float], scale: float) -> tuple[float, float, float]:
|
def _tuple_scale(values: tuple[float, float, float], scale: float) -> tuple[float, float, float]:
|
||||||
return (values[0] * scale, values[1] * scale, values[2] * scale)
|
return (values[0] * scale, values[1] * scale, values[2] * scale)
|
||||||
|
|
||||||
@@ -937,7 +1002,7 @@ def _cylinder_depth_readiness(
|
|||||||
status = "caution"
|
status = "caution"
|
||||||
|
|
||||||
if not warnings and not blockers:
|
if not warnings and not blockers:
|
||||||
note = "可以尝试调整盲孔深度。"
|
note = "可以尝试调整盲孔/盲槽深度。"
|
||||||
else:
|
else:
|
||||||
note = " ".join(blockers + warnings)
|
note = " ".join(blockers + warnings)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class InfoPanelMixin:
|
|||||||
self.info_tabs.setCurrentWidget(self.info_tree)
|
self.info_tabs.setCurrentWidget(self.info_tree)
|
||||||
if hasattr(self, "_refresh_property_editor"):
|
if hasattr(self, "_refresh_property_editor"):
|
||||||
self._refresh_property_editor()
|
self._refresh_property_editor()
|
||||||
|
if hasattr(self, "_update_selected_object_title"):
|
||||||
|
self._update_selected_object_title()
|
||||||
if hasattr(self, "mode_combo"):
|
if hasattr(self, "mode_combo"):
|
||||||
self._update_action_states()
|
self._update_action_states()
|
||||||
|
|
||||||
|
|||||||
+160
-10
@@ -98,6 +98,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
self.edge_solid_ids: list[int] = []
|
self.edge_solid_ids: list[int] = []
|
||||||
self.solids: list[tuple[int, TopoDS_Shape]] = []
|
self.solids: list[tuple[int, TopoDS_Shape]] = []
|
||||||
self._face_info_cache: dict[int, dict[str, object]] = {}
|
self._face_info_cache: dict[int, dict[str, object]] = {}
|
||||||
|
self._feature_info_cache: dict[int, dict[str, object]] = {}
|
||||||
self._edge_info_cache: dict[int, dict[str, object]] = {}
|
self._edge_info_cache: dict[int, dict[str, object]] = {}
|
||||||
self._face_edge_ids_cache: dict[int, list[int]] = {}
|
self._face_edge_ids_cache: dict[int, list[int]] = {}
|
||||||
self._edge_face_ids_cache: dict[int, list[int]] = {}
|
self._edge_face_ids_cache: dict[int, list[int]] = {}
|
||||||
@@ -176,6 +177,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
self.edge_solid_ids.clear()
|
self.edge_solid_ids.clear()
|
||||||
self.solids.clear()
|
self.solids.clear()
|
||||||
self._face_info_cache.clear()
|
self._face_info_cache.clear()
|
||||||
|
self._feature_info_cache.clear()
|
||||||
self._edge_info_cache.clear()
|
self._edge_info_cache.clear()
|
||||||
self._face_edge_ids_cache.clear()
|
self._face_edge_ids_cache.clear()
|
||||||
self._edge_face_ids_cache.clear()
|
self._edge_face_ids_cache.clear()
|
||||||
@@ -268,6 +270,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
if isinstance(logical_ids, (list, tuple)) and len(logical_ids) == len(self.faces):
|
if isinstance(logical_ids, (list, tuple)) and len(logical_ids) == len(self.faces):
|
||||||
self.face_logical_ids = [int(item) for item in logical_ids]
|
self.face_logical_ids = [int(item) for item in logical_ids]
|
||||||
self._face_info_cache.clear()
|
self._face_info_cache.clear()
|
||||||
|
self._feature_info_cache.clear()
|
||||||
self._same_domain_face_ids_cache.clear()
|
self._same_domain_face_ids_cache.clear()
|
||||||
|
|
||||||
def face_info(self, face_id: int) -> dict[str, object]:
|
def face_info(self, face_id: int) -> dict[str, object]:
|
||||||
@@ -356,15 +359,54 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
self._face_info_cache[face_id] = dict(info)
|
self._face_info_cache[face_id] = dict(info)
|
||||||
return dict(info)
|
return dict(info)
|
||||||
|
|
||||||
|
def face_surface_kind(self, face_id: int) -> str:
|
||||||
|
if face_id < 0 or face_id >= len(self.faces):
|
||||||
|
return ""
|
||||||
|
cached = self._face_info_cache.get(face_id)
|
||||||
|
if cached is not None and cached.get("surface") is not None:
|
||||||
|
return str(cached.get("surface"))
|
||||||
|
try:
|
||||||
|
surf = BRepAdaptor_Surface(self.faces[face_id])
|
||||||
|
surface_type = surf.GetType()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
return SURFACE_TYPES.get(surface_type, f"type {surface_type}")
|
||||||
|
|
||||||
|
def face_cylinder_diameter(self, face_id: int) -> float | None:
|
||||||
|
if face_id < 0 or face_id >= len(self.faces):
|
||||||
|
return None
|
||||||
|
cached = self._face_info_cache.get(face_id)
|
||||||
|
if cached is not None and cached.get("diameter") is not None and cached.get("surface") == "cylinder":
|
||||||
|
try:
|
||||||
|
return float(cached["diameter"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
surf = BRepAdaptor_Surface(self.faces[face_id])
|
||||||
|
if surf.GetType() != GeomAbs_Cylinder:
|
||||||
|
return None
|
||||||
|
return float(surf.Cylinder().Radius()) * 2.0
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def feature_info(self, face_id: int) -> dict[str, object]:
|
def feature_info(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):
|
||||||
raise ValueError(f"Unknown face id {face_id}")
|
raise ValueError(f"Unknown face id {face_id}")
|
||||||
|
if face_id in self._feature_info_cache:
|
||||||
|
return dict(self._feature_info_cache[face_id])
|
||||||
info = self.face_info(face_id)
|
info = self.face_info(face_id)
|
||||||
surface = str(info.get("surface", ""))
|
surface = str(info.get("surface", ""))
|
||||||
if surface == "cylinder":
|
if surface == "cylinder":
|
||||||
return self._cylindrical_feature_info(face_id, info)
|
result = self._cylindrical_feature_info(face_id, info)
|
||||||
if surface == "plane":
|
elif surface == "plane":
|
||||||
return self._planar_feature_info(face_id, info)
|
result = self._planar_feature_info(face_id, info)
|
||||||
|
elif surface == "cone":
|
||||||
|
result = self._conical_feature_info(face_id, info)
|
||||||
|
elif surface == "sphere":
|
||||||
|
result = self._spherical_feature_info(face_id, info)
|
||||||
|
elif surface == "torus":
|
||||||
|
result = self._toroidal_feature_info(face_id, info)
|
||||||
|
else:
|
||||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||||
result = dict(info)
|
result = dict(info)
|
||||||
result.update(
|
result.update(
|
||||||
@@ -375,8 +417,82 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
"feature_face_ids": (face_id,),
|
"feature_face_ids": (face_id,),
|
||||||
"feature_highlight_face_ids": (face_id,),
|
"feature_highlight_face_ids": (face_id,),
|
||||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||||
"feature_edit_actions": "当前版本只能查看该局部曲面,暂不支持直接编辑。",
|
"feature_edit_actions": "当前只读;复杂曲面局部面积修改暂未开放",
|
||||||
"feature_mode": "Feature 模式会把选中的Face解释为局部几何特征候选。",
|
"feature_mode": (
|
||||||
|
"Feature 模式会把选中的Face解释为局部几何特征候选;"
|
||||||
|
"复杂曲面局部面积修改需要更明确的边界/约束重建,当前不会把面积伪装成直接可改参数。"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self._feature_info_cache[face_id] = dict(result)
|
||||||
|
return dict(result)
|
||||||
|
|
||||||
|
def _toroidal_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||||
|
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||||
|
major_radius = float(info.get("major_radius", 0.0) or 0.0)
|
||||||
|
minor_radius = float(info.get("minor_radius", 0.0) or 0.0)
|
||||||
|
result = dict(info)
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"feature_type": "环面候选",
|
||||||
|
"feature_source_face_id": face_id,
|
||||||
|
"feature_face_ids": (face_id,),
|
||||||
|
"feature_highlight_face_ids": (face_id,),
|
||||||
|
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||||
|
"feature_edit_actions": "修改环面主半径/小半径",
|
||||||
|
"feature_mode": (
|
||||||
|
"这是从 STEP/B-Rep 环面直接识别出的几何候选;当前修改会围绕环面中心缩放所属零件/Solid,"
|
||||||
|
"主半径和小半径会等比例变化,不是 CAD 历史里的管径或圆角参数。"
|
||||||
|
),
|
||||||
|
"feature_torus_major_radius": major_radius,
|
||||||
|
"feature_torus_minor_radius": minor_radius,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _spherical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||||
|
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||||
|
radius = float(info.get("radius", 0.0) or 0.0)
|
||||||
|
result = dict(info)
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"feature_type": "球面候选",
|
||||||
|
"feature_source_face_id": face_id,
|
||||||
|
"feature_face_ids": (face_id,),
|
||||||
|
"feature_highlight_face_ids": (face_id,),
|
||||||
|
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||||
|
"feature_edit_actions": "修改球面半径/直径",
|
||||||
|
"feature_mode": (
|
||||||
|
"这是从 STEP/B-Rep 球面直接识别出的几何候选;修改会围绕球心缩放所属零件/Solid,"
|
||||||
|
"不是 CAD 历史里的球面或圆角参数。"
|
||||||
|
),
|
||||||
|
"feature_sphere_radius": radius,
|
||||||
|
"feature_sphere_diameter": radius * 2.0 if radius > 0 else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _conical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||||
|
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||||
|
reference_radius = float(info.get("reference_radius", 0.0) or 0.0)
|
||||||
|
result = dict(info)
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"kind": "feature",
|
||||||
|
"feature_type": "圆锥面候选",
|
||||||
|
"feature_source_face_id": face_id,
|
||||||
|
"feature_face_ids": (face_id,),
|
||||||
|
"feature_highlight_face_ids": (face_id,),
|
||||||
|
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||||
|
"feature_reference_radius": reference_radius,
|
||||||
|
"feature_reference_diameter": reference_radius * 2.0 if reference_radius > 0 else "",
|
||||||
|
"feature_edit_actions": "修改圆锥参考半径/直径",
|
||||||
|
"feature_mode": (
|
||||||
|
"这是从 STEP/B-Rep 圆锥面直接识别出的几何候选;修改会围绕圆锥轴做径向缩放,"
|
||||||
|
"不是 CAD 历史里的锥孔或倒角参数。"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
@@ -549,12 +665,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
if guess == "hole/groove candidate":
|
if guess == "hole/groove candidate":
|
||||||
if angular_span < math.tau * 0.92:
|
if angular_span < math.tau * 0.92:
|
||||||
feature_type = "槽/半孔候选"
|
feature_type = "槽/半孔候选"
|
||||||
|
edit_actions = "调整圆柱孔径;调整槽/半孔宽度;调整槽/半孔深度;调整槽/半孔圆弧长度;调整槽/半孔圆弧角度;调整槽孔总长度"
|
||||||
else:
|
else:
|
||||||
feature_type = "圆柱孔候选"
|
feature_type = "圆柱孔候选"
|
||||||
|
edit_actions = "调整圆柱孔径"
|
||||||
if info.get("cylinder_end_type") == "blind" and bottom_face_ids:
|
if info.get("cylinder_end_type") == "blind" and bottom_face_ids:
|
||||||
edit_actions = "调整圆柱孔径;调整盲孔深度"
|
edit_actions += ";调整盲孔/盲槽深度"
|
||||||
else:
|
else:
|
||||||
edit_actions = "调整圆柱孔径;孔深调整需要明确盲孔底面"
|
edit_actions += ";孔深调整需要明确盲孔底面"
|
||||||
if angular_span >= math.tau * 0.92:
|
if angular_span >= math.tau * 0.92:
|
||||||
edit_actions += ";封堵圆柱孔"
|
edit_actions += ";封堵圆柱孔"
|
||||||
elif guess == "round/fillet candidate":
|
elif guess == "round/fillet candidate":
|
||||||
@@ -562,7 +680,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
edit_actions = "可尝试修改已有圆角半径;当前版本会先移除圆角面,再在恢复出的锐边上重新倒圆。"
|
edit_actions = "可尝试修改已有圆角半径;当前版本会先移除圆角面,再在恢复出的锐边上重新倒圆。"
|
||||||
elif guess == "boss/outer-round candidate":
|
elif guess == "boss/outer-round candidate":
|
||||||
feature_type = "凸台/外圆候选"
|
feature_type = "凸台/外圆候选"
|
||||||
edit_actions = "调整圆柱凸台直径。"
|
edit_actions = "调整圆柱凸台直径;调整圆柱凸台高度;修改圆柱凸台轴心坐标。"
|
||||||
else:
|
else:
|
||||||
feature_type = "未明确圆柱特征"
|
feature_type = "未明确圆柱特征"
|
||||||
edit_actions = "可尝试调整圆柱孔径,但风险较高。"
|
edit_actions = "可尝试调整圆柱孔径,但风险较高。"
|
||||||
@@ -704,6 +822,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
|
|
||||||
start_end_face_ids: list[int] = []
|
start_end_face_ids: list[int] = []
|
||||||
end_end_face_ids: list[int] = []
|
end_end_face_ids: list[int] = []
|
||||||
|
cap_scan_skipped = False
|
||||||
for adjacent_id in adjacent_face_ids:
|
for adjacent_id in adjacent_face_ids:
|
||||||
match = self._axis_end_match_for_planar_face(
|
match = self._axis_end_match_for_planar_face(
|
||||||
adjacent_id,
|
adjacent_id,
|
||||||
@@ -720,6 +839,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
end_end_face_ids.append(adjacent_id)
|
end_end_face_ids.append(adjacent_id)
|
||||||
|
|
||||||
if info.get("cylinder_end_type") == "blind" and (not start_end_face_ids or not end_end_face_ids):
|
if info.get("cylinder_end_type") == "blind" and (not start_end_face_ids or not end_end_face_ids):
|
||||||
|
if len(self.faces) <= 1000:
|
||||||
scanned = self._axis_cap_face_candidates(
|
scanned = self._axis_cap_face_candidates(
|
||||||
face_id,
|
face_id,
|
||||||
axis_point,
|
axis_point,
|
||||||
@@ -734,6 +854,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
start_end_face_ids.extend(scanned["start"])
|
start_end_face_ids.extend(scanned["start"])
|
||||||
if not end_end_face_ids:
|
if not end_end_face_ids:
|
||||||
end_end_face_ids.extend(scanned["end"])
|
end_end_face_ids.extend(scanned["end"])
|
||||||
|
else:
|
||||||
|
cap_scan_skipped = True
|
||||||
|
|
||||||
bottom_face_ids: list[int] = []
|
bottom_face_ids: list[int] = []
|
||||||
opening_face_ids: list[int] = []
|
opening_face_ids: list[int] = []
|
||||||
@@ -755,6 +877,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
bottom_confidence = "medium" if bottom_detection == "axis-cap-scan" else "high"
|
bottom_confidence = "medium" if bottom_detection == "axis-cap-scan" else "high"
|
||||||
if not end_face_ids:
|
if not end_face_ids:
|
||||||
note = "没有在圆柱边界附近找到平面端面。"
|
note = "没有在圆柱边界附近找到平面端面。"
|
||||||
|
if cap_scan_skipped:
|
||||||
|
note += " 当前模型 Face 数较多,已跳过全模型底面扫描以避免选择时卡顿;如需改盲孔/盲槽深度,可以手动填写底面 Face ID。"
|
||||||
elif bottom_face_ids:
|
elif bottom_face_ids:
|
||||||
if bottom_detection == "axis-cap-scan":
|
if bottom_detection == "axis-cap-scan":
|
||||||
note = "已通过轴线端部采样和轴线附近圆盘面扫描标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
|
note = "已通过轴线端部采样和轴线附近圆盘面扫描标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
|
||||||
@@ -762,6 +886,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
note = "已根据圆柱轴线端部 inside/outside 采样标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
|
note = "已根据圆柱轴线端部 inside/outside 采样标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
|
||||||
else:
|
else:
|
||||||
note = "已找到端面候选,但端部采样显示这些端面更像开口附近的相邻面。"
|
note = "已找到端面候选,但端部采样显示这些端面更像开口附近的相邻面。"
|
||||||
|
if cap_scan_skipped:
|
||||||
|
note += " 当前模型 Face 数较多,已跳过全模型底面扫描以避免选择时卡顿。"
|
||||||
return {
|
return {
|
||||||
"end_face_ids": end_face_ids,
|
"end_face_ids": end_face_ids,
|
||||||
"start_end_face_ids": sorted(set(start_end_face_ids)),
|
"start_end_face_ids": sorted(set(start_end_face_ids)),
|
||||||
@@ -905,6 +1031,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
for face_id in valid_face_ids:
|
for face_id in valid_face_ids:
|
||||||
self.face_logical_ids[face_id] = int(logical_id)
|
self.face_logical_ids[face_id] = int(logical_id)
|
||||||
self._face_info_cache.pop(face_id, None)
|
self._face_info_cache.pop(face_id, None)
|
||||||
|
self._feature_info_cache.pop(face_id, None)
|
||||||
|
|
||||||
def nearest_edge_id_to_point(
|
def nearest_edge_id_to_point(
|
||||||
self,
|
self,
|
||||||
@@ -961,9 +1088,17 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
return valid_face_ids[0]
|
return valid_face_ids[0]
|
||||||
|
|
||||||
point_vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(float(point[0]), float(point[1]), float(point[2]))).Vertex()
|
point_vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(float(point[0]), float(point[1]), float(point[2]))).Vertex()
|
||||||
|
exact_face_ids = valid_face_ids
|
||||||
|
if len(valid_face_ids) > 48:
|
||||||
|
scored_face_ids: list[tuple[float, int]] = []
|
||||||
|
for face_id in valid_face_ids:
|
||||||
|
scored_face_ids.append((self._face_bounds_distance_sq(face_id, point), face_id))
|
||||||
|
scored_face_ids.sort(key=lambda item: item[0])
|
||||||
|
exact_face_ids = [face_id for _distance, face_id in scored_face_ids[:48]]
|
||||||
|
|
||||||
best_face_id: int | None = None
|
best_face_id: int | None = None
|
||||||
best_distance = math.inf
|
best_distance = math.inf
|
||||||
for face_id in valid_face_ids:
|
for face_id in exact_face_ids:
|
||||||
try:
|
try:
|
||||||
extrema = BRepExtrema_DistShapeShape(point_vertex, self.faces[face_id])
|
extrema = BRepExtrema_DistShapeShape(point_vertex, self.faces[face_id])
|
||||||
extrema.Perform()
|
extrema.Perform()
|
||||||
@@ -981,7 +1116,22 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
|||||||
if distance < best_distance:
|
if distance < best_distance:
|
||||||
best_distance = distance
|
best_distance = distance
|
||||||
best_face_id = face_id
|
best_face_id = face_id
|
||||||
return best_face_id if best_face_id is not None else valid_face_ids[0]
|
return best_face_id if best_face_id is not None else exact_face_ids[0]
|
||||||
|
|
||||||
|
def _face_bounds_distance_sq(self, face_id: int, point: tuple[float, float, float]) -> float:
|
||||||
|
px, py, pz = (float(point[0]), float(point[1]), float(point[2]))
|
||||||
|
try:
|
||||||
|
xmin, ymin, zmin, xmax, ymax, zmax = _shape_bounds(self.faces[face_id])
|
||||||
|
dx = max(xmin - px, 0.0, px - xmax)
|
||||||
|
dy = max(ymin - py, 0.0, py - ymax)
|
||||||
|
dz = max(zmin - pz, 0.0, pz - zmax)
|
||||||
|
return dx * dx + dy * dy + dz * dz
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
center = _surface_center(self.faces[face_id])
|
||||||
|
return _point_distance_sq((px, py, pz), _point_tuple(center))
|
||||||
|
except Exception:
|
||||||
|
return math.inf
|
||||||
|
|
||||||
def _adjacent_face_ids_for_edges(self, edge_ids: list[int], face_id: int) -> list[int]:
|
def _adjacent_face_ids_for_edges(self, edge_ids: list[int], face_id: int) -> list[int]:
|
||||||
if not edge_ids:
|
if not edge_ids:
|
||||||
|
|||||||
+1917
-2
File diff suppressed because it is too large
Load Diff
@@ -262,3 +262,300 @@ class TransformMixin:
|
|||||||
f"risk={plan['risk']}."
|
f"risk={plan['risk']}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def scale_part_plan(self, part_id: int, target_diagonal: float) -> dict[str, object]:
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
if part is None:
|
||||||
|
raise ValueError(f"Unknown part id {part_id}")
|
||||||
|
return self._scale_shape_plan(
|
||||||
|
target_kind="part",
|
||||||
|
part_id=part.id,
|
||||||
|
solid_id=-1,
|
||||||
|
shape=part.shape,
|
||||||
|
target_diagonal=target_diagonal,
|
||||||
|
part_solid_count=len(_explore(part.shape, TopAbs_SOLID)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def scale_solid_plan(self, solid_id: int, target_diagonal: float) -> dict[str, object]:
|
||||||
|
if solid_id < 0 or solid_id >= len(self.solids):
|
||||||
|
raise ValueError(f"Unknown solid id {solid_id}")
|
||||||
|
part_id, solid = self.solids[solid_id]
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
|
||||||
|
plan = self._scale_shape_plan(
|
||||||
|
target_kind="solid",
|
||||||
|
part_id=part_id,
|
||||||
|
solid_id=solid_id,
|
||||||
|
shape=solid,
|
||||||
|
target_diagonal=target_diagonal,
|
||||||
|
part_solid_count=part_solid_count,
|
||||||
|
)
|
||||||
|
if part_solid_count <= 1 and plan["status"] != "blocked":
|
||||||
|
plan["warnings"] = _join_nonempty(
|
||||||
|
plan.get("warnings", ""),
|
||||||
|
"当前零件只有一个Solid,缩放Solid实际会缩放整个零件 shape。",
|
||||||
|
)
|
||||||
|
if plan["risk"] == "low":
|
||||||
|
plan["risk"] = "medium"
|
||||||
|
plan["status"] = "caution"
|
||||||
|
plan["message"] = _join_nonempty(plan.get("message", ""), plan.get("warnings", ""))
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def scale_part_axis_plan(self, part_id: int, axis: str, target_size: float) -> dict[str, object]:
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
if part is None:
|
||||||
|
raise ValueError(f"Unknown part id {part_id}")
|
||||||
|
return self._axis_scale_shape_plan(
|
||||||
|
target_kind="part",
|
||||||
|
part_id=part.id,
|
||||||
|
solid_id=-1,
|
||||||
|
shape=part.shape,
|
||||||
|
axis=axis,
|
||||||
|
target_size=target_size,
|
||||||
|
part_solid_count=len(_explore(part.shape, TopAbs_SOLID)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def scale_solid_axis_plan(self, solid_id: int, axis: str, target_size: float) -> dict[str, object]:
|
||||||
|
if solid_id < 0 or solid_id >= len(self.solids):
|
||||||
|
raise ValueError(f"Unknown solid id {solid_id}")
|
||||||
|
part_id, solid = self.solids[solid_id]
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
|
||||||
|
plan = self._axis_scale_shape_plan(
|
||||||
|
target_kind="solid",
|
||||||
|
part_id=part_id,
|
||||||
|
solid_id=solid_id,
|
||||||
|
shape=solid,
|
||||||
|
axis=axis,
|
||||||
|
target_size=target_size,
|
||||||
|
part_solid_count=part_solid_count,
|
||||||
|
)
|
||||||
|
if part_solid_count <= 1 and plan["status"] != "blocked":
|
||||||
|
plan["warnings"] = _join_nonempty(
|
||||||
|
plan.get("warnings", ""),
|
||||||
|
"当前零件只有一个Solid,按轴缩放Solid实际会缩放整个零件shape。",
|
||||||
|
)
|
||||||
|
if plan["risk"] == "low":
|
||||||
|
plan["risk"] = "medium"
|
||||||
|
plan["status"] = "caution"
|
||||||
|
plan["message"] = _join_nonempty(plan.get("message", ""), plan.get("warnings", ""))
|
||||||
|
return plan
|
||||||
|
|
||||||
|
def _scale_shape_plan(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
target_kind: str,
|
||||||
|
part_id: int,
|
||||||
|
solid_id: int,
|
||||||
|
shape: TopoDS_Shape,
|
||||||
|
target_diagonal: float,
|
||||||
|
part_solid_count: int,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
blockers: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
try:
|
||||||
|
target_diagonal = float(target_diagonal)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
target_diagonal = 0.0
|
||||||
|
blockers.append("目标包围盒对角线必须是数字。")
|
||||||
|
current_diagonal = _shape_diagonal(shape)
|
||||||
|
center = _shape_center(shape)
|
||||||
|
if current_diagonal <= 1e-9:
|
||||||
|
blockers.append("当前对象包围盒对角线无效,不能缩放。")
|
||||||
|
if target_diagonal <= 1e-9:
|
||||||
|
blockers.append("目标包围盒对角线必须大于 0。")
|
||||||
|
scale = target_diagonal / max(current_diagonal, 1e-9)
|
||||||
|
delta_ratio = abs(target_diagonal - current_diagonal) / max(current_diagonal, 1e-9)
|
||||||
|
risk = "low"
|
||||||
|
status = "ready"
|
||||||
|
if abs(target_diagonal - current_diagonal) <= max(current_diagonal * 1e-6, 1e-6):
|
||||||
|
blockers.append("目标包围盒对角线与当前值几乎相同,不需要缩放。")
|
||||||
|
elif delta_ratio > 1.0:
|
||||||
|
risk = "high"
|
||||||
|
status = "caution"
|
||||||
|
warnings.append("目标尺寸变化超过当前包围盒对角线的 100%,请确认单位和目标。")
|
||||||
|
elif delta_ratio > 0.35:
|
||||||
|
risk = "medium"
|
||||||
|
status = "caution"
|
||||||
|
warnings.append("目标尺寸变化超过当前包围盒对角线的 35%,请确认缩放结果。")
|
||||||
|
if blockers:
|
||||||
|
risk = "blocked"
|
||||||
|
status = "blocked"
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"risk": risk,
|
||||||
|
"message": _join_nonempty(" ".join(blockers), " ".join(warnings)) or "可以按目标包围盒对角线等比缩放当前对象。",
|
||||||
|
"warnings": ";".join(warnings),
|
||||||
|
"blockers": ";".join(blockers),
|
||||||
|
"target_kind": target_kind,
|
||||||
|
"part_id": part_id,
|
||||||
|
"solid_id": solid_id,
|
||||||
|
"part_solid_count": part_solid_count,
|
||||||
|
"current_bbox_diagonal": current_diagonal,
|
||||||
|
"target_bbox_diagonal": target_diagonal,
|
||||||
|
"scale_factor": scale,
|
||||||
|
"scale_delta_ratio": delta_ratio,
|
||||||
|
"scale_center": center,
|
||||||
|
"resize_strategy": "uniform-scale-selected-shape-to-bbox-diagonal",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _axis_scale_shape_plan(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
target_kind: str,
|
||||||
|
part_id: int,
|
||||||
|
solid_id: int,
|
||||||
|
shape: TopoDS_Shape,
|
||||||
|
axis: str,
|
||||||
|
target_size: float,
|
||||||
|
part_solid_count: int,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
axis = str(axis or "X").strip().upper()
|
||||||
|
axis_index = {"X": 0, "Y": 1, "Z": 2}.get(axis)
|
||||||
|
blockers: list[str] = []
|
||||||
|
warnings: list[str] = [
|
||||||
|
"按单轴尺寸缩放是 B-Rep 仿射编辑,可能把解析几何转换成 B-spline 或影响同一对象上的其它特征。"
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
target_size = float(target_size)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
target_size = 0.0
|
||||||
|
blockers.append("目标轴向尺寸必须是数字。")
|
||||||
|
bounds = _shape_bounds_info(shape)
|
||||||
|
bbox_size = tuple(bounds.get("bbox_size", (0.0, 0.0, 0.0)))
|
||||||
|
current_size = float(bbox_size[axis_index]) if axis_index is not None else 0.0
|
||||||
|
center = _shape_center(shape)
|
||||||
|
if axis_index is None:
|
||||||
|
blockers.append("缩放轴必须是 X、Y 或 Z。")
|
||||||
|
if current_size <= 1e-9:
|
||||||
|
blockers.append("当前对象在该轴向的包围盒尺寸无效,不能按轴缩放。")
|
||||||
|
if target_size <= 1e-9:
|
||||||
|
blockers.append("目标轴向尺寸必须大于 0。")
|
||||||
|
scale = target_size / max(current_size, 1e-9)
|
||||||
|
delta_ratio = abs(target_size - current_size) / max(current_size, 1e-9)
|
||||||
|
risk = "medium"
|
||||||
|
status = "caution"
|
||||||
|
if abs(target_size - current_size) <= max(current_size * 1e-6, 1e-6):
|
||||||
|
blockers.append("目标轴向尺寸与当前值几乎相同,不需要缩放。")
|
||||||
|
elif delta_ratio > 1.0:
|
||||||
|
risk = "high"
|
||||||
|
warnings.append("目标轴向尺寸变化超过当前尺寸的 100%,请确认单位和目标。")
|
||||||
|
elif delta_ratio <= 0.2:
|
||||||
|
risk = "medium"
|
||||||
|
if blockers:
|
||||||
|
risk = "blocked"
|
||||||
|
status = "blocked"
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"risk": risk,
|
||||||
|
"message": _join_nonempty(" ".join(blockers), " ".join(warnings)) or "可以按目标轴向尺寸缩放当前对象。",
|
||||||
|
"warnings": ";".join(warnings),
|
||||||
|
"blockers": ";".join(blockers),
|
||||||
|
"target_kind": target_kind,
|
||||||
|
"part_id": part_id,
|
||||||
|
"solid_id": solid_id,
|
||||||
|
"part_solid_count": part_solid_count,
|
||||||
|
"scale_axis": axis,
|
||||||
|
"current_axis_size": current_size,
|
||||||
|
"target_axis_size": target_size,
|
||||||
|
"axis_scale_factor": scale,
|
||||||
|
"axis_scale_delta_ratio": delta_ratio,
|
||||||
|
"scale_center": center,
|
||||||
|
"bbox_size": bbox_size,
|
||||||
|
"resize_strategy": "axis-scale-selected-shape-to-bbox-size",
|
||||||
|
}
|
||||||
|
|
||||||
|
def scale_part(self, part_id: int, target_diagonal: float) -> str:
|
||||||
|
plan = self.scale_part_plan(part_id, target_diagonal)
|
||||||
|
if plan["status"] == "blocked":
|
||||||
|
raise ValueError(str(plan["message"]))
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
if part is None:
|
||||||
|
raise ValueError(f"Unknown part id {part_id}")
|
||||||
|
part.shape = _scaled_shape(part.shape, float(plan["scale_factor"]), plan["scale_center"])
|
||||||
|
_ensure_valid_shape(part.shape)
|
||||||
|
self.refresh_topology()
|
||||||
|
return (
|
||||||
|
f"Part scaled: part {part_id}, "
|
||||||
|
f"bbox_diagonal={float(plan['current_bbox_diagonal']):g}->{float(plan['target_bbox_diagonal']):g}, "
|
||||||
|
f"scale={float(plan['scale_factor']):g}, risk={plan['risk']}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def scale_part_axis(self, part_id: int, axis: str, target_size: float) -> str:
|
||||||
|
plan = self.scale_part_axis_plan(part_id, axis, target_size)
|
||||||
|
if plan["status"] == "blocked":
|
||||||
|
raise ValueError(str(plan["message"]))
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
if part is None:
|
||||||
|
raise ValueError(f"Unknown part id {part_id}")
|
||||||
|
part.shape = _axis_scaled_shape(part.shape, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
|
||||||
|
_ensure_valid_shape(part.shape)
|
||||||
|
self.refresh_topology()
|
||||||
|
return (
|
||||||
|
f"Part axis-scaled: part {part_id}, axis={plan['scale_axis']}, "
|
||||||
|
f"size={float(plan['current_axis_size']):g}->{float(plan['target_axis_size']):g}, "
|
||||||
|
f"scale={float(plan['axis_scale_factor']):g}, risk={plan['risk']}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def scale_solid(self, solid_id: int, target_diagonal: float) -> str:
|
||||||
|
plan = self.scale_solid_plan(solid_id, target_diagonal)
|
||||||
|
if plan["status"] == "blocked":
|
||||||
|
raise ValueError(str(plan["message"]))
|
||||||
|
part_id, solid = self.solids[solid_id]
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
if part is None:
|
||||||
|
raise ValueError(f"Unknown part id {part_id}")
|
||||||
|
part_solids = _explore(part.shape, TopAbs_SOLID)
|
||||||
|
if len(part_solids) <= 1:
|
||||||
|
part.shape = _scaled_shape(part.shape, float(plan["scale_factor"]), plan["scale_center"])
|
||||||
|
else:
|
||||||
|
scaled = _scaled_shape(solid, float(plan["scale_factor"]), plan["scale_center"])
|
||||||
|
replaced = False
|
||||||
|
shapes: list[TopoDS_Shape] = []
|
||||||
|
for item in part_solids:
|
||||||
|
if not replaced and _same_shape(item, solid):
|
||||||
|
shapes.append(scaled)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
shapes.append(item)
|
||||||
|
if not replaced:
|
||||||
|
raise RuntimeError(f"Could not locate solid {solid_id} inside part {part_id}.")
|
||||||
|
part.shape = _compound_from_shapes(shapes)
|
||||||
|
_ensure_valid_shape(part.shape)
|
||||||
|
self.refresh_topology()
|
||||||
|
return (
|
||||||
|
f"Solid scaled: solid {solid_id}, part {part_id}, "
|
||||||
|
f"bbox_diagonal={float(plan['current_bbox_diagonal']):g}->{float(plan['target_bbox_diagonal']):g}, "
|
||||||
|
f"scale={float(plan['scale_factor']):g}, risk={plan['risk']}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def scale_solid_axis(self, solid_id: int, axis: str, target_size: float) -> str:
|
||||||
|
plan = self.scale_solid_axis_plan(solid_id, axis, target_size)
|
||||||
|
if plan["status"] == "blocked":
|
||||||
|
raise ValueError(str(plan["message"]))
|
||||||
|
part_id, solid = self.solids[solid_id]
|
||||||
|
part = self.part_by_id(part_id)
|
||||||
|
if part is None:
|
||||||
|
raise ValueError(f"Unknown part id {part_id}")
|
||||||
|
part_solids = _explore(part.shape, TopAbs_SOLID)
|
||||||
|
if len(part_solids) <= 1:
|
||||||
|
part.shape = _axis_scaled_shape(part.shape, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
|
||||||
|
else:
|
||||||
|
scaled = _axis_scaled_shape(solid, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
|
||||||
|
replaced = False
|
||||||
|
shapes: list[TopoDS_Shape] = []
|
||||||
|
for item in part_solids:
|
||||||
|
if not replaced and _same_shape(item, solid):
|
||||||
|
shapes.append(scaled)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
shapes.append(item)
|
||||||
|
if not replaced:
|
||||||
|
raise RuntimeError(f"Could not locate solid {solid_id} inside part {part_id}.")
|
||||||
|
part.shape = _compound_from_shapes(shapes)
|
||||||
|
_ensure_valid_shape(part.shape)
|
||||||
|
self.refresh_topology()
|
||||||
|
return (
|
||||||
|
f"Solid axis-scaled: solid {solid_id}, part {part_id}, axis={plan['scale_axis']}, "
|
||||||
|
f"size={float(plan['current_axis_size']):g}->{float(plan['target_axis_size']):g}, "
|
||||||
|
f"scale={float(plan['axis_scale_factor']):g}, risk={plan['risk']}."
|
||||||
|
)
|
||||||
|
|||||||
@@ -459,6 +459,8 @@ INFO_LABELS = {
|
|||||||
"target_to_height_ratio": "目标直径/估算高度",
|
"target_to_height_ratio": "目标直径/估算高度",
|
||||||
"major_radius": "主半径",
|
"major_radius": "主半径",
|
||||||
"minor_radius": "小半径",
|
"minor_radius": "小半径",
|
||||||
|
"ellipse_edge_major_radius": "椭圆Edge主半径",
|
||||||
|
"ellipse_edge_minor_radius": "椭圆Edge小半径",
|
||||||
"reference_radius": "参考半径",
|
"reference_radius": "参考半径",
|
||||||
"semi_angle": "半角",
|
"semi_angle": "半角",
|
||||||
"angular_span": "角度跨度",
|
"angular_span": "角度跨度",
|
||||||
@@ -583,6 +585,28 @@ INFO_LABELS = {
|
|||||||
"existing_fillet_kind": "已有圆角类型",
|
"existing_fillet_kind": "已有圆角类型",
|
||||||
"existing_fillet_status": "已有圆角识别状态",
|
"existing_fillet_status": "已有圆角识别状态",
|
||||||
"existing_fillet_radius_estimate": "已有圆角半径估算",
|
"existing_fillet_radius_estimate": "已有圆角半径估算",
|
||||||
|
"feature_reference_radius": "特征参考半径",
|
||||||
|
"feature_reference_diameter": "特征参考直径",
|
||||||
|
"feature_sphere_radius": "球面半径",
|
||||||
|
"feature_sphere_diameter": "球面直径",
|
||||||
|
"feature_torus_major_radius": "环面主半径",
|
||||||
|
"feature_torus_minor_radius": "环面小半径",
|
||||||
|
"current_major_radius": "当前主半径",
|
||||||
|
"target_major_radius": "目标主半径",
|
||||||
|
"current_minor_radius": "当前小半径",
|
||||||
|
"target_minor_radius": "目标小半径",
|
||||||
|
"torus_radius_mode": "环面半径模式",
|
||||||
|
"current_area": "当前面积",
|
||||||
|
"target_area": "目标面积",
|
||||||
|
"area_delta": "面积变化量",
|
||||||
|
"area_delta_ratio": "面积变化比例",
|
||||||
|
"area_center": "面积中心",
|
||||||
|
"current_reference_radius": "当前参考半径",
|
||||||
|
"target_reference_radius": "目标参考半径",
|
||||||
|
"current_reference_diameter": "当前参考直径",
|
||||||
|
"target_reference_diameter": "目标参考直径",
|
||||||
|
"delta_reference_radius": "参考半径变化量",
|
||||||
|
"reference_radius_delta_ratio": "参考半径变化比例",
|
||||||
"existing_fillet_angular_span": "已有圆角圆弧角度",
|
"existing_fillet_angular_span": "已有圆角圆弧角度",
|
||||||
"existing_fillet_arc_length_estimate": "已有圆角圆弧长度估算",
|
"existing_fillet_arc_length_estimate": "已有圆角圆弧长度估算",
|
||||||
"existing_fillet_note": "已有圆角识别说明",
|
"existing_fillet_note": "已有圆角识别说明",
|
||||||
|
|||||||
+1931
-16
File diff suppressed because it is too large
Load Diff
+261
-23
@@ -43,15 +43,35 @@ class WindowCoreMixin:
|
|||||||
if watched is getattr(self, "vtk_widget", None):
|
if watched is getattr(self, "vtk_widget", None):
|
||||||
event_type = event.type()
|
event_type = event.type()
|
||||||
if event_type == QEvent.Type.MouseButtonRelease:
|
if event_type == QEvent.Type.MouseButtonRelease:
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
x, y = self._vtk_position_from_qt_event(event)
|
||||||
|
self.skip_next_vtk_left_release = True
|
||||||
|
self._handle_left_button_release(x, y)
|
||||||
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
|
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
|
||||||
elif event_type == QEvent.Type.MouseButtonPress:
|
elif event_type == QEvent.Type.MouseButtonPress:
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
x, y = self._vtk_position_from_qt_event(event)
|
||||||
|
self.skip_next_vtk_left_press = True
|
||||||
|
self._handle_left_button_press(x, y)
|
||||||
self.pointer_button_down = True
|
self.pointer_button_down = True
|
||||||
elif event_type == QEvent.Type.MouseMove:
|
elif event_type == QEvent.Type.MouseMove:
|
||||||
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
|
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
|
||||||
|
if event.buttons() & Qt.MouseButton.LeftButton:
|
||||||
|
x, y = self._vtk_position_from_qt_event(event)
|
||||||
|
self._update_left_button_drag_state(x, y)
|
||||||
if not self.pointer_button_down:
|
if not self.pointer_button_down:
|
||||||
self._queue_hover_from_qt_event(event)
|
self._queue_hover_from_qt_event(event)
|
||||||
|
elif event_type == QEvent.Type.Leave:
|
||||||
|
self._cancel_hover_tracking(render=True)
|
||||||
return super().eventFilter(watched, event)
|
return super().eventFilter(watched, event)
|
||||||
|
|
||||||
|
def _vtk_position_from_qt_event(self, event) -> tuple[int, int]:
|
||||||
|
position = event.position() if hasattr(event, "position") else event.pos()
|
||||||
|
scale = self.vtk_widget._getPixelRatio() if hasattr(self.vtk_widget, "_getPixelRatio") else 1.0
|
||||||
|
x = int(round(float(position.x()) * scale))
|
||||||
|
y = int(round((float(self.vtk_widget.height()) - float(position.y()) - 1.0) * scale))
|
||||||
|
return x, y
|
||||||
|
|
||||||
def _build_vtk(self) -> None:
|
def _build_vtk(self) -> None:
|
||||||
self.renderer = vtk.vtkRenderer()
|
self.renderer = vtk.vtkRenderer()
|
||||||
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
|
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
|
||||||
@@ -62,13 +82,19 @@ class WindowCoreMixin:
|
|||||||
self.interactor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
|
self.interactor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
|
||||||
self.picker = vtk.vtkCellPicker()
|
self.picker = vtk.vtkCellPicker()
|
||||||
self.picker.SetTolerance(0.003)
|
self.picker.SetTolerance(0.003)
|
||||||
self.interactor.AddObserver("LeftButtonPressEvent", self.on_left_click)
|
self.interactor.AddObserver("LeftButtonPressEvent", self.on_left_button_press)
|
||||||
self.interactor.AddObserver("LeftButtonReleaseEvent", self.on_pointer_button_release)
|
self.interactor.AddObserver("LeftButtonReleaseEvent", self.on_left_button_release)
|
||||||
self.interactor.AddObserver("MiddleButtonPressEvent", self.on_pointer_button_press)
|
self.interactor.AddObserver("MiddleButtonPressEvent", self.on_pointer_button_press)
|
||||||
self.interactor.AddObserver("MiddleButtonReleaseEvent", self.on_pointer_button_release)
|
self.interactor.AddObserver("MiddleButtonReleaseEvent", self.on_pointer_button_release)
|
||||||
self.interactor.AddObserver("RightButtonPressEvent", self.on_pointer_button_press)
|
self.interactor.AddObserver("RightButtonPressEvent", self.on_pointer_button_press)
|
||||||
self.interactor.AddObserver("RightButtonReleaseEvent", self.on_pointer_button_release)
|
self.interactor.AddObserver("RightButtonReleaseEvent", self.on_pointer_button_release)
|
||||||
self.interactor.AddObserver("MouseMoveEvent", self.on_mouse_move)
|
self.interactor.AddObserver("MouseMoveEvent", self.on_mouse_move)
|
||||||
|
self.interactor.AddObserver("StartInteractionEvent", self.on_camera_interaction_start)
|
||||||
|
self.interactor.AddObserver("EndInteractionEvent", self.on_camera_interaction_end)
|
||||||
|
if hasattr(self.interactor, "SetDesiredUpdateRate"):
|
||||||
|
self.interactor.SetDesiredUpdateRate(24.0)
|
||||||
|
if hasattr(self.interactor, "SetStillUpdateRate"):
|
||||||
|
self.interactor.SetStillUpdateRate(0.2)
|
||||||
|
|
||||||
light = vtk.vtkLight()
|
light = vtk.vtkLight()
|
||||||
light.SetLightTypeToHeadlight()
|
light.SetLightTypeToHeadlight()
|
||||||
@@ -78,6 +104,55 @@ class WindowCoreMixin:
|
|||||||
|
|
||||||
self.interactor.Initialize()
|
self.interactor.Initialize()
|
||||||
|
|
||||||
|
def on_camera_interaction_start(self, _obj, _event) -> None:
|
||||||
|
if not self._is_ui_thread():
|
||||||
|
self._invoke_on_ui_thread(self._begin_camera_interaction)
|
||||||
|
return
|
||||||
|
self._begin_camera_interaction()
|
||||||
|
|
||||||
|
def on_camera_interaction_end(self, _obj, _event) -> None:
|
||||||
|
if not self._is_ui_thread():
|
||||||
|
self._invoke_on_ui_thread(self._end_camera_interaction)
|
||||||
|
return
|
||||||
|
self._end_camera_interaction()
|
||||||
|
|
||||||
|
def _begin_camera_interaction(self) -> None:
|
||||||
|
if getattr(self, "camera_interaction_active", False):
|
||||||
|
return
|
||||||
|
self.camera_interaction_active = True
|
||||||
|
self._cancel_hover_tracking(render=False)
|
||||||
|
if self.edge_actor is not None:
|
||||||
|
try:
|
||||||
|
self.edge_actor_visible_before_interaction = bool(self.edge_actor.GetVisibility())
|
||||||
|
self.edge_actor.SetVisibility(False)
|
||||||
|
except Exception:
|
||||||
|
self.edge_actor_visible_before_interaction = True
|
||||||
|
if self.model_actor is not None:
|
||||||
|
try:
|
||||||
|
prop = self.model_actor.GetProperty()
|
||||||
|
self.model_interpolation_before_interaction = int(prop.GetInterpolation())
|
||||||
|
prop.SetInterpolationToGouraud()
|
||||||
|
except Exception:
|
||||||
|
self.model_interpolation_before_interaction = None
|
||||||
|
|
||||||
|
def _end_camera_interaction(self) -> None:
|
||||||
|
if not getattr(self, "camera_interaction_active", False):
|
||||||
|
return
|
||||||
|
self.camera_interaction_active = False
|
||||||
|
if self.edge_actor is not None:
|
||||||
|
try:
|
||||||
|
self.edge_actor.SetVisibility(bool(getattr(self, "edge_actor_visible_before_interaction", True)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if self.model_actor is not None and self.model_interpolation_before_interaction is not None:
|
||||||
|
try:
|
||||||
|
self.model_actor.GetProperty().SetInterpolation(int(self.model_interpolation_before_interaction))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.model_interpolation_before_interaction = None
|
||||||
|
if hasattr(self, "render_window"):
|
||||||
|
self.render_window.Render()
|
||||||
|
|
||||||
def closeEvent(self, event) -> None:
|
def closeEvent(self, event) -> None:
|
||||||
load_thread_running = bool(
|
load_thread_running = bool(
|
||||||
(self.load_thread is not None and self.load_thread.isRunning())
|
(self.load_thread is not None and self.load_thread.isRunning())
|
||||||
@@ -839,18 +914,73 @@ class WindowCoreMixin:
|
|||||||
if hasattr(self, "_update_id_select_title"):
|
if hasattr(self, "_update_id_select_title"):
|
||||||
self._update_id_select_title(mode)
|
self._update_id_select_title(mode)
|
||||||
|
|
||||||
def on_left_click(self, _obj, _event) -> None:
|
def _cancel_hover_tracking(self, render: bool = True) -> None:
|
||||||
|
self.pending_hover_position = None
|
||||||
|
self.last_hover_pick_position = None
|
||||||
|
if hasattr(self, "hover_timer"):
|
||||||
|
self.hover_timer.stop()
|
||||||
|
self._clear_hover(render=render)
|
||||||
|
|
||||||
|
def on_left_button_press(self, _obj, _event) -> None:
|
||||||
|
if getattr(self, "skip_next_vtk_left_press", False):
|
||||||
|
self.skip_next_vtk_left_press = False
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
x, y = self.interactor.GetEventPosition()
|
x, y = self.interactor.GetEventPosition()
|
||||||
except Exception:
|
except Exception:
|
||||||
x, y = 0, 0
|
x, y = 0, 0
|
||||||
if not self._is_ui_thread():
|
if not self._is_ui_thread():
|
||||||
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._handle_left_click(x, y))
|
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._handle_left_button_press(x, y))
|
||||||
return
|
return
|
||||||
self._handle_left_click(int(x), int(y))
|
self._handle_left_button_press(int(x), int(y))
|
||||||
|
|
||||||
|
def _handle_left_button_press(self, x: int, y: int) -> None:
|
||||||
|
self.pointer_button_down = True
|
||||||
|
self.left_button_press_position = (int(x), int(y))
|
||||||
|
self.left_button_dragged = False
|
||||||
|
self.pending_hover_position = None
|
||||||
|
self.last_hover_pick_position = None
|
||||||
|
self.hover_timer.stop()
|
||||||
|
self._clear_hover(render=True)
|
||||||
|
|
||||||
|
def on_left_button_release(self, _obj, _event) -> None:
|
||||||
|
if getattr(self, "skip_next_vtk_left_release", False):
|
||||||
|
self.skip_next_vtk_left_release = False
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
x, y = self.interactor.GetEventPosition()
|
||||||
|
except Exception:
|
||||||
|
x, y = 0, 0
|
||||||
|
if not self._is_ui_thread():
|
||||||
|
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._handle_left_button_release(x, y))
|
||||||
|
return
|
||||||
|
self._handle_left_button_release(int(x), int(y))
|
||||||
|
|
||||||
|
def _handle_left_button_release(self, x: int, y: int) -> None:
|
||||||
|
self._update_left_button_drag_state(x, y)
|
||||||
|
was_dragged = bool(getattr(self, "left_button_dragged", False))
|
||||||
|
self.pointer_button_down = False
|
||||||
|
self.left_button_press_position = None
|
||||||
|
self.left_button_dragged = False
|
||||||
|
self.pending_hover_position = None
|
||||||
|
self.last_hover_pick_position = None
|
||||||
|
if getattr(self, "camera_interaction_active", False):
|
||||||
|
self._end_camera_interaction()
|
||||||
|
if was_dragged:
|
||||||
|
return
|
||||||
|
self._handle_left_click(x, y)
|
||||||
|
|
||||||
|
def _update_left_button_drag_state(self, x: int, y: int) -> None:
|
||||||
|
press_position = getattr(self, "left_button_press_position", None)
|
||||||
|
if press_position is None:
|
||||||
|
return
|
||||||
|
threshold = int(getattr(self, "left_click_drag_threshold_px", 6) or 6)
|
||||||
|
dx = abs(int(x) - int(press_position[0]))
|
||||||
|
dy = abs(int(y) - int(press_position[1]))
|
||||||
|
if max(dx, dy) > threshold:
|
||||||
|
self.left_button_dragged = True
|
||||||
|
|
||||||
def _handle_left_click(self, x: int, y: int) -> None:
|
def _handle_left_click(self, x: int, y: int) -> None:
|
||||||
self.pointer_button_down = True
|
|
||||||
self.pending_hover_position = None
|
self.pending_hover_position = None
|
||||||
self.last_hover_pick_position = None
|
self.last_hover_pick_position = None
|
||||||
self.hover_timer.stop()
|
self.hover_timer.stop()
|
||||||
@@ -868,7 +998,11 @@ class WindowCoreMixin:
|
|||||||
mode = self._current_selection_mode()
|
mode = self._current_selection_mode()
|
||||||
target = self._pick_selection_target(mode, x, y)
|
target = self._pick_selection_target(mode, x, y)
|
||||||
if target is None:
|
if target is None:
|
||||||
self.statusBar().showMessage("未选中对象")
|
self._cancel_hover_tracking(render=False)
|
||||||
|
self._reset_selection(clear_highlight=True)
|
||||||
|
if hasattr(self, "render_window"):
|
||||||
|
self.render_window.Render()
|
||||||
|
self.statusBar().showMessage("已取消选择")
|
||||||
return
|
return
|
||||||
|
|
||||||
self._clear_hover(render=False)
|
self._clear_hover(render=False)
|
||||||
@@ -897,6 +1031,7 @@ class WindowCoreMixin:
|
|||||||
self.pointer_button_down = False
|
self.pointer_button_down = False
|
||||||
self.pending_hover_position = None
|
self.pending_hover_position = None
|
||||||
self.last_hover_pick_position = None
|
self.last_hover_pick_position = None
|
||||||
|
self._end_camera_interaction()
|
||||||
|
|
||||||
def on_mouse_move(self, _obj, _event) -> None:
|
def on_mouse_move(self, _obj, _event) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -909,8 +1044,15 @@ class WindowCoreMixin:
|
|||||||
self._handle_mouse_move(int(x), int(y))
|
self._handle_mouse_move(int(x), int(y))
|
||||||
|
|
||||||
def _handle_mouse_move(self, x: int, y: int) -> None:
|
def _handle_mouse_move(self, x: int, y: int) -> None:
|
||||||
if QApplication.mouseButtons() != Qt.MouseButton.NoButton:
|
buttons = QApplication.mouseButtons()
|
||||||
|
if buttons != Qt.MouseButton.NoButton:
|
||||||
self.pointer_button_down = True
|
self.pointer_button_down = True
|
||||||
|
if buttons & Qt.MouseButton.LeftButton:
|
||||||
|
self._update_left_button_drag_state(x, y)
|
||||||
|
if getattr(self, "left_button_dragged", False):
|
||||||
|
self._begin_camera_interaction()
|
||||||
|
else:
|
||||||
|
self._begin_camera_interaction()
|
||||||
return
|
return
|
||||||
self.pointer_button_down = False
|
self.pointer_button_down = False
|
||||||
if (
|
if (
|
||||||
@@ -934,10 +1076,7 @@ class WindowCoreMixin:
|
|||||||
):
|
):
|
||||||
self._clear_hover(render=True)
|
self._clear_hover(render=True)
|
||||||
return
|
return
|
||||||
position = event.position() if hasattr(event, "position") else event.pos()
|
x, y = self._vtk_position_from_qt_event(event)
|
||||||
scale = self.vtk_widget._getPixelRatio() if hasattr(self.vtk_widget, "_getPixelRatio") else 1.0
|
|
||||||
x = int(round(float(position.x()) * scale))
|
|
||||||
y = int(round((float(self.vtk_widget.height()) - float(position.y()) - 1.0) * scale))
|
|
||||||
self._queue_hover_position(x, y)
|
self._queue_hover_position(x, y)
|
||||||
|
|
||||||
def _queue_hover_position(self, x: int, y: int) -> None:
|
def _queue_hover_position(self, x: int, y: int) -> None:
|
||||||
@@ -1213,10 +1352,26 @@ class WindowCoreMixin:
|
|||||||
self._set_selection_mode("Feature")
|
self._set_selection_mode("Feature")
|
||||||
self.select_feature(int(target_id))
|
self.select_feature(int(target_id))
|
||||||
self.statusBar().showMessage(f"已选择可调整凸台直径候选Face {target_id}")
|
self.statusBar().showMessage(f"已选择可调整凸台直径候选Face {target_id}")
|
||||||
|
elif action == "resize_boss_height":
|
||||||
|
self._set_selection_mode("Feature")
|
||||||
|
self.select_feature(int(target_id))
|
||||||
|
self.statusBar().showMessage(f"已选择可调整凸台高度候选Face {target_id}")
|
||||||
|
elif action == "move_boss_axis":
|
||||||
|
self._set_selection_mode("Feature")
|
||||||
|
self.select_feature(int(target_id))
|
||||||
|
self.statusBar().showMessage(f"已选择可移动凸台轴心候选Face {target_id}")
|
||||||
elif action == "resize_slot_width":
|
elif action == "resize_slot_width":
|
||||||
self._set_selection_mode("Feature")
|
self._set_selection_mode("Feature")
|
||||||
self.select_feature(int(target_id))
|
self.select_feature(int(target_id))
|
||||||
self.statusBar().showMessage(f"已选择可调整槽/半孔宽度候选Face {target_id}")
|
self.statusBar().showMessage(f"已选择可调整槽/半孔宽度候选Face {target_id}")
|
||||||
|
elif action == "resize_slot_depth":
|
||||||
|
self._set_selection_mode("Feature")
|
||||||
|
self.select_feature(int(target_id))
|
||||||
|
self.statusBar().showMessage(f"已选择可调整槽/半孔深度候选Face {target_id}")
|
||||||
|
elif action == "resize_slot_arc_length":
|
||||||
|
self._set_selection_mode("Feature")
|
||||||
|
self.select_feature(int(target_id))
|
||||||
|
self.statusBar().showMessage(f"已选择可调整槽/半孔圆弧长度候选Face {target_id}")
|
||||||
elif action == "resize_shell_thickness":
|
elif action == "resize_shell_thickness":
|
||||||
self._set_selection_mode("Feature")
|
self._set_selection_mode("Feature")
|
||||||
self.select_feature(int(target_id))
|
self.select_feature(int(target_id))
|
||||||
@@ -1228,7 +1383,7 @@ class WindowCoreMixin:
|
|||||||
elif action == "resize_depth":
|
elif action == "resize_depth":
|
||||||
self._set_selection_mode("Feature")
|
self._set_selection_mode("Feature")
|
||||||
self.select_feature(int(target_id))
|
self.select_feature(int(target_id))
|
||||||
self.statusBar().showMessage(f"已选择可调整盲孔深度候选Face {target_id}")
|
self.statusBar().showMessage(f"已选择可调整盲孔/盲槽深度候选Face {target_id}")
|
||||||
elif action == "inspect_existing_fillet":
|
elif action == "inspect_existing_fillet":
|
||||||
self._set_selection_mode("Feature")
|
self._set_selection_mode("Feature")
|
||||||
self.select_feature(int(target_id))
|
self.select_feature(int(target_id))
|
||||||
@@ -1305,7 +1460,7 @@ class WindowCoreMixin:
|
|||||||
if self._edit_busy("编辑计算中,暂时不能选择零件。"):
|
if self._edit_busy("编辑计算中,暂时不能选择零件。"):
|
||||||
return
|
return
|
||||||
info = self.model.part_info(part_id)
|
info = self.model.part_info(part_id)
|
||||||
self._reset_selection(clear_highlight=False)
|
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||||
self.selected_kind = "part"
|
self.selected_kind = "part"
|
||||||
self.selected_part_id = part_id
|
self.selected_part_id = part_id
|
||||||
self.selected_pick_position = pick_position
|
self.selected_pick_position = pick_position
|
||||||
@@ -1328,7 +1483,7 @@ class WindowCoreMixin:
|
|||||||
if self._edit_busy("编辑计算中,暂时不能选择Solid。"):
|
if self._edit_busy("编辑计算中,暂时不能选择Solid。"):
|
||||||
return
|
return
|
||||||
info = self.model.solid_info(solid_id)
|
info = self.model.solid_info(solid_id)
|
||||||
self._reset_selection(clear_highlight=False)
|
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||||
self.selected_kind = "solid"
|
self.selected_kind = "solid"
|
||||||
self.selected_part_id = part_id
|
self.selected_part_id = part_id
|
||||||
self.selected_solid_id = solid_id
|
self.selected_solid_id = solid_id
|
||||||
@@ -1351,7 +1506,7 @@ class WindowCoreMixin:
|
|||||||
return
|
return
|
||||||
if self._edit_busy("编辑计算中,暂时不能选择Face。"):
|
if self._edit_busy("编辑计算中,暂时不能选择Face。"):
|
||||||
return
|
return
|
||||||
self._reset_selection(clear_highlight=False)
|
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||||
self.selected_kind = "feature" if feature_mode else "face"
|
self.selected_kind = "feature" if feature_mode else "face"
|
||||||
self.selected_face_id = face_id
|
self.selected_face_id = face_id
|
||||||
self.selected_pick_position = pick_position
|
self.selected_pick_position = pick_position
|
||||||
@@ -1386,7 +1541,7 @@ class WindowCoreMixin:
|
|||||||
if self._edit_busy("编辑计算中,暂时不能选择特征。"):
|
if self._edit_busy("编辑计算中,暂时不能选择特征。"):
|
||||||
return
|
return
|
||||||
info = self.model.feature_info(face_id)
|
info = self.model.feature_info(face_id)
|
||||||
self._reset_selection(clear_highlight=False)
|
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||||
self.selected_kind = "feature"
|
self.selected_kind = "feature"
|
||||||
self.selected_face_id = face_id
|
self.selected_face_id = face_id
|
||||||
self.selected_pick_position = pick_position
|
self.selected_pick_position = pick_position
|
||||||
@@ -1417,7 +1572,21 @@ class WindowCoreMixin:
|
|||||||
self.hole_diameter_input.clear()
|
self.hole_diameter_input.clear()
|
||||||
if hasattr(self, "slot_width_input"):
|
if hasattr(self, "slot_width_input"):
|
||||||
self.slot_width_input.clear()
|
self.slot_width_input.clear()
|
||||||
|
if hasattr(self, "slot_depth_input"):
|
||||||
|
self.slot_depth_input.clear()
|
||||||
|
if hasattr(self, "slot_arc_length_input"):
|
||||||
|
self.slot_arc_length_input.clear()
|
||||||
|
if hasattr(self, "slot_total_length_input"):
|
||||||
|
self.slot_total_length_input.clear()
|
||||||
self.boss_diameter_input.setText(suggested_diameter)
|
self.boss_diameter_input.setText(suggested_diameter)
|
||||||
|
if hasattr(self, "boss_height_input"):
|
||||||
|
boss_height = _float_or_none(info.get("same_domain_height_estimate"))
|
||||||
|
if boss_height is None:
|
||||||
|
boss_height = _float_or_none(info.get("height_estimate"))
|
||||||
|
if boss_height is not None and boss_height > 0:
|
||||||
|
self.boss_height_input.setText(_format_float(boss_height * 1.2))
|
||||||
|
else:
|
||||||
|
self.boss_height_input.clear()
|
||||||
elif feature_guess == "round/fillet candidate":
|
elif feature_guess == "round/fillet candidate":
|
||||||
radius = _float_or_none(info.get("existing_fillet_radius_estimate"))
|
radius = _float_or_none(info.get("existing_fillet_radius_estimate"))
|
||||||
if radius is None:
|
if radius is None:
|
||||||
@@ -1427,22 +1596,54 @@ class WindowCoreMixin:
|
|||||||
self.hole_diameter_input.clear()
|
self.hole_diameter_input.clear()
|
||||||
if hasattr(self, "slot_width_input"):
|
if hasattr(self, "slot_width_input"):
|
||||||
self.slot_width_input.clear()
|
self.slot_width_input.clear()
|
||||||
|
if hasattr(self, "slot_depth_input"):
|
||||||
|
self.slot_depth_input.clear()
|
||||||
|
if hasattr(self, "slot_arc_length_input"):
|
||||||
|
self.slot_arc_length_input.clear()
|
||||||
|
if hasattr(self, "slot_total_length_input"):
|
||||||
|
self.slot_total_length_input.clear()
|
||||||
self.boss_diameter_input.clear()
|
self.boss_diameter_input.clear()
|
||||||
|
if hasattr(self, "boss_height_input"):
|
||||||
|
self.boss_height_input.clear()
|
||||||
else:
|
else:
|
||||||
suggested_diameter = _format_float(float(info["diameter"]) * 1.2)
|
suggested_diameter = _format_float(float(info["diameter"]) * 1.2)
|
||||||
self.hole_diameter_input.setText(suggested_diameter)
|
self.hole_diameter_input.setText(suggested_diameter)
|
||||||
self.boss_diameter_input.clear()
|
self.boss_diameter_input.clear()
|
||||||
|
if hasattr(self, "boss_height_input"):
|
||||||
|
self.boss_height_input.clear()
|
||||||
if hasattr(self, "slot_width_input"):
|
if hasattr(self, "slot_width_input"):
|
||||||
slot_width = _float_or_none(info.get("slot_chord_width_estimate"))
|
slot_width = _float_or_none(info.get("slot_chord_width_estimate"))
|
||||||
if slot_width is not None and slot_width > 0:
|
if slot_width is not None and slot_width > 0:
|
||||||
self.slot_width_input.setText(_format_float(slot_width * 1.2))
|
self.slot_width_input.setText(_format_float(slot_width * 1.2))
|
||||||
else:
|
else:
|
||||||
self.slot_width_input.clear()
|
self.slot_width_input.clear()
|
||||||
|
if hasattr(self, "slot_depth_input"):
|
||||||
|
slot_depth = _float_or_none(info.get("slot_sagitta_depth_estimate"))
|
||||||
|
if slot_depth is not None and slot_depth > 0:
|
||||||
|
self.slot_depth_input.setText(_format_float(slot_depth * 1.2))
|
||||||
|
else:
|
||||||
|
self.slot_depth_input.clear()
|
||||||
|
if hasattr(self, "slot_arc_length_input"):
|
||||||
|
slot_arc = _float_or_none(info.get("slot_arc_length_estimate"))
|
||||||
|
if slot_arc is not None and slot_arc > 0:
|
||||||
|
self.slot_arc_length_input.setText(_format_float(slot_arc * 1.2))
|
||||||
|
else:
|
||||||
|
self.slot_arc_length_input.clear()
|
||||||
|
if hasattr(self, "slot_total_length_input"):
|
||||||
|
self.slot_total_length_input.clear()
|
||||||
else:
|
else:
|
||||||
self.hole_diameter_input.clear()
|
self.hole_diameter_input.clear()
|
||||||
if hasattr(self, "slot_width_input"):
|
if hasattr(self, "slot_width_input"):
|
||||||
self.slot_width_input.clear()
|
self.slot_width_input.clear()
|
||||||
|
if hasattr(self, "slot_depth_input"):
|
||||||
|
self.slot_depth_input.clear()
|
||||||
|
if hasattr(self, "slot_arc_length_input"):
|
||||||
|
self.slot_arc_length_input.clear()
|
||||||
|
if hasattr(self, "slot_total_length_input"):
|
||||||
|
self.slot_total_length_input.clear()
|
||||||
self.boss_diameter_input.clear()
|
self.boss_diameter_input.clear()
|
||||||
|
if hasattr(self, "boss_height_input"):
|
||||||
|
self.boss_height_input.clear()
|
||||||
if info.get("cylinder_end_type") == "blind" and "hole_depth_estimate" in info:
|
if info.get("cylinder_end_type") == "blind" and "hole_depth_estimate" in info:
|
||||||
self.hole_depth_input.setText(_format_float(float(info["hole_depth_estimate"]) * 1.2))
|
self.hole_depth_input.setText(_format_float(float(info["hole_depth_estimate"]) * 1.2))
|
||||||
else:
|
else:
|
||||||
@@ -1469,7 +1670,15 @@ class WindowCoreMixin:
|
|||||||
self.shell_thickness_input.clear()
|
self.shell_thickness_input.clear()
|
||||||
if hasattr(self, "slot_width_input"):
|
if hasattr(self, "slot_width_input"):
|
||||||
self.slot_width_input.clear()
|
self.slot_width_input.clear()
|
||||||
|
if hasattr(self, "slot_depth_input"):
|
||||||
|
self.slot_depth_input.clear()
|
||||||
|
if hasattr(self, "slot_arc_length_input"):
|
||||||
|
self.slot_arc_length_input.clear()
|
||||||
|
if hasattr(self, "slot_total_length_input"):
|
||||||
|
self.slot_total_length_input.clear()
|
||||||
self.boss_diameter_input.clear()
|
self.boss_diameter_input.clear()
|
||||||
|
if hasattr(self, "boss_height_input"):
|
||||||
|
self.boss_height_input.clear()
|
||||||
self.hole_depth_input.clear()
|
self.hole_depth_input.clear()
|
||||||
if hasattr(self, "hole_bottom_face_input"):
|
if hasattr(self, "hole_bottom_face_input"):
|
||||||
self.hole_bottom_face_input.clear()
|
self.hole_bottom_face_input.clear()
|
||||||
@@ -1491,7 +1700,7 @@ class WindowCoreMixin:
|
|||||||
return
|
return
|
||||||
if self._edit_busy("编辑计算中,暂时不能选择Edge。"):
|
if self._edit_busy("编辑计算中,暂时不能选择Edge。"):
|
||||||
return
|
return
|
||||||
self._reset_selection(clear_highlight=False)
|
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||||
self.selected_kind = "edge"
|
self.selected_kind = "edge"
|
||||||
self.selected_edge_id = edge_id
|
self.selected_edge_id = edge_id
|
||||||
self.selected_pick_position = pick_position
|
self.selected_pick_position = pick_position
|
||||||
@@ -1583,8 +1792,7 @@ 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":
|
||||||
info = self.model.feature_info(target_id)
|
face_ids = self.model.connected_same_domain_face_ids(target_id) or [target_id]
|
||||||
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [target_id]
|
|
||||||
self._highlight_hover_faces(face_ids=face_ids)
|
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)
|
||||||
@@ -1699,6 +1907,7 @@ class WindowCoreMixin:
|
|||||||
polydata,
|
polydata,
|
||||||
color: tuple[float, float, float],
|
color: tuple[float, float, float],
|
||||||
opacity: float | None = None,
|
opacity: float | None = None,
|
||||||
|
position: tuple[float, float, float] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
mapper = vtk.vtkPolyDataMapper()
|
mapper = vtk.vtkPolyDataMapper()
|
||||||
mapper.SetInputData(polydata)
|
mapper.SetInputData(polydata)
|
||||||
@@ -1708,16 +1917,24 @@ class WindowCoreMixin:
|
|||||||
actor.GetProperty().SetOpacity(opacity if opacity is not None else self.edit_preview_base_opacity)
|
actor.GetProperty().SetOpacity(opacity if opacity is not None else self.edit_preview_base_opacity)
|
||||||
actor.GetProperty().SetSpecular(0.28)
|
actor.GetProperty().SetSpecular(0.28)
|
||||||
actor.GetProperty().SetLineWidth(1)
|
actor.GetProperty().SetLineWidth(1)
|
||||||
|
if position is not None:
|
||||||
|
actor.SetPosition(float(position[0]), float(position[1]), float(position[2]))
|
||||||
self.edit_preview_actors.append(actor)
|
self.edit_preview_actors.append(actor)
|
||||||
self.edit_preview_actor = self.edit_preview_actor or actor
|
self.edit_preview_actor = self.edit_preview_actor or actor
|
||||||
self.renderer.AddActor(actor)
|
self.renderer.AddActor(actor)
|
||||||
|
|
||||||
def _show_push_pull_preview(self, face_id: int, distance: float) -> None:
|
def _show_push_pull_preview(self, face_id: int, distance: float, plan: dict[str, object] | None = None) -> None:
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
return
|
return
|
||||||
self.clear_edit_preview(render=False)
|
self.clear_edit_preview(render=False)
|
||||||
try:
|
try:
|
||||||
polydata = self.model.push_pull_preview_polydata(face_id, distance)
|
plan = dict(plan or self.model.push_pull_plan(face_id, distance))
|
||||||
|
scope_face_ids = _int_values(plan.get("push_pull_scope_face_ids")) or [face_id]
|
||||||
|
polydata = self._cached_face_overlay_polydata(face_ids=scope_face_ids, smooth=False)
|
||||||
|
if polydata is None:
|
||||||
|
polydata = self._cached_face_overlay_polydata(face_ids=[face_id], smooth=False)
|
||||||
|
if polydata is None:
|
||||||
|
raise RuntimeError("当前显示网格里没有可复用的选中 Face 预览数据")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.statusBar().showMessage(f"推拉预览不可用:{exc}")
|
self.statusBar().showMessage(f"推拉预览不可用:{exc}")
|
||||||
return
|
return
|
||||||
@@ -1725,7 +1942,15 @@ class WindowCoreMixin:
|
|||||||
color = (0.0, 0.86, 0.34)
|
color = (0.0, 0.86, 0.34)
|
||||||
else:
|
else:
|
||||||
color = (1.0, 0.18, 0.06)
|
color = (1.0, 0.18, 0.06)
|
||||||
self._add_edit_preview_actor(polydata, color)
|
outward = plan.get("outward_direction") if isinstance(plan, dict) else None
|
||||||
|
if not isinstance(outward, (tuple, list)) or len(outward) != 3:
|
||||||
|
outward = (0.0, 0.0, 0.0)
|
||||||
|
position = (
|
||||||
|
float(outward[0]) * float(distance),
|
||||||
|
float(outward[1]) * float(distance),
|
||||||
|
float(outward[2]) * float(distance),
|
||||||
|
)
|
||||||
|
self._add_edit_preview_actor(polydata, color, opacity=0.38, position=position)
|
||||||
self._start_edit_preview_pulse()
|
self._start_edit_preview_pulse()
|
||||||
self.render_window.Render()
|
self.render_window.Render()
|
||||||
|
|
||||||
@@ -1776,6 +2001,19 @@ class WindowCoreMixin:
|
|||||||
self._start_edit_preview_pulse()
|
self._start_edit_preview_pulse()
|
||||||
self.render_window.Render()
|
self.render_window.Render()
|
||||||
|
|
||||||
|
def _show_cylinder_boss_height_preview(self, face_id: int, target_height: float) -> None:
|
||||||
|
if self.model is None:
|
||||||
|
return
|
||||||
|
self.clear_edit_preview(render=False)
|
||||||
|
try:
|
||||||
|
polydata = self.model.cylindrical_boss_height_preview_polydata(face_id, target_height)
|
||||||
|
except Exception as exc:
|
||||||
|
self.statusBar().showMessage(f"凸台高度调整预览不可用:{exc}")
|
||||||
|
return
|
||||||
|
self._add_edit_preview_actor(polydata, (0.0, 0.72, 0.78), opacity=0.34)
|
||||||
|
self._start_edit_preview_pulse()
|
||||||
|
self.render_window.Render()
|
||||||
|
|
||||||
def _show_cylinder_suppress_preview(self, face_id: int) -> None:
|
def _show_cylinder_suppress_preview(self, face_id: int) -> None:
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
return
|
return
|
||||||
|
|||||||
+1851
-85
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user