feat: 完善 Face 一级编辑与隔离计算

This commit is contained in:
2026-08-05 15:08:16 +08:00
parent a76282d7dd
commit ed1faf51d8
28 changed files with 3740 additions and 876 deletions
+184 -166
View File
@@ -74,11 +74,13 @@ python scripts\generate_cube_step.py
python scripts\verify_face_edit_suite.py
python scripts\verify_isolated_face_edit.py
python scripts\verify_edge_edit_suite.py
python scripts\verify_edge_isolated_edit.py
python scripts\verify_shell_thickness_resize.py
python scripts\verify_analytic_surface_resize.py
python scripts\verify_boss_resize.py
python scripts\verify_hole_resize.py
python scripts\verify_slot_resize.py
python scripts\verify_hole_slot_isolated_edit.py
python scripts\verify_cylindrical_first_level_topology.py
```
@@ -124,8 +126,8 @@ git diff --check
- 用户可以选择零件、Solid、Face、Edge 和几何特征。
- 用户可以做基础测量,例如把两个拾取点或对象中心设为 A/B 并计算距离。
- 程序可以识别孔、圆角、凸台、槽、壳体局部区域等候选特征。
- 识别结果会区分 `当前可改``当前受限修改``识别限制``受限能力`:比如一个通孔可以改孔径,但盲孔深度属于受限能力;一个带内孔的平面可以推拉,但“只改当前面局部尺寸/中心/偏移”会被标成受限修改。
- 用户可以修改可控的特征,比如 Face 的面积、面宽/面高、中心、偏移和薄壁厚度,并通过 `影响范围` 选择推拉当前面、只改当前面、移动整个特征或调整整个特征;还可以做孔径/半径调整、孔/槽轴心并通过 `影响范围` 选择移动局部特征或移动整个特征、槽宽/槽深/弧长并通过 `影响范围` 选择局部重建或整体缩放、弧角、开口角和槽孔总长度调整、盲孔/盲槽深度(局部切补或整体缩放)、薄壁厚度(局部拉或整体缩放)、凸台直径/半径/高度/轴心并通过 `影响范围` 选择局部修改或整体调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度并通过 `影响范围` 选择重建圆角或整体调整、圆锥参考半径/直径/半角(简单圆锥解析重建,嵌入式锥孔优先局部重切)、球面半径/直径(整体)、环面主/小半径或直径(整体)、Edge倒圆、Edge倒角、圆Edge半径/直径并通过 `影响范围` 选择自动/相邻圆柱/缩放所属、修改Edge长度并通过 `影响范围` 选择自动/只改当前Edge/移动端面/相邻圆柱/缩放所属,以及直线Edge起点/终点坐标。
- 识别结果会区分 `当前可改``当前受限修改``识别限制``受限能力`:比如一个通孔可以改孔径,但盲孔深度属于受限能力;一个带内孔的平面可以拉伸/切除,但“局部重建尺寸/中心/偏移”会被标成受限修改。
- 用户可以修改可控的特征,比如 Face 的面积、U向尺寸/V向尺寸、中心、偏移曲面和壳体厚度,并通过 `影响范围` 选择拉伸/切除、局部重建、移动特征或缩放特征;还可以做孔径/半径调整、孔/槽轴心并通过 `影响范围` 选择移动局部特征或移动特征、槽宽/槽深/弧长并通过 `影响范围` 选择局部重建或整体缩放、弧角、开口角和槽孔总长度调整、盲孔/盲槽深度(局部切补或整体缩放)、壳体厚度(局部拉伸/切除或整体缩放)、凸台直径/半径/高度/轴心并通过 `影响范围` 选择局部修改或整体调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度并通过 `影响范围` 选择重建圆角或整体调整、圆锥参考半径/直径/半角(简单圆锥解析重建,嵌入式锥孔优先局部重切)、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge倒圆、Edge倒角、圆Edge半径/直径并通过 `影响范围` 选择自动/相邻圆柱/缩放所属、修改Edge长度并通过 `影响范围` 选择自动/只改当前Edge/移动端面/相邻圆柱/缩放所属,以及直线Edge起点/终点坐标。
- 用户可以撤销/重做修改,避免一步编辑做坏。
- 用户可以导出修改后的完整模型。
- 如果 STEP 文件里存在多个互不关联的零件,用户可以只导出选中的某个零件。
@@ -134,35 +136,39 @@ git diff --check
## 几何修改语义原则
所有可修改参数都要先明确“这次修改到底代表什么”,不能只因为程序能算出一个结果就直接放行。这个原则适用于 Face、Edge、孔、槽、凸台、圆角、Solid 和整件特征,不是 Edge 专属。比如修改 Face 可能代表面推拉、局部补料、局部切削、平移所属对象或整体缩放;修改孔/槽可能代表重切侧壁、移动轴心、调整端面深度或重新配对长圆槽端部;修改圆角可能代表移除旧圆角后重建;修改 Edge 可能代表只动这条边、移动相关端面,或者高风险地缩放所属几何。
所有可修改参数都要先明确“这次修改到底代表什么”,不能只因为程序能算出一个结果就直接放行。这个原则适用于 Face、Edge、孔、槽、凸台、圆角、Solid 和整件特征,不是 Edge 专属。比如修改 Face 可能代表 Face 拉伸/切除、局部补料、局部切削、平移所属对象或整体缩放;修改孔/槽可能代表重切侧壁、移动轴心、调整端面深度或重新配对长圆槽端部;修改圆角可能代表移除旧圆角后重建;修改 Edge 可能代表只动这条边、移动相关端面,或者高风险地缩放所属几何。
所以每个可编辑行都应尽量说明本次采用的策略、影响范围和风险;当同一个目标值有多种合理语义时,应逐步做成用户可选择的策略,而不是程序悄悄替用户决定。高风险 fallback 必须提示用户确认,不能把整体尺寸变化、局部形变、面推拉、切削补料、对象平移、对象缩放和特征重建混成一个简单的“修改成功”。
所以每个可编辑行都应尽量说明本次采用的策略、影响范围和风险;当同一个目标值有多种合理语义时,应逐步做成用户可选择的策略,而不是程序悄悄替用户决定。高风险 fallback 必须提示用户确认,不能把整体尺寸变化、局部形变、Face 拉伸/切除、切削补料、对象平移、对象缩放和特征重建混成一个简单的“修改成功”。
当前 Face 参数化修改已经接入隔离子进程执行,包括 `推拉当前面``偏移(当前面)``面积(当前面)``面宽/面高(当前面)``中心(当前面)``薄壁厚度(当前面)``面积(整体)``面宽/面高(整体)``薄壁厚度(整体)`、普通圆柱高度、圆柱凸台高度,以及圆锥参考半径/直径/半角、球面半径/直径、环面主/小半径或直径:主程序先导出临时 STEP,让 helper 进程执行危险 OCCT 计算并导出结果 STEP,再由主程序加载成功结果;如果 helper 卡死、超时或崩溃,主界面进程和原模型保持不变。关闭窗口时如果正在运行隔离子进程,主程序会请求终止该子进程,避免用户被长时间计算锁住。打包后的 `main.exe` 也会通过 `--isolated-edit-worker` 进入同一条 worker 通道,避免子进程误打开第二个 GUI。无法识别为简单圆锥或锥孔/沉孔的复杂圆锥/拔模面参考半径、参考直径和半角暂不开放,不再退回整体径向缩放;复杂浅锥/拔模面会在计划阶段快速阻止,避免界面线程长时间等待或生成无效 B-Rep。其它还没接入隔离通道的 high-risk 圆角、倒角或复杂 fallback 会被稳定性保护阻止,后续按特征逐步开放。
当前 Face 参数化修改已经接入隔离子进程执行,包括 `拉伸/切除``偏移曲面(局部重建)``面积(局部重建)``U向尺寸/V向尺寸(局部重建)``中心(局部重建)``壳体厚度(拉伸/切除)``面积(缩放特征)``U向尺寸/V向尺寸(缩放特征)``壳体厚度(缩放特征)`、普通圆柱高度、圆柱凸台高度,以及圆锥参考半径/直径/半角、球面半径/直径、环面主/小半径或直径:主程序先导出临时 STEP,让 helper 进程执行危险 OCCT 计算并导出结果 STEP,再由主程序加载成功结果;如果 helper 卡死、超时或崩溃,主界面进程和原模型保持不变。关闭窗口时如果正在运行隔离子进程,主程序会请求终止该子进程,避免用户被长时间计算锁住。打包后的 `main.exe` 也会通过 `--isolated-edit-worker` 进入同一条 worker 通道,避免子进程误打开第二个 GUI。无法识别为简单圆锥或锥孔/沉孔的复杂圆锥/拔模面参考半径、参考直径和半角暂不开放,不再退回整体径向缩放;复杂浅锥/拔模面会在计划阶段快速阻止,避免界面线程长时间等待或生成无效 B-Rep。仍未归类到明确 Face、孔槽或 Edge 语义的复杂 fallback 会继续被稳定性保护阻止,后续按特征逐步开放。
孔/槽一级关系的主要布尔/重建入口也开始复用同一条隔离执行通道,包括孔径、圆柱尺寸缩放、孔轴心、孔封堵、盲孔/盲槽深度、槽/半孔轴心、槽宽、槽深、圆弧长度、圆弧角度、长圆槽总长度和中心距。这样即使孔/槽局部重切在复杂 STEP 上失败、超时或触发 OCCT 崩溃,主界面和原模型也应保持不变。圆柱直径整体缩放和盲孔/盲槽深度整体缩放会在模型层做结果校验;如果结果虽然是有效 B-Rep,但已经丢失目标圆柱/盲孔语义,会自动回滚并提示原因。
Edge 一级关系的主要高风险入口也接入同一条隔离执行通道,包括修改 Edge 长度、直线 Edge 起点/中心/终点移动、圆形 Edge 相邻轴心移动、椭圆 Edge 主/小半径、已有圆角半径、直线 Edge 新增圆角、对称倒角、不等距倒角和距离+角度倒角。Edge 操作仍然不是通用 CAD 历史特征编辑;复杂曲线、复杂圆角链、无法确认一级邻域或 OCCT 倒圆/倒角失败时会回滚或提示失败,但这些危险计算不应再拖垮主界面进程。
Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示底层 OCCT 异常。当前会尽量附带诊断信息:操作对象、Face 类型、目标值/目标位置、一级边界 Edge/Vertex、共享边相邻 Face、内孔/内边界数量、已识别的端盖专用路径,以及是否可能因为孔壁、槽底、台阶终点等二级/三级关系暂未传播而失败。
复杂大 STEP 里的多边界平面端盖不再在 UI 线程同步生成完整 `push_pull_plan`。界面只做轻量预检查并标记高风险,然后把完整一级关系识别、解析重建或快速阻止放到后台隔离进程里执行,避免用户点击 `参数化建模` 后先卡住几秒到几十秒。
如果轻量预检查已经能判断出风险不可接受,比如复杂大模型里的多内孔端盖向内推拉,界面会直接阻止并说明需要二级关系判断,不再提交隔离子进程等待失败返回。
如果轻量预检查已经能判断出风险不可接受,比如复杂大模型里的多内孔端盖向内切除已经接近切穿,或需要判断孔底、槽底、台阶背后的二级关系,界面会直接阻止并说明原因,不再提交隔离子进程等待失败返回。浅范围内的多内孔端盖向内收缩会走后台/隔离子进程里的一级边界侧壁重建。
复杂大 STEP 中带内孔/内边界的平面 Face,如果用户选择 `面积(当前面)``面宽/面高(当前面)``中心(当前面)``偏移(当前面)` 这类“只改当前 Face 顶点并重建相邻面”的局部变形语义,UI 会在轻量预检查阶段直接阻止并说明原因,不再先进入完整局部重建计划。`薄壁厚度(当前面)` 在这类复杂面上会先标记为后台高风险计划,把相对面识别和拉距离计算放进隔离进程,避免主界面等待复杂拓扑扫描。
复杂大 STEP 中带内孔/内边界的平面 Face,如果用户选择 `面积(局部重建)``U向尺寸/V向尺寸(局部重建)``中心(局部重建)``偏移曲面(局部重建)` 这类“只改当前 Face 顶点并重建相邻面”的局部变形语义,UI 会在轻量预检查阶段直接阻止并说明原因,不再先进入完整局部重建计划。`壳体厚度(拉伸/切除)` 在这类复杂面上会先标记为后台高风险计划,把相对面识别和拉伸/切除距离计算放进隔离进程,避免主界面等待复杂拓扑扫描。
普通平面 Face 的核心修改路径现在还会在模型层做结果守门:执行后必须通过 B-Rep 校验、能重新找到目标 Face 的目标值,并且能重新识别该 Face 的一级共享边邻域。矩形、多边形 Face 会检查边界 Edge 和 Vertex;圆形端盖这类解析边界会允许 1 条闭合圆 Edge。圆锥参考半径/半角、球面半径、环面半径这类解析 Face 也会强制校验目标值和 B-Rep。若目标值没达到、一级邻域丢失或 B-Rep 无效,模型层会恢复操作前 shape,不把坏结果交给界面或导出流程。
`推拉当前面` 对一次性较大的向外拉不会按绝对距离一刀切阻止;如果距离相对当前 Face 或所属实体跨度较大,会标为高风险并走隔离子进程执行。这样可以允许用户做类似 CAD 里的大距离拉伸,同时如果 OCCT 布尔失败、超时或返回无效 B-Rep,主程序和原模型仍保持不变。
`拉伸/切除` 对一次性较大的向外拉不会按绝对距离一刀切阻止;如果距离相对当前 Face 或所属实体跨度较大,会标为高风险并走隔离子进程执行。这样可以允许用户做类似 CAD 里的大距离拉伸,同时如果 OCCT 布尔失败、超时或返回无效 B-Rep,主程序和原模型仍保持不变。
实心圆柱或同轴空心筒体的端盖拉会优先走解析/局部重建:选中圆柱上/下底面或环形端面时,程序会识别相邻外圆柱面、内孔圆柱面和端盖一级关系。纯简单圆柱/筒体会直接重建新的完整高度;如果同一 Solid 上还有不碰端盖边界的侧向缺口、台阶等额外特征,向外拉会只生成旧端盖到新端盖之间的局部延长段去补料,向内推拉会只切除旧端盖到新端盖之间的局部段,避免拿整根新圆柱/新筒体或通用平面 prism 和原模型做大范围布尔。同轴阶梯/盲孔式环形端盖允许内外圆柱侧面的旧深度不同;向外拉时会保留旧侧面的复杂底部裁剪,只删除旧端盖,并补上旧端盖到新端盖之间的侧面段和新端盖,结果校验会优先检查新增端盖而不是全模型扫描。如果端盖边界本身连接了槽、缺口或台阶等额外一级相邻 Face,向外拉会改用当前端盖真实轮廓拉伸,让这些开口边界跟随延长;向内推拉如果没有越过这些开口的内侧终点,会改用当前端盖真实轮廓切削,让开口边界跟随缩短;如果越过槽底/台阶内侧终点,会快速阻止并说明原因。无法确认同轴圆柱/筒体一级关系时,才退回隔离子进程里的通用拉和回滚保护。
实心圆柱或同轴空心筒体的端盖拉伸/切除会优先走解析/局部重建:选中圆柱上/下底面或环形端面时,程序会识别相邻外圆柱面、内孔圆柱面和端盖一级关系。纯简单圆柱/筒体会直接重建新的完整高度;如果同一 Solid 上还有不碰端盖边界的侧向缺口、台阶等额外特征,向外拉会只生成旧端盖到新端盖之间的局部延长段去补料,向内切除会只切除旧端盖到新端盖之间的局部段,避免拿整根新圆柱/新筒体或通用平面 prism 和原模型做大范围布尔。同轴阶梯/盲孔式环形端盖允许内外圆柱侧面的旧深度不同;向外拉时会保留旧侧面的复杂底部裁剪,只删除旧端盖,并补上旧端盖到新端盖之间的侧面段和新端盖,结果校验会优先检查新增端盖而不是全模型扫描。如果端盖边界本身连接了槽、缺口或台阶等额外一级相邻 Face,向外拉会改用当前端盖真实轮廓拉伸,让这些开口边界跟随延长;向内切除如果没有越过这些开口的内侧终点,会改用当前端盖真实轮廓切削,让开口边界跟随缩短;如果越过槽底/台阶内侧终点,会快速阻止并说明原因。无法确认同轴圆柱/筒体一级关系时,才退回隔离子进程里的通用拉伸/切除和回滚保护。
带多个内孔/内边界的大平面端盖向外推拉时,如果不能归类成同轴圆柱/筒体端盖,会改走 `boundary-shell-rebuild`:移动当前端面,并把该端面的一级边界 Edge 沿推拉方向生成延长侧壁后局部缝合,避免复杂 STEP 退回 180s 通用布尔慢路径;向内推拉如果没有识别到安全的端盖缩短路径,或需要判断槽底、台阶终点、孔壁背后的二级/三级关系,会快速阻止并把原因写清楚。
带多个内孔/内边界的大平面端盖如果不能归类成同轴圆柱/筒体端盖,会优先改走 `boundary-shell-rebuild`:移动当前端面,并把该端面的一级边界 Edge 沿拉伸/切除方向重建侧壁后局部缝合,避免复杂 STEP 退回 180s 通用布尔慢路径。向外拉伸会生成延长侧壁;浅范围向内收缩会重建缩短后的侧壁并保留内孔线圈;如果向内距离接近切穿,或需要判断槽底、台阶终点、孔壁背后的二级/三级关系,会快速阻止并把原因写清楚。
选中普通完整圆柱侧面修改 `高度` 时,局部语义会换算成端盖拉:执行后必须重新检测到同轴、同半径、目标高度的圆柱侧面,并且圆柱一级边界/相邻 Face 关系没有丢失,否则自动回滚。`调整整个特征` 语义会沿圆柱轴向缩放所属特征或 Solid,执行后按轴向跨度校验目标高度;这条语义可能影响同一对象上的孔距、台阶位置和其它轴向尺寸。
选中普通完整圆柱侧面修改 `高度` 时,局部语义会换算成端盖拉伸/切除:执行后必须重新检测到同轴、同半径、目标高度的圆柱侧面,并且圆柱一级边界/相邻 Face 关系没有丢失,否则自动回滚。`缩放特征` 语义会沿圆柱轴向缩放所属特征或 Solid,执行后按轴向跨度校验目标高度;这条语义可能影响同一对象上的孔距、台阶位置和其它轴向尺寸。
产品目标里必须明确保留“用户选择建模意图”的能力。以正方体改一条 Edge 为例,用户应该能区分并选择:只拉长这一条 Edge 让相邻面自然变斜、移动相关端面让正方体变成长方体、保持某些面垂直、固定某些边/点不动,或者让某些相邻边跟随变化。这个原则不是 Edge 专属,Face、孔、槽、凸台、圆角、Solid 和整件特征也一样:当一个目标值存在多种合理 CAD 语义时,界面应让用户选择“改法、约束、影响对象和哪些几何跟随变化”,不能无脑套用一个默认结果。当前 Edge 的 `移动端面/保持垂直` 就是这类语义:它不是只拉歪一条边,而是让端面和相关边跟随移动,使相邻平面尽量保持垂直。
设计上优先把选择做成当前选中对象里的清晰属性行或策略控件,而不是弹出一堆工程术语。每个可选策略都要用用户能看懂的话说明结果,例如“只改当前边,邻面会变斜”“移动端面,整体变成长方体”“缩放所属对象,所有尺寸同比例变化”“重切孔壁,只改孔径”。默认策略可以存在,但必须可见、可解释、可切换。
界面里的 `当前选中对象` 会优先显示 `编辑方式` 行,用来告诉你当前对象的修改会走哪类几何语义。比如孔径是同轴重切,槽/半孔是局部扇形槽或配对长圆槽重建,凸台是包络重建或端盖拉,已有圆角是先移除旧圆角再重建。对于圆柱直径/半径,当前也开始提供独立的 `缩放所属对象` 行,让你明确选择“重切/重建局部特征”还是“按目标尺寸比例均匀缩放所属特征或 Solid”。
界面里的 `当前选中对象` 会优先显示 `编辑方式` 行,用来告诉你当前对象的修改会走哪类几何语义。比如孔径是同轴重切,槽/半孔是局部扇形槽或配对长圆槽重建,凸台是包络重建或端盖拉伸/切除,已有圆角是先移除旧圆角再重建。对于圆柱直径/半径,当前也开始提供独立的 `缩放所属对象` 行,让你明确选择“重切/重建局部特征”还是“按目标尺寸比例均匀缩放所属特征或 Solid”。
## 开源复用原则
@@ -193,10 +199,10 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
| 对象 | 现在能改哪些 | 程序大概怎么改 | 暂时还不能稳定改 |
| --- | --- | --- | --- |
| Edge / 边 | 直线边的长度、起点、中心、终点;直线边新增圆角/倒角;圆形或圆弧边的半径/直径、圆心/相邻轴心;椭圆边的主半径/小半径。 | 简单模型优先局部重建当前边和相邻面;圆边可复用相邻圆柱改孔/槽/凸台直径或轴心;椭圆边可沿主轴或小轴单轴缩放所属特征。 | 任意复杂曲线边、B-spline 边、需要复杂约束跟随的边。 |
| Face / 面 | 面积、面宽/面高、中心、偏移、薄壁厚度;圆柱端盖或完整圆柱侧面的高度。 | 拉当前平面或圆柱端盖;移动当前面顶点并重建相邻面;或平移/缩放所属特征。 | 自由曲面带复杂内孔的面、复杂壳体局部区域、非完整圆柱侧面高度。 |
| Face / 面 | 面积、U向尺寸/V向尺寸、中心、偏移曲面、壳体厚度;圆柱端盖或完整圆柱侧面的高度。 | 拉伸/切除当前平面或圆柱端盖;移动当前面顶点并重建相邻面;或平移/缩放所属特征。 | 自由曲面会明确识别为只读,不开放伪参数化修改;带复杂内孔的面、复杂壳体局部区域、非完整圆柱侧面高度。 |
| 孔 / 圆柱孔 | 规则孔的直径/半径、完整孔轴心、完整孔封堵、可识别盲孔/盲槽深度。 | 改孔径时同轴重切孔壁;移动孔时先补旧孔再切新孔;改深度时切削或补料孔底。 | 螺纹孔、锥孔、复杂孔组、底面识别不可靠的盲孔。 |
| 槽 / 半孔 | 槽宽、槽深、弧长、弧角、开口角、简单轴心、长圆槽总长度/中心距。 | 按圆柱槽或长圆槽胶囊区域,先补旧槽,再切出新槽。 | 复杂草图槽、非圆柱槽、多槽联动、跨复杂面的槽。 |
| 凸台 / 外圆 | 圆柱凸台的直径/半径、高度、轴心。 | 移除旧凸台包络后重建;或拉端盖、轴向缩放。 | 异形凸台、多台阶凸台、复杂相邻拓扑。 |
| 凸台 / 外圆 | 圆柱凸台的直径/半径、高度、轴心。 | 移除旧凸台包络后重建;或拉伸/切除端盖、轴向缩放。 | 异形凸台、多台阶凸台、复杂相邻拓扑。 |
| 圆角 / 倒角 | 部分已有圆角的半径/弧长;直线边新增圆角或倒角。 | 已有圆角先移除再重建;新增圆角/倒角调用 OCCT 的圆角/倒角算法。 | 复杂 blend、圆角链、支撑面不明确的圆角。 |
| Solid / 整体特征 | 平移、旋转、包围盒尺寸、体积、表面积整体缩放。 | 对所属 Solid / 特征做整体变换。 | 装配约束、零件间关联约束、原 CAD 建模历史恢复。 |
@@ -206,14 +212,14 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
| 对象 / 特征 | 现在可以修改 | 暂不能保证或还不能修改 |
| --- | --- | --- |
| Face | 平面 Face 可以改 `面积``面宽``面高``中心``偏移`;这些行会在 `影响范围` 里选择 `只改当前面``推拉当前面``移动整个特征``调整整个特征`。识别到相对平面时还可以改 `薄壁厚度`,并通过 `影响范围` 选择 `推拉当前面``调整整个特征`。圆柱上/下端盖、同轴空心圆柱的环形端面可以通过 `推拉当前面` 改高度;选中完整圆柱侧面时,也可以把 `高度` 换算成端盖拉,或者选择沿轴向 `调整整个特征`。简单端盖会解析重建,复杂同轴筒体会优先只处理旧端盖到新端盖之间的局部段,端盖边界上有槽口/缺口时会按真实端盖轮廓向外拉伸或在安全范围内向内切削。 | `面宽/面高` 是这个 Face 自身平面里的两个方向尺寸,不是面积,也不是模型高度;`偏移` 是沿当前面垂直方向测到的位置,不再单独显示容易混淆的额外的“本次移动量”提示。`推拉当前面` 会加料或切削;`只改当前面` 会移动当前 Face 顶点并重建相邻平面,相邻面可能变斜;`移动整个特征` 会平移所属特征或 Solid`调整整个特征` 会缩放所属特征或 Solid 并影响其它尺寸;自由曲面复杂壳体局部区域、非完整圆柱侧面、非同轴多孔环形端面、端盖边界本身连着槽/缺口/台阶且向内推拉越过槽底/台阶终点的面和其它带复杂边界的面仍不能稳定做局部尺寸编辑,界面会禁用或快速阻止并在提示里说明具体原因,建议改用孔/槽专门入口、`推拉当前面` 的简单端盖路径或整体调整。 |
| 圆柱孔 / 圆柱面 | 孔或圆柱的 `直径``半径`,并通过 `影响范围` 选择 `只改孔/槽壁``调整整个特征`;完整圆柱孔可以改轴心坐标;完整孔可以尝试封堵。普通孔径语义是同轴圆柱重切,孔轴心语义是先填旧孔再切新孔。 | 复杂孔、螺纹孔、锥孔、不完整圆柱孔和拓扑不稳定的孔可能失败并回滚;`调整整个特征` 会影响同一对象上的高度、厚度和其它尺寸,不适合只想改孔壁的场景。 |
| 盲孔 / 盲槽 | 识别到疑似底面 Face 时,可以改 `盲孔/盲槽深度`,并通过 `影响范围` 选择 `改底面深度``调整整个特征``改底面深度` 加深会沿开口到底面方向切削,变浅会从新底面到旧底面补料;`调整整个特征` 会沿孔/槽轴向缩放所属特征或 Solid。 | STEP 不保存真实孔深历史;找不到可靠底面或底面被复杂拓扑切碎时,深度只能只读或需要手动辅助;`调整整个特征` 会影响同一对象上的壁厚、孔距和其它轴向尺寸,不适合只想改孔底位置的场景。 |
| 槽 / 半孔 | 可以改槽宽、槽深、弧长、弧角、开口角和简单局部轴心;槽宽、槽深和弧长会在 `影响范围` 里选择 `只改槽壁``调整整个特征`。长圆槽的轴心、总长度和中心距会在点击修改时自动尝试配对另一个半圆端,也可以手动填写配对端 Face ID。 | 轴心仍是受限的局部扇形槽/胶囊槽重建;复杂草图槽、非圆柱槽、多槽关联迁移、链式槽和跨多个不规则面的槽还不是稳定能力。`只改槽壁` 会局部重建槽,`调整整个特征` 会缩放所属对象并影响其它尺寸;如果结果真的把一个 Solid 拆成多个 Solid,会自动回滚。 |
| 圆柱凸台 / 外圆 | 完整圆柱凸台可以改直径、半径、高度和轴心;普通完整圆柱也有高度兜底修改。凸台的 `直径``半径``高度``轴心` 会在 `影响范围` 里选择 `只改凸台``拉端盖``移动凸台``调整整个特征``移动整个特征`。 | 复杂凸台、异形凸台、多台阶凸台和不完整外圆不能保证稳定重建;`调整整个特征` 会连带改变同一对象上的高度、厚度和其它尺寸,不能当作单独重建凸台包络或只移动端盖使用。 |
| 已有圆角 / 倒圆面 | 部分规则圆柱倒圆面可以改 `圆角半径``圆角弧长`,并通过 `影响范围` 选择 `重建圆角``调整整个特征``重建圆角` 会先移除旧圆角再重建新圆角;`调整整个特征` 会把目标值换算为圆柱直径比例,再整体缩放所属特征或 Solid。 | 复杂 blend、支撑面不明确、圆角链或恢复锐边失败时会阻止或回滚;`调整整个特征` 会影响同一对象上的其它尺寸,不适合只想改圆角本身的场景。 |
| Face | 平面 Face 可以改 `面积``U向尺寸``V向尺寸``中心``偏移曲面`;这些行会在 `影响范围` 里选择 `局部重建``拉伸/切除``移动特征``缩放特征`。识别到相对平面时还可以改 `壳体厚度`,并通过 `影响范围` 选择 `拉伸/切除``缩放特征`。圆柱上/下端盖、同轴空心圆柱的环形端面可以通过 `拉伸/切除` 改高度;选中完整圆柱侧面时,也可以把 `高度` 换算成端盖拉伸/切除,或者选择沿轴向 `缩放特征`。简单端盖会解析重建,复杂同轴筒体会优先只处理旧端盖到新端盖之间的局部段,端盖边界上有槽口/缺口时会按真实端盖轮廓向外拉伸或在安全范围内向内切削。 | `U向尺寸/V向尺寸` 是这个 Face 自身平面里的两个方向尺寸,不是面积,也不是模型高度;`偏移曲面` 是沿当前面垂直方向测到的位置,不再单独显示容易混淆的额外的“本次移动量”提示。`拉伸/切除` 会加料或切削;`局部重建` 会移动当前 Face 顶点并重建相邻平面,相邻面可能变斜;`移动特征` 会平移所属特征或 Solid`缩放特征` 会缩放所属特征或 Solid 并影响其它尺寸;自由曲面会显示为只读受限对象,不把面积、中心、包围盒或底层 UV 参数伪装成可修改尺寸;复杂壳体局部区域、非完整圆柱侧面、非同轴多孔环形端面、端盖边界本身连着槽/缺口/台阶且向内切除越过槽底/台阶终点的面和其它带复杂边界的面仍不能稳定做局部尺寸编辑,界面会禁用或快速阻止并在提示里说明具体原因,建议改用孔/槽专门入口、`拉伸/切除` 的简单端盖路径或整体调整。 |
| 圆柱孔 / 圆柱面 | 孔或圆柱的 `直径``半径`,并通过 `影响范围` 选择 `只改孔/槽壁``缩放特征`;完整圆柱孔可以改轴心坐标;完整孔可以尝试封堵。普通孔径语义是同轴圆柱重切,孔轴心语义是先填旧孔再切新孔。 | 复杂孔、螺纹孔、锥孔、不完整圆柱孔和拓扑不稳定的孔可能失败并回滚;`缩放特征` 会影响同一对象上的高度、厚度和其它尺寸,不适合只想改孔壁的场景。 |
| 盲孔 / 盲槽 | 识别到疑似底面 Face 时,可以改 `盲孔/盲槽深度`,并通过 `影响范围` 选择 `改底面深度``缩放特征``改底面深度` 加深会沿开口到底面方向切削,变浅会从新底面到旧底面补料;`缩放特征` 会沿孔/槽轴向缩放所属特征或 Solid。 | STEP 不保存真实孔深历史;找不到可靠底面或底面被复杂拓扑切碎时,深度只能只读或需要手动辅助;`缩放特征` 会影响同一对象上的壁厚、孔距和其它轴向尺寸,不适合只想改孔底位置的场景。 |
| 槽 / 半孔 | 可以改槽宽、槽深、弧长、弧角、开口角和简单局部轴心;槽宽、槽深和弧长会在 `影响范围` 里选择 `只改槽壁``缩放特征`。长圆槽的轴心、总长度和中心距会在点击修改时自动尝试配对另一个半圆端,也可以手动填写配对端 Face ID。 | 轴心仍是受限的局部扇形槽/胶囊槽重建;复杂草图槽、非圆柱槽、多槽关联迁移、链式槽和跨多个不规则面的槽还不是稳定能力。`只改槽壁` 会局部重建槽,`缩放特征` 会缩放所属对象并影响其它尺寸;如果结果真的把一个 Solid 拆成多个 Solid,会自动回滚。 |
| 圆柱凸台 / 外圆 | 完整圆柱凸台可以改直径、半径、高度和轴心;普通完整圆柱也有高度兜底修改。凸台的 `直径``半径``高度``轴心` 会在 `影响范围` 里选择 `只改凸台``拉伸/切除端盖``移动凸台``缩放特征``移动特征`。 | 复杂凸台、异形凸台、多台阶凸台和不完整外圆不能保证稳定重建;`缩放特征` 会连带改变同一对象上的高度、厚度和其它尺寸,不能当作单独重建凸台包络或只移动端盖使用。 |
| 已有圆角 / 倒圆面 | 部分规则圆柱倒圆面可以改 `圆角半径``圆角弧长`,并通过 `影响范围` 选择 `重建圆角``缩放特征``重建圆角` 会先移除旧圆角再重建新圆角;`缩放特征` 会把目标值换算为圆柱直径比例,再整体缩放所属特征或 Solid。 | 复杂 blend、支撑面不明确、圆角链或恢复锐边失败时会阻止或回滚;`缩放特征` 会影响同一对象上的其它尺寸,不适合只想改圆角本身的场景。 |
| Edge | 直线 Edge 可以尝试改目标长度、起点坐标、中心坐标和终点坐标;直线 Edge 可以加倒圆、对称倒角、不等距倒角和距离+角度倒角;圆形/圆弧 Edge 可以尝试改半径、直径和 `圆心/轴心`,其中圆心移动会把圆边移动量转成相邻孔/槽/凸台的轴心移动;椭圆 Edge 可以改 `椭圆主半径``椭圆小半径`,分别沿主轴或小轴单轴缩放所属对象。 | “通用任意 Edge”仍是受限实现;复杂曲线、B-spline、和无法确定局部变形区域的 Edge 可能走高风险 fallback 或失败;圆Edge轴心移动需要找到稳定相邻圆柱特征;椭圆单轴缩放会影响同一对象上同方向的其它几何,不是恢复 CAD 草图约束。 |
| 圆锥 / 球 / 环面 | 圆锥可以改 `参考半径``参考直径``半角`:简单圆锥会解析重建,嵌入式锥孔/沉孔会优先局部重切;选中圆锥特征时会显示可识别的小端半径、大端半径、锥孔高度和半角,便于判断真实修改对象。球面可以改 `半径(整体)``直径(整体)`;环面可以改 `主半径(整体)``主直径(整体)``小半径(整体)``小直径(整体)`。 | B-Rep 里的 Face 可以是平面、圆柱面、圆锥面等;半角属于圆锥面属性,1° 左右的浅锥/拔模面看起来会很像平面。圆锥局部重切主要覆盖可识别的双圆边界锥孔;复杂圆锥/拔模面的参考半径、参考直径和半角暂不开放,会提前禁用或快速阻止,而不是进入危险整体缩放或布尔计算。解析曲面的识别评分过低、置信度低或存在识别限制时,属性会保留当前值展示,但修改按钮会变为不可用并显示原因。其它复杂球面和环面仍可能几何缩放所属特征或 Solid,不是恢复原 CAD 历史参数,复杂相邻拓扑可能失败或回滚。 |
| 圆锥 / 球 / 环面 | 圆锥可以改 `参考半径``参考直径``半角`:简单圆锥会解析重建,嵌入式锥孔/沉孔会优先局部重切;选中圆锥特征时会显示可识别的小端半径、大端半径、锥孔高度和半角,便于判断真实修改对象。球面可以改 `半径(缩放特征)``直径(缩放特征)`;环面可以改 `主半径(缩放特征)``主直径(缩放特征)``小半径(缩放特征)``小直径(缩放特征)`。 | B-Rep 里的 Face 可以是平面、圆柱面、圆锥面等;半角属于圆锥面属性,1° 左右的浅锥/拔模面看起来会很像平面。圆锥局部重切主要覆盖可识别的双圆边界锥孔;复杂圆锥/拔模面的参考半径、参考直径和半角暂不开放,会提前禁用或快速阻止,而不是进入危险整体缩放或布尔计算。解析曲面的识别评分过低、置信度低或存在识别限制时,属性会保留当前值展示,但修改按钮会变为不可用并显示原因。其它复杂球面和环面仍可能几何缩放所属特征或 Solid,不是恢复原 CAD 历史参数,复杂相邻拓扑可能失败或回滚。 |
| Solid / 特征整体 | 可以对选中 Solid 或当前单一模型做平移、旋转和按包围盒尺寸 / 体积 / 表面积的缩放类修改。 | 还没有装配约束、零件间关联约束或完整多零件装配编辑。 |
| 只读信息 | 面积、体积、包围盒、曲面类型、相邻关系、候选特征说明等会展示出来帮助判断。 | 标成只读的值不会被 `参数化建模` 修改;显示、测量和导出不是建模操作。 |
@@ -238,18 +244,19 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 特征
- 鼠标悬停对象会以红色预高亮,真正选中后会以黄色高亮。
- 普通 Face / 特征点选优先走轻量选择:只高亮命中的对象并显示基础参数,避免旋转、悬停、点选时触发同域面、端盖、底面等重计算;真正执行编辑计划、扫描候选或历史定位时才做更深识别。
- 拉后程序会尽量保留侧壁面区域的 `逻辑 Face ID`:即使 OCCT 把原来的一个侧壁拓扑Face重建成上下两段,新旧两段也会绑定回拉前那片侧壁的逻辑 ID。属性表会同时显示 `逻辑 Face ID``拓扑 Face ID`;前者面向用户选择和按 ID 定位,后者用于调试当前 B-Rep 拓扑。
-伸/切除后程序会尽量保留侧壁面区域的 `逻辑 Face ID`:即使 OCCT 把原来的一个侧壁拓扑Face重建成上下两段,新旧两段也会绑定回拉伸/切除前那片侧壁的逻辑 ID。属性表会同时显示 `逻辑 Face ID``拓扑 Face ID`;前者面向用户选择和按 ID 定位,后者用于调试当前 B-Rep 拓扑。
- 操作历史会记录 `target_logical_id` 和当时的拓扑Face ID。点击历史记录定位Face / 特征时,会优先用逻辑 ID 找回当前模型中的整片面区域;撤销/重做快照也会保留这层逻辑 ID 映射。
- 鼠标选择会按当前模式做就近映射;例如 `Edge` 模式点到面时,会自动选择鼠标附近的边界Edge。
- 支持按当前选择模式输入 ID 直接选择零件 / Solid / Face / Edge / 特征。
- Face / 特征按 ID 选择会优先解析 `逻辑 Face ID`。例如拉前侧壁是 558拉后当前拓扑里变成 1556 和 1802,只要它们被绑定到逻辑 558,输入 558 就会选中这片面区域。
- Face / 特征按 ID 选择会优先解析 `逻辑 Face ID`。例如拉伸/切除前侧壁是 558,拉伸/切除后当前拓扑里变成 1556 和 1802,只要它们被绑定到逻辑 558,输入 558 就会选中这片面区域。
- Face / 特征的 `复制 ID` 也优先复制 `逻辑 Face ID`,属性区会同时显示 `逻辑选择 ID``当前拓扑 Face ID`;这样编辑后拓扑编号变化时,用户仍能用原来的逻辑 ID 回到同一个面区域。
- `鼠标选择模式` 下拉框关闭时会把鼠标滚轮交还给左侧滚动面板,避免鼠标经过时误切换模式;打开下拉列表后仍可用滚轮滚动选项。
- `按 ID 选择` 会在输入框和 `选择` 按钮之间同步显示当前鼠标选择模式。
- `特征` 模式现在会把点到的Face解释为几何特征候选:
- 平面会识别为可拉平面候选。
- 平面会尝试查找同一Solid内投影重叠的相对平面,用于估算薄壁/壳体局部区域厚度。
- 平面会识别为可拉伸/切除平面候选。
- 平面会尝试查找同一Solid内投影重叠的相对平面,用于估算壳体局部区域厚度。
- 圆柱面会识别为圆柱孔候选、槽/半孔候选、圆角/倒圆候选、凸台/外圆候选或未明确圆柱特征。
- 圆柱特征会先把同一实体内同轴、同半径且轴向连续/重叠的完整圆柱Face当作同一侧壁区域,再尝试找出边界Edge、相邻Face、端面Face、开口端相邻Face和疑似底面Face,并一起高亮相关局部区域。
- 圆柱特征会先把同一实体内同轴、同半径且轴向连续/重叠的完整圆柱Face当作同一侧壁区域,再尝试找出边界Edge、相邻Face、端面Face、开口端相邻Face和疑似底面 Face,并一起高亮相关局部区域。
- 槽/半孔候选会额外显示槽宽、圆弧长度、槽深估算和槽边界相邻Face。
- 圆角/倒圆候选会额外显示已有圆角半径估算、圆弧角度、圆弧长度和相邻支撑Face。
- 属性面板会统一显示 `识别摘要``识别候选``识别置信度``识别风险``识别评分``识别结论``识别依据``识别限制``受限能力`。这些字段来自当前已知的 B-Rep 几何事实、一级拓扑关系、同域区域、材料采样和阻止原因,用来说明程序为什么把对象当作某类候选,而不是把猜测隐藏在代码里。`识别限制` 表示这个候选整体不能稳定编辑;`受限能力` 表示对象本身可识别、某些修改可用,但另一些子能力暂不开放。低置信解析曲面不会开放可编辑按钮,避免把“底层曲面类型像圆锥/球/环面”误当成稳定 CAD 设计参数。
@@ -270,20 +277,20 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 零件 / Solid 的包围盒、尺寸、体积、重心、表面积。
- Face 的方向、面积中心、UV 参数范围、边界边数量、包围盒。
- 平面Face的几何法向和按拓扑方向修正后的法向。
- 平面Face的拉向外方向、向内方向和方向判断置信度。
- 平面Face的拉伸/切除向外方向、向内方向和方向判断置信度。
- 圆柱Face的轴线点、轴方向、半径、直径、角度跨度、估算高度和是否接近完整圆柱。
- Edge 的参数范围、起点、终点、长度中心、包围盒。
- 直线Edge的方向,圆弧Edge的圆心、轴线、半径和直径。
- 属性面板支持复制当前对象 ID、拾取坐标和完整信息。
- 面/边属性查询带缓存;重复选择同一个对象会复用上次计算结果,模型编辑、撤销、重做或重新加载后缓存会自动清空。
- 支持高亮选中的零件、Solid、Face、Edge或特征候选,并在鼠标悬停时用红色预览当前可选对象;普通 Face / 特征点选现在优先使用轻量信息,避免一次点击就扫描同域面、端盖和底面,当前选中对象表会先显示可安全快速得到的参数,点击具体修改或手动扫描时再生成完整编辑计划。
- 默认模型边线渲染会隐藏同一平面或同一圆柱面内部的拓扑分割边,也会隐藏同域区域里几何位置重复的拼接边,减少布尔拉后新旧侧壁交界处看起来像“两块拼起来”的视觉伤疤;`显示` 面板里的 `显示同域内部边` 可以临时打开完整拓扑边线。如果按 ID 或 Edge 模式专门选中这类内部Edge,仍然可以单独高亮查看。
- 默认模型边线渲染会隐藏同一平面或同一圆柱面内部的拓扑分割边,也会隐藏同域区域里几何位置重复的拼接边,减少布尔拉伸/切除后新旧侧壁交界处看起来像“两块拼起来”的视觉伤疤;`显示` 面板里的 `显示同域内部边` 可以临时打开完整拓扑边线。如果按 ID 或 Edge 模式专门选中这类内部Edge,仍然可以单独高亮查看。
- 支持只显示当前选中对象、对准当前选中对象,并可一键恢复显示完整模型。
- 支持两点测量:
- 可以把当前选中对象的拾取点设为 A 或 B;如果没有拾取点,会退回使用对象中心、面积中心、长度中心、重心或包围盒中心。
- 设置 A/B 后会显示两点距离和 X/Y/Z 方向差值。
- 3D 视图中会显示一条青色测量线;测量结果可以复制,加载新模型时会自动清除旧测量。
- 特征模式会先显示特征来源Face、基础几何参数和当前可用的快速编辑入口;侧壁Face、同域圆柱范围、端面Face、疑似底面Face、开口端相邻Face和边界Edge等深层信息会在扫描候选、执行编辑计划或历史定位时再计算。
- 特征模式会先显示特征来源Face、基础几何参数和当前可用的快速编辑入口;侧壁Face、同域圆柱范围、端面Face、疑似底面 Face、开口端相邻Face和边界Edge等深层信息会在扫描候选、执行编辑计划或历史定位时再计算。
- 支持导出当前完整模型为 STEP。
- 支持导出选中的零件为 STEP。
- 支持导出选中的Solid为 STEP。
@@ -297,7 +304,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 支持扫描可编辑对象:
- 默认扫描优先保证界面响应速度,会列出一组有代表性的候选。
- `深度扫描` 会尝试列出更多候选,适合默认列表里没有找到目标对象时使用。
-拉的平面Face。
- 可拉伸/切除的平面Face。
- 可调整孔径的圆柱Face候选。
- 可调整槽/半孔宽度、深度、圆弧长度和轴心坐标的局部圆柱孔/槽候选。
- 可调整盲孔/盲槽深度的圆柱Face候选。
@@ -307,8 +314,8 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 可尝试直接调整长度的直线Edge候选,简单平面多面体会优先局部边形变。
- 可调整圆Edge半径/直径的圆形或圆弧Edge候选。
- 表格会显示当前值、状态、风险、置信度和说明。
- 盲孔/盲槽深度候选会在列表阶段先确认疑似底面,列表里的深度值优先使用底面Face轴向位置计算。
- 点击表格行会直接选中对应Face或Edge,方便继续执行拉、孔径调整、槽/半孔宽度/深度/圆弧长度调整、凸台高度调整、盲孔/盲槽深度调整、Edge倒圆、Edge倒角或修改Edge长度。
- 盲孔/盲槽深度候选会在列表阶段先确认疑似底面,列表里的深度值优先使用底面 Face轴向位置计算。
- 点击表格行会直接选中对应Face或Edge,方便继续执行拉伸/切除、孔径调整、槽/半孔宽度/深度/圆弧长度调整、凸台高度调整、盲孔/盲槽深度调整、Edge倒圆、Edge倒角或修改Edge长度。
- 支持列出圆柱面候选特征。
- 圆柱面候选会显示在表格中,点击候选行可以直接选中并高亮对应 Face。
- 圆柱面候选可以按类型筛选:全部、孔/槽候选、圆角候选、凸柱/外圆候选、未明确分类。
@@ -321,17 +328,17 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 圆柱面候选表格会显示直径、角度跨度、估算高度和置信度。
- 圆柱面候选表格会显示 `risk`,用于提示调整圆柱孔径的风险。
- 圆柱面候选表默认列出前 60 个候选,以避免打开大模型时刷新过慢。
- 当前版本支持平面Face拉。
- 平面拉会自动判断面两侧的 inside / outside,输入正数表示向外加料,输入负数表示向内切削。
- 拉平面会先显示半透明预览体,表示拉方向和大致范围,然后在后台执行真实 OCCT 布尔计算。
- 拉平面现在会从选中Face出发,自动查找共面且相接/重叠的相邻Face,并把它们作为同一片平面区域一起拉。
- 当前版本支持平面Face拉伸/切除
- 平面拉伸/切除会自动判断面两侧的 inside / outside,输入正数表示向外加料,输入负数表示向内切削。
-伸/切除平面会先显示半透明预览体,表示拉伸/切除方向和大致范围,然后在后台执行真实 OCCT 布尔计算。
-伸/切除平面现在会从选中Face出发,自动查找共面且相接/重叠的相邻Face,并把它们作为同一片平面区域一起拉伸/切除
- 这可以减少 STEP 碎面导致的“只推了一小块,导出后像贴上去一块,中间有伤疤”的情况。
- 如果选中的平面被识别为完整圆柱端盖,并且是向外拉长,程序会优先使用“完整旧段 + 新段圆柱”的专用 Fuse 路线,而不是只把一个短圆柱 prism 拼到端面上。简单圆柱场景下,这会让拉长结果保持为一个连续圆柱侧面,减少新旧圆柱段拼接痕迹。
- 拉加料时,工具体会向原实体内部保留一个很小的重叠量,再执行 Fuse,避免只靠两个面贴合导致导出后像“原模型 + 新增块”叠在一起。
- Fuse/Cut 完成后会执行更强的拉结果清理:按模型尺寸设置小容差,显式禁止保留 internal edges,并多轮执行 OCCT 同域面/边合并,减少第三方 CAD 中出现不必要共面分割线的概率。
-伸/切除加料时,工具体会向原实体内部保留一个很小的重叠量,再执行 Fuse,避免只靠两个面贴合导致导出后像“原模型 + 新增块”叠在一起。
- Fuse/Cut 完成后会执行更强的拉伸/切除结果清理:按模型尺寸设置小容差,显式禁止保留 internal edges,并多轮执行 OCCT 同域面/边合并,减少第三方 CAD 中出现不必要共面分割线的概率。
- 布尔计算前会启用 OCCT glue/fuzzy 选项,布尔后会调用 `SimplifyResult` 和同域合并;但某些 CAD/OCCT 场景仍可能保留拓扑分割边。当前程序会先在选择层把同域平面/圆柱区域作为整体高亮,并在默认边线渲染中隐藏同域内部拓扑边和几何重复拼接边;真正从 STEP/B-Rep 拓扑里消除这类分割边,还需要后续做更专门的局部圆柱/侧壁重建。
- 如果拉本身产生了真实台阶边界,第三方 CAD 仍会显示这条几何边;清理只能减少不必要的共面残留边,不能消除真实形状边界。
- 拉平面会先生成操作计划:检查距离是否为 0、方向置信度和相对Face尺寸的风险;能拉时直接执行,不能拉时会提示阻止原因。
- 如果拉伸/切除本身产生了真实台阶边界,第三方 CAD 仍会显示这条几何边;清理只能减少不必要的共面残留边,不能消除真实形状边界。
-伸/切除平面会先生成操作计划:检查距离是否为 0、方向置信度和相对Face尺寸的风险;能拉伸/切除时直接执行,不能拉伸/切除时会提示阻止原因。
- 调整圆柱孔径也会先显示半透明预览体:红色表示切削 cutter,绿色表示缩小孔径时的补料范围;如果圆柱侧壁被拆成多段同域Face,工具范围会优先覆盖整段同域圆柱。
- 当前版本支持盲孔/盲槽深度调整:
- 只对 `hole/groove candidate` 且端部类型为 `blind`、并识别到疑似底面 Face 的圆柱面候选开放。
@@ -339,7 +346,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 目标深度小于当前估算深度时,会从目标新底面到旧底面方向补料,让孔变浅。
- 调整盲孔深度会先显示半透明预览体:红色表示加深切削范围,绿色表示变浅补料范围。
- 孔深调整计划会记录当前深度、目标深度、变化比例、疑似底面、开口端和工具参数。
- 当前深度会优先使用疑似底面Face在圆柱轴线上的位置计算;如果相邻端面找不到底面,会额外扫描同一Solid内轴线附近的平面圆盘候选。
- 当前深度会优先使用疑似底面 Face在圆柱轴线上的位置计算;如果相邻端面找不到底面,会额外扫描同一Solid内轴线附近的平面圆盘候选。
- 当前版本支持槽/半孔候选结构化识别:
- 对局部圆柱形 `hole/groove candidate`,会标记为 `partial-cylindrical-groove`
- 会估算槽宽、圆弧长度和槽深。
@@ -365,14 +372,14 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 当前版本支持长圆槽/槽孔总长度和中心距调整:
- 你可以直接在当前选中对象表里修改 `槽孔总长度``槽孔中心距`,不需要先填写配对端 Face ID。
- 点击修改时,程序会自动寻找另一个半圆槽端;识别成功后保持槽宽不变,先补旧长圆槽体,再切出目标总长度。
- 如果自动配对失败,可以在当前选中对象表中填写 `槽孔配对端Face ID`,后续槽宽、槽深、圆弧长度、总长度和中心距调整会优先尝试使用这个手动配对端。
- 当前版本支持薄壁/壳体厚度调整:
- 如果自动配对失败,可以在当前选中对象表中填写 `槽孔配对端 Face ID`,后续槽宽、槽深、圆弧长度、总长度和中心距调整会优先尝试使用这个手动配对端。
- 当前版本支持壳体厚度调整:
- 对已找到投影重叠相对平面的平面候选开放。
- 输入目标厚度后,程序会把厚度变化换算成当前平面区域的拉距离,保留相对平面不动。
- 会先显示半透明拉预览,并记录当前厚度、目标厚度、相对平面Face、重叠率和拉距离。
- 这是局部 B-Rep 平面拉近似,不是 CAD 壳命令或完整壳体参数编辑。
- 当前选中对象表的 `薄壁厚度` 会通过 `影响范围` 选择 `推拉当前面``调整整个特征`;后者会沿厚度方向缩放所属特征或 Solid,让厚度接近目标值,同一对象上的其它厚度方向尺寸也会跟随变化。
- 对简单全平面薄板/盒体,`调整整个特征` 会优先用平面重建路线,避免把原本的平面 Face 变成 B-spline 曲面,并保持原选中 Face 的逻辑 ID 继续指向修改后的面。
- 输入目标厚度后,程序会把厚度变化换算成当前平面区域的拉伸/切除距离,保留相对平面不动。
- 会先显示半透明拉伸/切除预览,并记录当前厚度、目标厚度、相对平面Face、重叠率和拉伸/切除距离。
- 这是局部 B-Rep 平面拉伸/切除近似,不是 CAD 壳命令或完整壳体参数编辑。
- 当前选中对象表的 `壳体厚度` 会通过 `影响范围` 选择 `拉伸/切除``缩放特征`;后者会沿厚度方向缩放所属特征或 Solid,让厚度接近目标值,同一对象上的其它厚度方向尺寸也会跟随变化。
- 对简单全平面薄板/盒体,`缩放特征` 会优先用平面重建路线,避免把原本的平面 Face 变成 B-spline 曲面,并保持原选中 Face 的逻辑 ID 继续指向修改后的面。
- 当前版本支持已有圆角/倒圆候选结构化识别:
- 对局部小半径圆柱面,会标记为 `round/fillet candidate`
- 会估算已有圆角半径、圆弧角度和圆弧长度。
@@ -380,7 +387,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- `可编辑对象` 会把已有圆角候选单独列为 `修改已有圆角半径`,点击后会选中该圆角 Face,并把圆角半径输入框预填为当前估算半径的参考目标值。
- 选中已有圆角候选时,不会再误把它填入 `孔直径`,避免把圆角当孔径修改。
- 当前版本的已有圆角半径修改会先尝试移除圆角Face,再在恢复出的锐边上重新倒圆;也可以在当前选中对象表中输入已有圆角圆弧长度,程序会按当前圆弧角度换算成目标半径。只对较规整的圆柱圆角候选开放,复杂 blend 可能失败并回滚。
- 当前选中对象表的 `圆角半径` / `圆角弧长` 会通过 `影响范围` 选择 `重建圆角``调整整个特征`。后者不会移除并重建圆角,而是按目标半径或弧长比例整体缩放所属特征或 Solid,同一对象上的其它尺寸也会变化。
- 当前选中对象表的 `圆角半径` / `圆角弧长` 会通过 `影响范围` 选择 `重建圆角``缩放特征`。后者不会移除并重建圆角,而是按目标半径或弧长比例整体缩放所属特征或 Solid,同一对象上的其它尺寸也会变化。
- 当前版本支持圆柱孔封堵:
- 只对 `hole/groove candidate` 且角度跨度接近完整圆柱的孔开放。
- 不对半孔/槽开放,避免用完整圆柱补料时把不该补的位置也补上。
@@ -392,14 +399,14 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 不处理局部外圆角、圆角面或未明确圆柱面,避免把圆角当凸台修改。
- 目标直径变大时,会在同域圆柱侧壁范围内生成目标直径圆柱并 Fuse。
- 目标直径变小时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建,避免环形 cutter 在部分场景下把实体切空。
- 当前选中对象表的 `直径` / `半径` 会通过 `影响范围` 选择 `只改凸台``调整整个特征`;后者用于按目标尺寸比例均匀缩放所属特征或 Solid,不是凸台包络重建,会连带影响其它尺寸。
- 当前选中对象表的 `直径` / `半径` 会通过 `影响范围` 选择 `只改凸台``缩放特征`;后者用于按目标尺寸比例均匀缩放所属特征或 Solid,不是凸台包络重建,会连带影响其它尺寸。
- 会先显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。
- 当前默认模型可能没有明确凸台候选;包含完整圆柱凸台的 STEP 文件可以使用这个能力。
- 当前版本支持圆柱凸台高度和轴心坐标调整:
- 只对已识别为完整圆柱凸台、并找到可拉端盖 Face 的候选开放。
- 输入目标高度后,程序会选择凸台端盖并把高度变化换算成端盖拉距离。
- 只对已识别为完整圆柱凸台、并找到可拉伸/切除端盖 Face 的候选开放。
- 输入目标高度后,程序会选择凸台端盖并把高度变化换算成端盖拉伸/切除距离。
- 输入目标轴心坐标后,程序会先移除旧凸台包络,再按同直径在目标轴心补出凸台。
- 这不是 CAD 历史里的拉伸特征高度参数;端盖识别不稳定或拉计划被阻止时会拒绝执行。
- 这不是 CAD 历史里的拉伸特征高度参数;端盖识别不稳定或拉伸/切除计划被阻止时会拒绝执行。
- 当前版本支持直线Edge添加圆角:
- 只对直线Edge开放。
- 会先检查边长、相邻Face数量和目标半径相对边长的比例。
@@ -420,14 +427,14 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- `长度基准` 行可以选择自动、中心、固定起点、固定终点。
- `长度` 行的 `影响范围` 可以选择 `自动``只改当前Edge``移动端面``相邻圆柱``缩放所属`
- `只变当前Edge` 会强制使用局部边形变;如果当前Edge不满足简单全平面多面体条件,会直接阻止,不会偷偷改成整体缩放。
- `移动端面` 会强制找端点附近、方向匹配的平面端面并做端面拉;对正方体这类模型,会得到更像“变成长方体”的结果。如果找不到端面拉路径,会直接阻止。
- `移动端面` 会强制找端点附近、方向匹配的平面端面并做端面拉伸/切除;对正方体这类模型,会得到更像“变成长方体”的结果。如果找不到端面拉伸/切除路径,会直接阻止。
- `缩放所属` 会强制对所属特征或 Solid 做轴向/整体缩放,是高风险兜底语义,会影响其它尺寸。
- 对直线Edge,当前选中对象表会额外提供 `起点``中心``终点`,可以直接输入目标 X/Y/Z 坐标来移动单个端点或整条边。
- 对正方体这类全平面多面体,直线Edge会优先走“局部边形变”:只移动被选Edge的一个端点或两个端点,并重建周边平面。
- 局部边形变不会把整个端面拉走,因此正方体不会默认变成长方体;相邻面会按被移动的边自然变成梯形或拆成三角面。
- 直线Edge端点/中心坐标修改复用同一套局部边形变;当前只对简单全平面多面体开放,复杂曲面、带内孔Face或大型复杂Solid会阻止执行。
- `自动` 默认固定起点、移动终点;`固定起点` / `固定终点` 会明确控制哪一端不动;`中心` 会让两端各移动一半。
- 如果模型不是简单全平面多面体,直线Edge会继续尝试寻找端点附近、方向匹配的平面端面,并把“改边长”转换成该端面的平面拉操作。
- 如果模型不是简单全平面多面体,直线Edge会继续尝试寻找端点附近、方向匹配的平面端面,并把“改边长”转换成该端面的平面拉伸/切除操作。
- 圆形/圆弧Edge会优先检查相邻圆柱面;如果能复用孔/槽或凸台直径编辑,就把Edge长度按比例换算成目标圆柱直径再执行局部编辑。
- 圆边没有可复用圆柱面时,会优先围绕圆边轴线做径向缩放,尽量只改半径方向并保留轴向尺寸;这类一般仿射可能把解析圆/圆锥/圆柱几何转换为 B-spline。
- 椭圆 Edge 的 `椭圆主半径` / `椭圆小半径` 是单独的属性行:主半径会沿椭圆主轴方向单轴缩放,小半径会沿小轴方向单轴缩放;另一个半径方向尽量不动。
@@ -455,11 +462,11 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 如果后台编辑失败,程序会自动尝试恢复到编辑前快照,避免出现“提示失败但模型已经被部分改动”的状态。
- 当前版本支持圆柱孔/圆柱面扩大切削。
- 当前版本支持圆柱孔/圆柱面缩小孔径:先补料,再用目标直径重切。
- 当前版本支持完整圆柱孔轴心坐标修改:在当前选中对象表里修改 `轴心`,并把 `影响范围` 设为 `移动孔` 时,程序会先补旧孔,再按同直径在目标轴心位置重切新孔;半孔/开放槽会走 `移动槽/半孔` 语义,避免整圆柱工具误补材料。把 `影响范围` 设为 `移动整个特征` 时,会平移所属特征或 Solid。
- 当前版本支持孔/槽 `直径` / `半径``调整整个特征` 影响范围:
- 当前版本支持完整圆柱孔轴心坐标修改:在当前选中对象表里修改 `轴心`,并把 `影响范围` 设为 `移动孔` 时,程序会先补旧孔,再按同直径在目标轴心位置重切新孔;半孔/开放槽会走 `移动槽/半孔` 语义,避免整圆柱工具误补材料。把 `影响范围` 设为 `移动特征` 时,会平移所属特征或 Solid。
- 当前版本支持孔/槽 `直径` / `半径``缩放特征` 影响范围:
- 这条路线不会重切孔壁,而是按目标直径或半径与当前值的比例均匀缩放所属特征或 Solid。
- 高度、厚度和同一对象上的其它尺寸会同比例变化。
- 如果你只想让孔径变大/变小,优先在 `直径``半径` 行把 `影响范围` 设为 `只改孔/槽壁`;如果你的建模意图是让所属圆柱区域整体缩放,再设为 `调整整个特征`
- 如果你只想让孔径变大/变小,优先在 `直径``半径` 行把 `影响范围` 设为 `只改孔/槽壁`;如果你的建模意图是让所属圆柱区域整体缩放,再设为 `缩放特征`
- 圆柱孔/圆柱面孔径调整现在会先生成切削计划:
- 目标直径必须大于 0;如果目标直径与当前直径几乎相同,会直接阻止。
- 会计算直径变化比例;变化超过当前直径 35% 会提高到中风险,超过 100% 会提高到高风险。
@@ -471,7 +478,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示
- 实际切削优先使用有限长度 cutter,按选中圆柱面的 V 参数范围加少量余量生成,不再默认贯穿整个零件。
- 会沿圆柱轴线采样两端材料状态,初步区分通孔/开口端、盲孔和封闭端。
- 会把圆柱相邻平面按轴线位置分成起点端/终点端,并结合端部采样标记疑似盲孔底面;相邻端面缺失时,会尝试扫描轴线附近的平面圆盘候选作为兜底。
- 孔径修改计划会记录疑似底面Face、开口端相邻Face和 cutter 底面保护状态。
- 孔径修改计划会记录疑似底面 Face、开口端相邻Face和 cutter 底面保护状态。
- 对盲孔/封闭端,有限长度 cutter 会使用更小的端部余量,降低切深变长的风险。
- 支持撤销/重做编辑。
- 撤销/重做会先确认模型恢复成功,再移动历史栈;如果恢复失败,会尽量回到操作前状态。
@@ -518,8 +525,8 @@ vertices: 3262
已经有当前版本的初步实现,但还需要继续稳定和完善:
- 复杂特征识别:已经有平面、圆柱、边、孔/槽候选、槽/半孔候选、已有圆角候选、圆柱凸台候选等基础候选列表,但还不是完整 CAD 语义级识别。
- 盲孔/盲槽深度:已经能对部分可识别候选做深度估算和编辑,并会优先使用疑似底面Face的轴向位置计算当前深度;但还不能保证所有模型都能恢复真实 CAD 语义深度。
- 槽、凸台、壳体局部区域:已经有圆柱孔、槽/半孔候选结构化识别、槽/半孔宽度/深度/圆弧长度/圆弧角度/开口角度调整、槽宽/槽深/弧长的整体缩放所属对象语义、长圆槽总长度/中心距执行时自动配对和手动配对端 Face ID、完整圆柱凸台直径/半径/高度/轴心坐标修改、普通完整圆柱高度兜底拉或通过 `影响范围` 做轴向整体缩放,以及平面薄壁/壳体相对面厚度估算、局部厚度拉和通过 `影响范围` 做厚度方向整体缩放;复杂槽、复杂凸台和完整壳体区域仍需要继续做。
- 盲孔/盲槽深度:已经能对部分可识别候选做深度估算和编辑,并会优先使用疑似底面 Face的轴向位置计算当前深度;但还不能保证所有模型都能恢复真实 CAD 语义深度。
- 槽、凸台、壳体局部区域:已经有圆柱孔、槽/半孔候选结构化识别、槽/半孔宽度/深度/圆弧长度/圆弧角度/开口角度调整、槽宽/槽深/弧长的整体缩放所属对象语义、长圆槽总长度/中心距执行时自动配对和手动配对端 Face ID、完整圆柱凸台直径/半径/高度/轴心坐标修改、普通完整圆柱高度兜底拉伸/切除或通过 `影响范围` 做轴向整体缩放,以及平面壳体相对面厚度估算、局部厚度拉伸/切除和通过 `影响范围` 做厚度方向整体缩放;复杂槽、复杂凸台和完整壳体区域仍需要继续做。
- 已有圆角/倒圆面半径:已经有当前版本的受限修改实现,使用 defeature + refillet 路线,但复杂 blend、支撑面不明确或恢复锐边失败时仍会阻止或回滚。
- 局部偏差分析:已经有编辑前后体积、面积、包围盒、距离热力图和差异报告,但还不是工程公差级的局部区域偏差报告。
- 错误恢复和模型修复:已经有编辑前快照、失败回滚、基础 B-Rep 校验、同域面合并、布尔结果自动修复,以及一键修复当前模型或选中零件/solid,但还不是完整的模型修复流水线。
@@ -534,7 +541,7 @@ vertices: 3262
建议下一步按这个顺序推进:
1. 先集中完成 Face 一级拓扑关系编辑:面积、面宽/面高、中心、偏移和薄壁厚度,都要明确 `只改当前面``推拉当前面``移动整个特征``调整整个特征` 等语义;当前阶段只传播当前 Face/同域碎片和共享边相邻 Face,不递归处理二级、三级关系。
1. 先集中完成 Face 一级拓扑关系编辑:面积、U向尺寸/V向尺寸、中心、偏移曲面和壳体厚度,都要明确 `局部重建``拉伸/切除``移动特征``缩放特征` 等语义;当前阶段只传播当前 Face/同域碎片和共享边相邻 Face,不递归处理二级、三级关系。
2. Face 一级关系闭环稳定后,再集中做槽/孔类特征的一级关系:槽宽、槽深、弧长、弧角、开口角、轴心、长圆槽总长度、中心距、孔径、孔轴心和盲孔/盲槽深度要统一验证。
3. 槽/孔一级关系稳定后,再集中做 Edge 一级关系:直线 Edge 长度、起点、中心、终点和倒圆/倒角语义先做完整,再处理圆弧 Edge、椭圆 Edge 和高风险 fallback。
4. Edge 闭环稳定后,再集中做凸台、已有圆角、圆锥/球/环面、壳体局部区域等对象;每类都先补一级关系能力和验证,再进入下一类。
@@ -545,11 +552,11 @@ Face 阶段的当前验收口径:
- 当前阶段的 `一级关系` 定义为:选中 Face 本身、必要的同域/共面碎片 Face、这些 Face 的边界 Edge/Vertex,以及与该区域共享边的直接相邻 Face。只共享顶点的对象、相邻 Face 再连出去的 Face 都不作为当前阶段的自动传播范围。
- 选中平面 Face 时,`当前选中对象` 的参数表会显示 `一级关系` 说明行,直接列出当前 Face 区域数量、边界 Edge/Vertex 数量、共享边相邻 Face 数量,并提示二级、三级关系暂不自动传播。
- `python scripts\verify_face_edit_suite.py` 必须通过,集中覆盖面积、面宽/面高、中心、偏移、薄壁厚度、推拉、局部变形禁用、目标值保护、属性回读逻辑 Face ID 保持。
- `python scripts\verify_property_editor_specs.py` 必须通过,确保平面 Face 的属性表使用 `面积``面宽``面高``中心``偏移` 这些用户可理解的名称,并递归检查提示文字里不再出现容易误解的旧词。
- `python scripts\verify_face_edit_suite.py` 必须通过,集中覆盖面积、U向尺寸/V向尺寸、中心、偏移曲面、壳体厚度、拉伸/切除、局部变形禁用、目标值保护、属性回读逻辑 Face ID 保持,以及 UI 里标题/复制 ID/按 ID 选择是否优先使用逻辑 Face ID
- `python scripts\verify_property_editor_specs.py` 必须通过,确保平面 Face 的属性表使用 `面积``U向尺寸``V向尺寸``中心``偏移曲面` 这些用户可理解的名称,并递归检查提示文字里不再出现容易误解的旧词。
- `python scripts\verify_face_first_level_topology.py` 必须通过,确保普通正方体 Face 的一级关系能识别 4 条边界 Edge、4 个边界 Vertex、4 个共享边相邻 Face,属性表能显示这份一级关系摘要,并证明局部移动后目标 Face 的边界 Vertex 与共享边 Edge 已到新位置、一级侧面跟随重建、二级底面不跟随移动。
- `python scripts\verify_cylindrical_first_level_topology.py` 必须通过,确保孔/槽类圆柱特征的计划层已经能识别圆柱侧壁、边界 Edge/Vertex、直接相邻 Face、底面/开口面/槽边界面,并明确二级、三级关系暂不自动传播。特征参数表也会显示圆柱特征的 `一级关系` 摘要,让用户知道当前孔/槽修改会围绕哪些直接邻域执行;结果校验会拒绝虽然尺寸匹配但一级邻域明显弱于计划要求的圆柱结果。后续继续把这些一级关系用于更细的孔/槽实际重建策略。
- 对不能稳定做 `只改当前面` 的 Face,要禁用该影响范围,并在提示中说明原因;不能让复杂内孔、曲面 Solid 或复杂边界静悄悄走高风险局部变形。
- 对不能稳定做 `局部重建` 的 Face,要禁用该影响范围,并在提示中说明原因;不能让复杂内孔、曲面 Solid 或复杂边界静悄悄走高风险局部变形。
- Face 一级关系闭环后,再进入槽/孔一级关系阶段;不要在 Face 还没有稳定解释和验证时提前分散到 Edge、圆锥/球/环面等对象。
## 怎么使用
@@ -570,7 +577,7 @@ Face 阶段的当前验收口径:
- `Solid`:选择实体。
- `Face`:选择面。
- `Edge`:选择边。
- `特征`:按几何特征候选方式选择。平面会识别为可拉平面候选;圆柱面会进一步识别为圆柱孔、槽/半孔、圆角/倒圆、凸台/外圆等候选。
- `特征`:按几何特征候选方式选择。平面会识别为可拉伸/切除平面候选;圆柱面会进一步识别为圆柱孔、槽/半孔、圆角/倒圆、凸台/外圆等候选。
圆柱面候选会额外显示侧壁 Face、端面 Face、疑似底面 Face、开口端相邻 Face 和边界 Edge。
- 鼠标悬停时,当前模式下将要选中的对象会显示为红色;真正选中后会显示为黄色。
- 如果当前模式是 `Edge`,即使鼠标点到的是面,程序也会从这个面的边界里选取距离鼠标拾取点最近的Edge。
@@ -601,7 +608,7 @@ Face 阶段的当前验收口径:
- Face 模式下 `只显示选中` 会优先隔离当前命中的 Face,避免普通点选时触发同域面重计算;如果当前选择来自编辑计划、扫描候选或历史定位,并且已经解析出特征区域,则会使用这片已知区域。
- `对准选中`:不改变显示范围,只把相机对准当前选中的零件、Solid、Face、Edge 或特征,适合快速查看小孔、小边和局部特征。
- `显示全部`:恢复显示完整模型。
- `显示同域内部边`:默认关闭,用来隐藏同一平面/同一圆柱面内部的拓扑分割边和几何重复拼接边;打开后可以查看完整 B-Rep 拓扑边线,适合调试布尔拉后的分面情况。
- `显示同域内部边`:默认关闭,用来隐藏同一平面/同一圆柱面内部的拓扑分割边和几何重复拼接边;打开后可以查看完整 B-Rep 拓扑边线,适合调试布尔拉伸/切除后的分面情况。
- `导出选中面区域` 会导出当前选中 Face;如果当前选择已经带有明确的特征区域或同域区域信息,则导出这片已知区域。
- Solid / Edge 的显示查询已经使用拓扑索引,避免每次隔离或高亮时反复全模型查找。
- 这是视图操作,不会修改 STEP 几何,也不会写入操作历史。
@@ -622,7 +629,7 @@ Face 阶段的当前验收口径:
- 点击 `深度扫描` 会用更高上限重新扫描,可能需要等待更久。
- 为了避免打开大模型时界面卡住,程序不会在加载 STEP 时自动扫描;需要编辑时再手动点击扫描。
- 当前默认显示一批代表性的可编辑对象,详细几何判断会尽量延后到选中对象或执行编辑前,避免扫描过慢。
- `拉平面` 行表示这个Face是平面;当前主要在 `当前选中对象``偏移` 行里把 `影响范围` 选为 `推拉当前面` 来执行。
- `拉伸/切除平面` 行表示这个Face是平面;当前主要在 `当前选中对象``偏移曲面` 行里把 `影响范围` 选为 `拉伸/切除` 来执行。
- `调整圆柱孔径` 行表示这个 Face 是圆柱面候选,可以配合 `孔直径``调整圆柱孔径` 使用。
- `调整槽/半孔宽度` / `调整槽/半孔深度` / `调整槽/半孔圆弧长度` 行表示这个Face是槽/半孔候选,可以在当前选中对象表里修改槽宽、槽深或圆弧长度。
- `调整圆柱凸台直径` / `调整圆柱凸台高度` 行表示这个Face是较明确的完整圆柱凸台候选,可以在当前选中对象表里修改凸台直径、半径、高度或轴心坐标。
@@ -672,25 +679,25 @@ Face 阶段的当前验收口径:
编辑:
- 编辑按钮会根据当前选择自动启用或禁用。例如未选中平面时不能点击 `拉平面`,未选中直线Edge时不能点击 `给Edge添加圆角` / `给Edge添加倒角`,未选中明确孔/凸台/盲孔候选时对应圆柱编辑按钮会禁用。
- `偏移`
- 编辑按钮会根据当前选择自动启用或禁用。例如未选中平面时不能点击 `拉伸/切除平面`,未选中直线Edge时不能点击 `给Edge添加圆角` / `给Edge添加倒角`,未选中明确孔/凸台/盲孔候选时对应圆柱编辑按钮会禁用。
- `偏移曲面`
- 先选择一个平面 Face。
- 在当前选中对象表里修改 `偏移`,再用 `影响范围` 选择改法。
- `偏移` 是沿当前面垂直方向测到的位置,不是面积,不是移动距离,也不是 X/Y/Z 坐标。
- `推拉当前面` 会把目标位置换算成这次要移动的距离,然后沿当前面的垂直方向加料或切削,适合改变厚度、拉出凸起或切入凹槽。
- `只改当前面` 会把当前 Face 移到目标位置,并重建相邻平面;相邻面可能变斜。
- `移动整个特征` 会沿当前面垂直方向平移所属特征或 Solid;形状和内部尺寸不变。
- 如果选中Face周围有共面且相接/重叠的碎面,`推拉当前面` 会自动把这些Face合成同一片拉区域,属性表和历史记录会显示 `拉共面区域 Face 数`
- 当前拉方向判断通过Solid的 inside / outside 采样得到;如果采样不明确,会退回到拓扑方向法向,并在属性里显示低置信度说明。
- `面积` / `面宽` / `面高`
- 在当前选中对象表里修改 `偏移曲面`,再用 `影响范围` 选择改法。
- `偏移曲面` 是沿当前面垂直方向测到的位置,不是面积,不是移动距离,也不是 X/Y/Z 坐标。
- `拉伸/切除` 会把目标位置换算成这次要移动的距离,然后沿当前面的垂直方向加料或切削,适合改变厚度、拉出凸起或切入凹槽。
- `局部重建` 会把当前 Face 移到目标位置,并重建相邻平面;相邻面可能变斜。
- `移动特征` 会沿当前面垂直方向平移所属特征或 Solid;形状和内部尺寸不变。
- 如果选中Face周围有共面且相接/重叠的碎面,`拉伸/切除` 会自动把这些Face合成同一片拉伸/切除区域,属性表和历史记录会显示 `拉伸/切除共面区域 Face 数`
- 当前拉伸/切除方向判断通过Solid的 inside / outside 采样得到;如果采样不明确,会退回到拓扑方向法向,并在属性里显示低置信度说明。
- `面积` / `U向尺寸` / `V向尺寸`
- 先选择一个 Face,在当前选中对象表里修改目标值,再用 `影响范围` 选择改法。
- `只改当前面` 会在当前 Face 所在平面内移动顶点并重建周边相邻平面;当前只对简单全平面实体开放。
- `调整整个特征` 会缩放所属特征或 Solid;同一对象上的孔、槽、凸台、厚度和其它间距会跟着变化。
- `面宽` / `面高` 是当前 Face 在自身平面内两个稳定方向上的投影尺寸,不是面积,不是模型整体高度,也不是全局 X/Y/Z 包围盒尺寸。
- `局部重建` 会在当前 Face 所在平面内移动顶点并重建周边相邻平面;当前只对简单全平面实体开放。
- `缩放特征` 会缩放所属特征或 Solid;同一对象上的孔、槽、凸台、厚度和其它间距会跟着变化。
- `U向尺寸` / `V向尺寸` 是当前 Face 在自身平面内两个稳定方向上的投影尺寸,不是面积,不是模型整体高度,也不是全局 X/Y/Z 包围盒尺寸。
- `中心`
- 在当前选中对象表里输入目标 X, Y, Z 坐标,再用 `影响范围` 选择改法。
- `只改当前面` 会移动当前 Face 的顶点并重建相邻平面;相邻面可能自然变斜,必要时非共面面会拆成三角面。
- `移动整个特征` 会把目标中心换算成平移向量,并平移这个 Face 所属的特征或 Solid;Face 自身形状和所属对象内部尺寸不变。
- `局部重建` 会移动当前 Face 的顶点并重建相邻平面;相邻面可能自然变斜,必要时非共面面会拆成三角面。
- `移动特征` 会把目标中心换算成平移向量,并平移这个 Face 所属的特征或 Solid;Face 自身形状和所属对象内部尺寸不变。
- `孔直径` + `调整圆柱孔径`
- 先选择一个圆柱面。
- 输入新的直径。
@@ -700,18 +707,18 @@ Face 阶段的当前验收口径:
- 程序会先生成切削计划,检查当前直径、目标直径、候选类型、材料投票和风险。
- 切削计划会显示直径变化量、直径变化比例,以及目标直径相对圆柱面估算高度的比例。
- 切削计划会显示端部类型、深度估算、起点端状态和终点端状态。
- 切削计划会显示疑似底面Face、开口端相邻Face和底面保护说明;这些信息会写入操作历史。
- 切削计划会显示疑似底面 Face、开口端相邻Face和底面保护说明;这些信息会写入操作历史。
- 如果目标直径小于等于 0,或几乎等于当前直径,程序会直接阻止。
- 如果目标直径变化过大,或者目标直径相对圆柱估算高度明显异常,程序会提高风险并要求确认。
- 缩小孔径当前版本只对 `hole/groove candidate` 开放;圆角、凸柱或未明确圆柱面会被直接阻止。
- 缩小孔径属于高风险实验功能,因为它依赖“补料 + 重切”的布尔近似,不是 CAD 历史参数修改。
- 如果当前Face的猜测类型不是 `hole/groove candidate`,或风险不是低风险,程序会先弹窗确认。
- 如果当前 Face的猜测类型不是 `hole/groove candidate`,或风险不是低风险,程序会先弹窗确认。
- 切削计划会显示 cutter 策略、高度和余量;当前优先使用有限长度 cutter,减少误切其他区域。
- 缩小孔径时,操作历史会额外记录补料策略、补料半径和补料高度。
- 端部类型是通过轴线端部 inside / outside 采样得到的估算,不等于 CAD 原始建模历史里的“孔深”参数。
- 如果选到的圆柱面不是孔,而是柱或圆角,结果可能不是你想要的,所以它目前仍是实验功能。
- `直径` / `半径``调整整个特征`
- 先选择一个圆柱 Face,然后在当前选中对象表里修改 `直径``半径`,并把 `影响范围` 设为 `调整整个特征`
- `直径` / `半径``缩放特征`
- 先选择一个圆柱 Face,然后在当前选中对象表里修改 `直径``半径`,并把 `影响范围` 设为 `缩放特征`
- 它会按目标直径或半径比例均匀缩放所属特征或 Solid,适合“整个所属对象一起放大/缩小”的意图。
- 它不适合只想改孔壁的场景,因为高度、厚度和同一对象上的其它尺寸会一起变化。
- `槽/半孔宽度` + `调整槽/半孔宽度`
@@ -728,9 +735,9 @@ Face 阶段的当前验收口径:
- 先选择一个槽/半孔候选 Face。
- 在当前选中对象表里修改 `槽/半孔圆弧长度`,点击该行的 `可修改` 或点击 `参数化建模`
- 程序会按当前圆弧角度把目标圆弧长度换算成目标圆柱直径,再复用圆柱孔/槽调整流程。
- `槽宽` / `槽深` / `弧长``调整整个特征`
- `槽宽` / `槽深` / `弧长``缩放特征`
- 先选择一个槽/半孔候选 Face。
- 在当前选中对象表里修改对应属性,并把 `影响范围` 设为 `调整整个特征`
- 在当前选中对象表里修改对应属性,并把 `影响范围` 设为 `缩放特征`
- 程序会把目标槽宽、槽深或弧长换算成目标圆柱直径,然后按比例整体缩放所属特征或 Solid。
- 这不是局部重切槽;同一所属对象上的高度、厚度和其它尺寸也会跟随变化。
- `槽/半孔圆弧角度` / `槽/半孔开口角度`
@@ -743,27 +750,27 @@ Face 阶段的当前验收口径:
- 在当前选中对象表里修改目标坐标,格式是 `X, Y, Z`,并把 `影响范围` 设为 `移动槽/半孔`
- 程序会先填充旧的局部扇形槽,再在目标轴心位置按同宽度、同圆弧角度切出新槽。
- 这一步只移动选中的局部扇形槽面,不等于完整 CAD 草图里的槽整体迁移;长圆槽、链式槽和复杂槽需要确认结果,失败会回滚。
- `轴心``移动整个特征`
- `轴心``移动特征`
- 同样输入目标 X, Y, Z 坐标。
-`影响范围` 设为 `移动整个特征`
-`影响范围` 设为 `移动特征`
- 程序会把目标轴心换算成平移向量,并平移所属特征或 Solid;不会填旧槽,也不会切新槽。
- 这适合“整个对象搬动到新位置”的意图,不适合只想移动槽本身的场景。
- `槽孔配对端Face ID` / `槽孔总长度` / `槽孔中心距`
- `槽孔配对端 Face ID` / `槽孔总长度` / `槽孔中心距`
- 对长圆槽或成对半圆槽,程序会在点击修改时自动寻找另一个槽端,避免选择对象时扫描过重。
- 如果自动配对失败,可以先在 `槽孔配对端Face ID` 行填入另一个半圆端的 Face ID 并点击 `设置`
- 如果自动配对失败,可以先在 `槽孔配对端 Face ID` 行填入另一个半圆端的 Face ID 并点击 `设置`
- 修改 `槽孔总长度``槽孔中心距` 时,会保持槽宽不变并重建完整长圆槽体。
- `薄壁厚度``推拉当前面`
- 先选择一个已识别到相对平面的薄壁/壳体平面 Face。
- 在当前选中对象表里修改 `薄壁厚度`,并把 `影响范围` 设为 `推拉当前面`
- `壳体厚度``拉伸/切除`
- 先选择一个已识别到相对平面的壳体平面 Face。
- 在当前选中对象表里修改 `壳体厚度`,并把 `影响范围` 设为 `拉伸/切除`
- 输入目标厚度后点击该行的 `可修改` 或点击 `参数化建模`
- 程序会移动当前平面区域,让它和相对平面的距离接近目标厚度;相对平面保持不动。
- 如果相对平面重叠率低、厚度变化过大或拉方向不稳定,会提高风险或阻止执行。
- 这一步仍是局部 B-Rep 平面拉近似,不是完整 CAD 壳体/薄壁特征参数编辑。
- `薄壁厚度``调整整个特征`
- 先选择一个已识别到相对平面的薄壁/壳体平面 Face。
- 在当前选中对象表里修改 `薄壁厚度`,并把 `影响范围` 设为 `调整整个特征`
- 程序会沿薄壁厚度方向缩放所属特征或 Solid,让估算厚度接近目标值。
- 这不是推拉当前面;同一对象上的孔距、台阶位置和其它厚度方向尺寸也会跟随变化。
- 如果相对平面重叠率低、厚度变化过大或拉伸/切除方向不稳定,会提高风险或阻止执行。
- 这一步仍是局部 B-Rep 平面拉伸/切除近似,不是完整 CAD 壳体特征参数编辑。
- `壳体厚度``缩放特征`
- 先选择一个已识别到相对平面的壳体平面 Face。
- 在当前选中对象表里修改 `壳体厚度`,并把 `影响范围` 设为 `缩放特征`
- 程序会沿壳体厚度方向缩放所属特征或 Solid,让估算厚度接近目标值。
- 这不是拉伸/切除;同一对象上的孔距、台阶位置和其它厚度方向尺寸也会跟随变化。
- `凸台直径` + `调整圆柱凸台直径`
- 先选择一个完整圆柱凸台 Face,建议从 `可编辑对象` 中点击 `调整圆柱凸台直径` 行进入。
- 只有接近完整圆柱的 `boss/outer-round candidate` 会放行;局部外圆角、圆角面、孔/槽和未明确圆柱面会被阻止。
@@ -771,36 +778,36 @@ Face 阶段的当前验收口径:
- 目标直径小于当前直径时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建。
- 点击后会显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。
- 这不是 CAD 历史里的“修改凸台特征参数”,而是基于当前 B-Rep 的受限几何布尔编辑。
- `直径` / `半径``调整整个特征`
- `直径` / `半径``缩放特征`
- 先选择一个圆柱凸台 Face,然后在当前选中对象表里修改这一行。
-`影响范围` 设为 `调整整个特征`
-`影响范围` 设为 `缩放特征`
- 它会按目标直径或半径比例均匀缩放所属特征或 Solid,不走凸台包络移除/重建路线。
- 这会连带影响所属对象上的高度、厚度和其它尺寸,需要确认这正是你的建模意图。
- `凸台高度`
- 先选择一个完整圆柱凸台 Face。
- 在当前选中对象表里修改 `高度`,并把 `影响范围` 设为 `拉端盖`,点击该行的 `可修改` 或点击 `参数化建模`
- 程序会尝试找到凸台外端盖 Face,并通过拉端盖改变高度。
- `高度``调整整个特征`
- 在当前选中对象表里修改 `高度`,并把 `影响范围` 设为 `拉伸/切除端盖`,点击该行的 `可修改` 或点击 `参数化建模`
- 程序会尝试找到凸台外端盖 Face,并通过拉伸/切除端盖改变高度。
- `高度``缩放特征`
- 先选择一个完整圆柱凸台或普通完整圆柱 Face。
- 在当前选中对象表里修改 `高度`,并把 `影响范围` 设为 `调整整个特征`
- 在当前选中对象表里修改 `高度`,并把 `影响范围` 设为 `缩放特征`
- 程序会沿圆柱轴向缩放所属特征或 Solid,让圆柱高度接近目标值。
- 这不是端盖拉;同一对象上的孔距、台阶位置和其它轴向尺寸也会跟随变化。
- 这不是端盖拉伸/切除;同一对象上的孔距、台阶位置和其它轴向尺寸也会跟随变化。
- `轴心`
- 先选择一个完整圆柱凸台 Face。
- 在当前选中对象表里修改 `轴心`,并把 `影响范围` 设为 `移动凸台`,点击该行的 `可修改` 或点击 `参数化建模`
- 程序会切掉旧凸台包络,再按原直径和原高度范围在目标轴心位置补出新凸台;目标位置离基体太远时风险会升高。
- `轴心``移动整个特征`
- `轴心``移动特征`
- 同样输入目标 X, Y, Z 坐标。
-`影响范围` 设为 `移动整个特征`
-`影响范围` 设为 `移动特征`
- 程序会整体平移凸台所属特征或 Solid;不会移除旧凸台,也不会补新凸台。
- 这适合移动整个所属对象,不适合只改凸台相对基体的位置。
- `轴心``移动孔`
- 先选择一个完整圆柱孔 Face。
- 在当前选中对象表里修改目标 X, Y, Z 坐标,并把 `影响范围` 设为 `移动孔`
- 程序会先填补旧孔,再按同直径在目标轴心位置切出新孔。
- `轴心``移动整个特征`
- `轴心``移动特征`
- 同样输入目标 X, Y, Z 坐标。
-`影响范围` 设为 `移动整个特征`
-`影响范围` 设为 `移动特征`
- 程序会整体平移孔所属特征或 Solid;不会填旧孔,也不会切新孔。
- 这适合移动整个所属对象,不适合只改孔相对基体的位置。
- `封堵圆柱孔`
@@ -820,10 +827,10 @@ Face 阶段的当前验收口径:
- 当前只支持 `hole/groove candidate`、端部类型为 `blind`、并且已经识别到 `疑似底面 Face` 的对象。
- 通孔、封闭内腔、圆角、凸柱和未明确圆柱面会被阻止,避免误切。
- 这个深度来自圆柱面 V 参数范围和端部 inside / outside 采样,是几何估算,不是 CAD 历史参数。
- 操作历史会记录当前深度、目标深度、深度变化比例、底面Face、开口端Face和工具参数。
- `盲孔/盲槽深度``调整整个特征`
- 先选择一个已经识别出底面的盲孔或盲槽圆柱面;自动底面不准时,可以先填写 `底面Face`
- 在当前选中对象表里修改 `盲孔/盲槽深度`,并把 `影响范围` 设为 `调整整个特征`
- 操作历史会记录当前深度、目标深度、深度变化比例、底面 Face、开口端 Face和工具参数。
- `盲孔/盲槽深度``缩放特征`
- 先选择一个已经识别出底面的盲孔或盲槽圆柱面;自动底面不准时,可以先填写 `底面 Face`
- 在当前选中对象表里修改 `盲孔/盲槽深度`,并把 `影响范围` 设为 `缩放特征`
- 程序会沿孔/槽轴向缩放所属特征或 Solid,让深度接近目标值。
- 这不是加深切削,也不是变浅补料;同一对象上的壁厚、孔距和其它轴向尺寸会跟随变化。
- `圆角半径` + `给Edge添加圆角`
@@ -843,9 +850,9 @@ Face 阶段的当前验收口径:
- 通过检查后,会显示蓝色半透明预览,表示将尝试移除并重建的已有圆角面。
- 真实编辑会先用 OCCT defeaturing 移除已有圆角面,再从恢复出的候选锐边中寻找可重新倒圆的边并调用 OCCT 倒圆 API。
- 这是 B-Rep 几何重建,不是 CAD 历史圆角参数编辑;复杂 blend、支撑面不明确或恢复锐边失败时会失败并回滚。
- `圆角半径` / `圆角弧长``调整整个特征`
- `圆角半径` / `圆角弧长``缩放特征`
- 先选择一个已有圆角/倒圆候选 Face。
- 在当前选中对象表里修改 `圆角半径``圆角弧长`,并把 `影响范围` 设为 `调整整个特征`
- 在当前选中对象表里修改 `圆角半径``圆角弧长`,并把 `影响范围` 设为 `缩放特征`
- 程序会按目标圆角半径或圆弧长度换算出的比例整体缩放所属特征或 Solid,不会移除旧圆角再重建。
- 这会连带改变同一对象上的其它尺寸。
- `倒角距离` + `给Edge添加倒角`
@@ -869,11 +876,11 @@ Face 阶段的当前验收口径:
- 如果选中的是直线Edge,也可以在当前选中对象表中直接修改 `起点``中心``终点`,格式是 `X, Y, Z`,然后点击该行的 `可修改` 按钮。
- 修改 `中心` 会保持这条直线Edge长度不变,两个端点一起移动;修改起点或终点则只移动对应端点。
-`assets/models/cube_10mm.step` 这类全平面多面体,选择 `只变当前Edge` 时会只移动被选Edge的端点并重建周边平面。这时正方体不会变成长方体;相邻面会跟着被移动的边自然变成梯形,必要时非共面的面会拆成三角面。
- 对同一个正方体,选择 `移动端面` 时会把改边长转成端面拉,相关平行边会一起变化,结果更像从正方体变成长方体。
- 对同一个正方体,选择 `移动端面` 时会把改边长转成端面拉伸/切除,相关平行边会一起变化,结果更像从正方体变成长方体。
- 如果确认窗口里的策略是 `local-edge-only-deform`,说明走的就是“只变这条边”的局部形变路径。
- 如果确认窗口里的策略是 `local-edge-endpoint-deform`,说明走的是“移动起点/终点坐标”的局部形变路径。
- 如果确认窗口里的策略是 `local-edge-center-deform`,说明走的是“移动整条Edge中心”的局部形变路径。
- 如果模型不是简单全平面多面体,直线Edge会继续尝试端面拉后备路径,并计算需要移动的端面Face和拉距离。
- 如果模型不是简单全平面多面体,直线Edge会继续尝试端面拉伸/切除后备路径,并计算需要移动的端面Face和拉伸/切除距离。
- 圆形/圆弧Edge会优先尝试复用相邻圆柱面的孔/槽或凸台直径编辑,并在确认窗口显示换算出来的目标直径。
- 如果圆边没有可复用圆柱面,但能读取圆心和轴线,会走 `围绕圆边轴线径向缩放` 后备策略,尽量不改变轴向厚度/高度;执行后部分解析几何可能变成 B-spline。
- 椭圆边、平面 B-spline / Bezier 边会尝试走 `围绕平面曲线法向径向缩放`;确认窗口会显示平面采样偏差,偏差不稳定时会回退到更保守的缩放 fallback。
@@ -958,26 +965,28 @@ pythonocc-step-editor/
verify_ellipse_edge_resize.py # 临时生成椭圆 STEP 并验证椭圆主半径/小半径单轴缩放
verify_edge_length_resize.py # 验证立方体 Edge 长度局部形变基线
verify_edge_round_chamfer.py # 临时生成盒子 STEP 并验证 Edge 圆角/倒角/已有圆角半径修改
verify_face_coplanar_push_pull.py # 验证共面碎面会作为一个 Face 区域一起推拉
verify_face_complex_boundary_guard.py # 验证带内孔平面 Face 会禁用只改当前面的局部变形,但保留向外推拉和向内切削
verify_edge_isolated_edit.py # 通过 isolated worker 真实验证 Edge 一级编辑隔离执行
verify_face_coplanar_push_pull.py # 验证共面碎面会作为一个 Face 区域一起拉伸/切除
verify_face_complex_boundary_guard.py # 验证带内孔平面 Face 会禁用局部重建的局部变形,但保留向外拉伸和向内切削
verify_face_edit_suite.py # 一键运行 Face 专项修改验证
verify_face_extreme_target_guards.py # 验证 Face 面积、面宽/面高、面偏移和薄壁厚度不能输入过大或过小的极端目标值
verify_face_invalid_target_guards.py # 验证 Face 面积、面宽/面高 和薄壁厚度不能输入非数字、0 或负数
verify_large_stepped_cap_push_pull.py # 验证大 STEP 里的同轴阶梯环形端盖推拉走局部 shell 重建而不是 180s 布尔慢路径
verify_face_extreme_target_guards.py # 验证 Face 面积、U向尺寸/V向尺寸、偏移曲面和壳体厚度不能输入过大或过小的极端目标值
verify_face_freeform_guard.py # 验证 B-spline 等自由曲面 Face 只显示只读限制说明,不暴露伪参数化编辑入口
verify_face_invalid_target_guards.py # 验证 Face 面积、U向尺寸/V向尺寸 和壳体厚度不能输入非数字、0 或负数
verify_large_stepped_cap_push_pull.py # 验证大 STEP 里的同轴阶梯环形端盖拉伸/切除走局部 shell 重建而不是 180s 布尔慢路径
verify_face_logical_selection_retention.py # 验证 Face 修改后逻辑 ID 仍指向被编辑的新面
verify_face_mixed_surface_guard.py # 验证圆柱端盖这类平面 Face 会禁用局部变形,但保留
verify_face_nonrectangular_local_edit.py # 验证三角平面 Face 的局部/整体中心、面积、面宽/面高、面偏移和推拉
verify_face_mixed_surface_guard.py # 验证圆柱端盖这类平面 Face 会禁用局部变形,但保留拉伸/切除
verify_face_nonrectangular_local_edit.py # 验证三角平面 Face 的局部/整体中心、面积、U向尺寸/V向尺寸、偏移曲面和拉伸/切除
verify_face_noop_guards.py # 验证 Face 目标值不变时会被阻止
verify_first_level_fact_graph.py # 验证平面、孔、槽会输出统一一级拓扑事实包,且编辑计划会携带这份事实
verify_feature_recognition_summary.py # 验证平面、孔、槽候选会输出统一识别摘要、置信度、风险和证据
verify_face_property_readback.py # 验证 Face 编辑完成后属性表当前值和目标值会刷新到新几何
verify_face_push_pull_limits.py # 验证 Face 向内推拉不会允许切穿实体厚度
verify_face_push_pull_limits.py # 验证 Face 向内切除不会允许切穿实体厚度
verify_face_ui_isolation_contract.py # 验证 Face UI 入口和 isolated worker 的隔离操作列表一致
verify_face_rectangular_axes.py # 验证矩形 Face 的面宽 和面高 方向不会混淆
verify_face_rectangular_axes.py # 验证矩形 Face 的U向尺寸 和V向尺寸 方向不会混淆
verify_face_resize_semantics.py # 验证 Face 当前面/整体尺寸编辑语义
verify_hole_resize.py # 临时生成通孔/盲孔 STEP 并验证孔径/孔轴心/盲孔深度修改
verify_property_editor_specs.py # 验证属性表不会把通用 Face 编辑混入孔/槽/凸台/圆角/解析曲面特征
verify_shell_thickness_resize.py # 临时生成薄板 STEP 并验证薄壁厚度局部/整体修改
verify_shell_thickness_resize.py # 临时生成薄板 STEP 并验证壳体厚度局部/整体修改
verify_slot_resize.py # 临时生成槽 STEP 并验证槽宽/槽深/弧长/弧角/半圆槽轴心/长圆槽轴心/总长度/中心距修改
verify_cylindrical_first_level_topology.py # 临时生成孔/槽 STEP 并验证圆柱孔、盲孔、半圆槽、长圆槽的一级共边拓扑字段
step_editor/
@@ -990,7 +999,7 @@ pythonocc-step-editor/
info_panel.py # 属性面板、信息树和复制信息逻辑
model.py # StepModel 核心状态、拓扑索引、基础属性和 mixin 组合
model_types.py # PartNode、TopologyStats 等模型数据结构
operations.py # 拉、孔径、孔深、圆角、倒角、边长等实际编辑操作
operations.py # 拉伸/切除、孔径、孔深、圆角、倒角、边长等实际编辑操作
polydata.py # OCC Shape / Edge / Face 到 VTK polydata 的显示数据生成
records.py # 操作历史记录数据结构
step_io.py # STEP/XCAF 读取、产品名解析和 STEP 写出
@@ -1049,6 +1058,7 @@ python scripts\generate_cube_step.py
python scripts\verify_face_edit_suite.py
python scripts\verify_isolated_face_edit.py
python scripts\verify_edge_edit_suite.py
python scripts\verify_edge_isolated_edit.py
python scripts\verify_shell_thickness_resize.py
python scripts\verify_analytic_surface_resize.py
python scripts\verify_slot_resize.py
@@ -1081,30 +1091,36 @@ git diff --check
- `scripts/verify_edge_coordinate_edit.py`:加载 `cube_10mm.step`,分别验证直线 Edge 起点、中心和终点坐标修改,确认局部重建后仍能找到目标端点位置的直线 Edge,并保持单 Solid。
- `scripts/verify_edge_length_resize.py`:加载 `cube_10mm.step`,把一条 10mm 直线 Edge 改到 15mm,并断言策略是 `local-edge-only-deform`、目标边长误差在容差内。
- `scripts/verify_edge_round_chamfer.py`:临时生成盒子和已有圆角盒子 STEP,验证直线 Edge 新增圆角、对称倒角、不等距倒角、距离+角度倒角,以及已有圆角半径修改。
- `scripts/verify_face_edit_suite.py`集中运行当前 Face 阶段验证,覆盖 Face 一级共享边拓扑、面宽/面高、矩形 Face 两个面内方向区分、面积、中心、面偏移、共面碎面一起推拉/切削、向内推拉切穿保护、带内孔平面 Face 的局部变形保护、圆柱端盖这类含曲面 Solid 的平面 Face 局部变形保护、大 STEP 同轴阶梯环形端盖局部 shell 推拉、非矩形平面 Face 局部编辑、编辑后属性值回读、修改后逻辑 Face ID 仍指向被编辑的新面、薄壁厚度增厚/减厚、目标值等于当前值时必须阻止修改,以及面偏移/面积/面宽/面高/薄壁厚度目标值为非数字、0、负数或极端过大/过小时必须阻止修改
- `scripts/verify_edge_isolated_edit.py`通过 `python -m step_editor.isolated_edit_worker` 的真实 worker 入口验证 Edge 长度、直线 Edge 起点/中心移动、圆形 Edge 相邻轴心移动、椭圆 Edge 主半径、直线 Edge 新增圆角、三类倒角和已有圆角半径修改可以在隔离执行通道里完成,输出 STEP 能重新加载并保持目标几何语义
- `scripts/verify_face_edit_suite.py`:集中运行当前 Face 阶段验证,覆盖 Face 一级共享边拓扑、U向尺寸/V向尺寸、矩形 Face 两个面内方向区分、面积、中心、偏移曲面、共面碎面一起拉伸/切除/切削、向内切除切穿保护、带内孔平面 Face 的局部变形保护、圆柱端盖这类含曲面 Solid 的平面 Face 局部变形保护、B-spline 自由曲面只读守卫、大 STEP 同轴阶梯环形端盖局部 shell 拉伸/切除、非矩形平面 Face 局部编辑、编辑后属性值回读、修改后逻辑 Face ID 仍指向被编辑的新面、壳体厚度增厚/减厚、目标值等于当前值时必须阻止修改,以及偏移曲面/面积/U向尺寸/V向尺寸/壳体厚度目标值为非数字、0、负数或极端过大/过小时必须阻止修改。
- `scripts/verify_face_edit_integrity_guards.py`:验证 Face 编辑结果不能丢 Solid、不能新增无效 B-Rep、不能只达到目标数值却破坏一级拓扑;若目标检查、一级拓扑计数或一级事实图校验失败,模型层会恢复编辑前快照。
- `scripts/verify_face_first_level_topology.py`:加载 `cube_10mm.step`,验证正方体上表面 Face 的一级关系只包含当前 Face、4 条边界 Edge、4 个边界 Vertex 和 4 个共享边相邻侧面;局部移动上表面后,目标 Face 的边界 Vertex 与共享边 Edge 会到达新高度,4 个一级侧面会跟随重建,二级底面保持不动。
- `scripts/verify_face_coplanar_push_pull.py`:临时生成顶部被拆成两片共面 Face 的单 Solid,验证选中其中一片时会把整片共面区域作为一个 profile 推拉,并确认推拉后原逻辑 Face ID 会定位到新生成的整片顶面。
- `scripts/verify_face_complex_boundary_guard.py`:临时生成带通孔板件,验证带内孔的平面 Face 会报告边界环数量,并禁用 `只改当前面` 的局部面积、面宽/面高、中心和偏移计划;同时确认 `推拉当前面` 的向外补料和向内切削都仍可用并保持单 Solid,且同一 Solid 的局部变形可用性判断会复用缓存。
- `scripts/verify_face_mixed_surface_guard.py`:临时生成简单圆柱,验证圆柱端盖这个平面 Face 会因为所属 Solid 含有曲面而禁用 `只改当前面` 的局部面积、中心和偏移计划,属性提示会说明具体原因;同时确认 `推拉当前面` 仍可用并让圆柱高度增加。
- `scripts/verify_face_invalid_target_guards.py`:验证 Face 面积、面宽/面高 和薄壁厚度的局部/整体计划在目标值为非数字、0 或负数时都会返回 blocked,并提示目标值必须是有效正数。
- `scripts/verify_face_extreme_target_guards.py`验证 Face 面积、面宽/面高、面偏移和薄壁厚度的局部/整体计划在目标值会把几何缩到当前 5% 以下、放大到当前 5 倍以上,或把面偏移移动到远超模型尺寸时都会返回 blocked,避免退化面、拉穿相邻几何或长时间无效计算
- `scripts/verify_face_logical_selection_retention.py`:验证 Face 的局部/整体面宽、局部/整体面积、局部/整体中心、局部/整体面偏移和推拉正负方向完成后,原逻辑 Face ID 会排他地指向被编辑后的新 Face,避免 UI 改完参数后选中跑到别的面
- `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `只改当前面``调整整个特征``移动整个特征``推拉当前面` 语义,在中心、面积、面宽/面高 和面偏移场景下都能保持单 Solid
- `scripts/verify_face_resize_semantics.py`加载 `cube_10mm.step`,验证 Face 的面宽/面高、面积、中心坐标和面偏移在 `当前面` 局部编辑、`整体` 所属对象编辑与 `推拉当前面` 之间不会互相混淆
- `scripts/verify_face_noop_guards.py`:验证 Face 的推拉、面偏移、中心、面积、面宽/面高 和薄壁厚度在目标值等于当前值时都会返回 blocked,并给出“不需要修改”的说明
- `scripts/verify_first_level_fact_graph.py`:验证正方体平面 Face、通孔圆柱面和槽/半孔候选都会输出统一一级拓扑事实包,包含主体 Face、边界 Edge/Vertex、共享边一级相邻 Face、角色分组,以及明确不自动传播的二级/三级关系;同时验证 Face 推拉/面积/中心移动、孔径修改和槽宽修改的编辑计划都会携带这份事实
- `scripts/verify_face_coplanar_push_pull.py`:临时生成顶部被拆成两片共面 Face 的单 Solid,验证选中其中一片时会把整片共面区域作为一个 profile 拉伸/切除,并确认拉伸/切除后原逻辑 Face ID 会定位到新生成的整片顶面。
- `scripts/verify_face_complex_boundary_guard.py`:临时生成带通孔板件,验证带内孔的平面 Face 会报告边界环数量,并禁用 `局部重建` 的局部面积、U向尺寸/V向尺寸、中心和偏移曲面计划;同时确认 `拉伸/切除` 的向外补料和向内切削都仍可用并保持单 Solid,且同一 Solid 的局部变形可用性判断会复用缓存。
- `scripts/verify_face_mixed_surface_guard.py`:临时生成简单圆柱,验证圆柱端盖这个平面 Face 会因为所属 Solid 含有曲面而禁用 `局部重建` 的局部面积、中心和偏移曲面计划,属性提示会说明具体原因;同时确认 `拉伸/切除` 仍可用并让圆柱高度增加。
- `scripts/verify_face_freeform_guard.py`临时生成 B-spline 自由曲面 STEP,验证它会被识别为只读受限 Face,识别风险为 blocked;属性表只显示限制说明和只读诊断,不暴露面积、中心、偏移曲面、半径、半角等伪参数化编辑入口
- `scripts/verify_face_invalid_target_guards.py`:验证 Face 面积、U向尺寸/V向尺寸 和壳体厚度的局部/整体计划在目标值为非数字、0 或负数时都会返回 blocked,并提示目标值必须是有效正数
- `scripts/verify_face_extreme_target_guards.py`:验证 Face 面积、U向尺寸/V向尺寸、偏移曲面和壳体厚度的局部/整体计划在目标值会把几何缩到当前 5% 以下、放大到当前 5 倍以上,或把偏移曲面移动到远超模型尺寸时都会返回 blocked,避免退化面、拉穿相邻几何或长时间无效计算
- `scripts/verify_face_logical_selection_retention.py`验证 Face 的局部/整体U向尺寸、局部/整体面积、局部/整体中心、局部/整体偏移曲面和拉伸/切除正负方向完成后,原逻辑 Face ID 会排他地指向被编辑后的新 Face,避免 UI 改完参数后选中跑到别的面
- `scripts/verify_selection_identity_ui.py`:验证编辑后当前拓扑 Face ID 改变时,Face / 特征标题会显示 `逻辑 Face ID(拓扑 Face ID`,复制 ID 会优先复制可再次选择的逻辑 Face ID,避免把用户带到重建后的临时拓扑编号上
- `scripts/verify_face_feature_parameter_consistency.py`:加载默认模型 Face 594,验证轻量 `当前特征` 选择在 full feature 缓存前后都显示同一组基础 Face 参数,避免先改其它对象或触发深层识别后,同一个 Face 的属性列表突然缩水或膨胀
- `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `局部重建``缩放特征``移动特征``拉伸/切除` 语义,在中心、面积、U向尺寸/V向尺寸 和偏移曲面场景下都能保持单 Solid。
- `scripts/verify_face_resize_semantics.py`:加载 `cube_10mm.step`,验证 Face 的U向尺寸/V向尺寸、面积、中心坐标和偏移曲面在 `当前面` 局部编辑、`整体` 所属对象编辑与 `拉伸/切除` 之间不会互相混淆。
- `scripts/verify_face_noop_guards.py`:验证 Face 的拉伸/切除、偏移曲面、中心、面积、U向尺寸/V向尺寸 和壳体厚度在目标值等于当前值时都会返回 blocked,并给出“不需要修改”的说明。
- `scripts/verify_first_level_fact_graph.py`:验证正方体平面 Face、通孔圆柱面和槽/半孔候选都会输出统一一级拓扑事实包,包含主体 Face、边界 Edge/Vertex、共享边一级相邻 Face、角色分组,以及明确不自动传播的二级/三级关系;同时验证 Face 拉伸/切除/面积/中心移动、孔径修改和槽宽修改的编辑计划都会携带这份事实。
- `scripts/verify_feature_recognition_summary.py`:验证平面 Face、带内孔平面 Face、通孔圆柱面和槽/半孔候选都会输出统一的 `识别摘要`、候选类型、置信度、风险、证据项、`当前可改``当前受限修改`;这些摘要只使用已有识别结果生成,不额外触发深层几何扫描。
- `scripts/verify_face_property_readback.py`:验证 Face 的面宽、面积、中心和偏移编辑完成后,当前选中对象表里的当前值和目标值会重新读到新几何;特别覆盖 `偏移``只改当前面``移动整个特征``推拉当前面` 三种语义。
- `scripts/verify_face_push_pull_limits.py`:验证平面 Face 向内推拉时会估算当前面背后的材料厚度;浅切削允许执行,接近切穿会标为高风险,达到或超过材料厚度会直接 blocked;向外一次性拉较大时会标为高风险并通过隔离子进程执行,而不是按绝对距离直接拒绝。
- `scripts/verify_face_ui_isolation_contract.py`:验证 Face 阶段属性表入口使用的隔离操作集合、`WindowActionMixin._isolation_for_plan()` 的 UI 隔离列表,以及 `isolated_edit_worker` 真正支持的 worker 操作完全一致;脚本还会反向追踪属性表暴露的 Face 修改动作是否最终进入隔离 worker,防止后续新增 Face 参数行时忘记接入后台隔离执行,导致 UI 卡死或主进程被危险 OCCT 计算拖崩。
- `scripts/verify_face_rectangular_axes.py`:临时生成 20 x 10 x 6 的矩形盒子,验证 10 x 20 的矩形 Face 在 `面宽``面高` 两个方向上可以分别做 `只改当前面``调整整个特征`,另一个方向不会被误改,属性表也会读回新值。
- `scripts/verify_cylindrical_height_resize.py`:临时生成简单圆柱、同轴空心圆柱、多孔圆柱和顶端带缺口圆柱,验证选中圆柱侧面修改高度时会走端盖拉并校验目标高度、半径和一级拓扑;同时覆盖 isolated worker 子进程路径,确认这些高频 Face 修改不会把主界面拖进危险计算;`调整整个特征` 的轴向缩放会按轴向跨度校验结果。
- `scripts/verify_large_stepped_cap_push_pull.py`:加载 `assets/models/geom_extract.step`,自动找到 y=-39 的同轴阶梯环形端盖和 y=-57.5 的多内孔大平面端盖,分别验证 `-39 -> 50``local-shell-rebuild``57.5 -> 92``boundary-shell-rebuild`,并验证多内孔大端面向内推拉会快速阻止且说明二级关系原因;成功路径保持单 Solid,原逻辑 Face ID 会继续指向移动后的新端面,一级边界 Edge/Vertex、共享边相邻 Face 和内孔线圈数量不能丢,并同时覆盖隔离子进程路径,避免 UI 操作退回 180s 布尔/全模型结果扫描慢路径。
- `scripts/verify_face_property_readback.py`:验证 Face 的U向尺寸、面积、中心和偏移曲面编辑完成后,当前选中对象表里的当前值和目标值会重新读到新几何;特别覆盖 `偏移曲面``局部重建``移动特征``拉伸/切除` 三种语义。
- `scripts/verify_face_push_pull_limits.py`:验证平面 Face 向内切除时会估算当前面背后的材料厚度;浅范围切除允许执行,接近切穿会标为高风险,达到或超过材料厚度会直接 blocked;向外一次性拉伸/切除较大时会标为高风险并通过隔离子进程执行,而不是按绝对距离直接拒绝。
- `scripts/verify_face_ui_isolation_contract.py`:验证 Face、孔槽和 Edge 阶段使用的隔离操作集合、`WindowActionMixin._isolation_for_plan()` 的 UI 隔离列表,以及 `isolated_edit_worker` 真正支持的 worker 操作完全一致;脚本还会反向追踪属性表暴露的 Face 修改动作、孔槽按钮和 Edge 按钮是否最终进入隔离 worker,防止后续新增参数行时忘记接入后台隔离执行,导致 UI 卡死或主进程被危险 OCCT 计算拖崩。没有 pyocc 时可以先运行 `python scripts\verify_face_ui_isolation_contract.py --static-only` 做 AST 级契约检查。
- `scripts/verify_face_rectangular_axes.py`:临时生成 20 x 10 x 6 的矩形盒子,验证 10 x 20 的矩形 Face 在 `U向尺寸``V向尺寸` 两个方向上可以分别做 `局部重建``缩放特征`,另一个方向不会被误改,属性表也会读回新值。
- `scripts/verify_cylindrical_height_resize.py`:临时生成简单圆柱、同轴空心圆柱、多孔圆柱和顶端带缺口圆柱,验证选中圆柱侧面修改高度时会走端盖拉伸/切除并校验目标高度、半径和一级拓扑;同时覆盖 isolated worker 子进程路径,确认这些高频 Face 修改不会把主界面拖进危险计算;`缩放特征` 的轴向缩放会按轴向跨度校验结果。
- `scripts/verify_large_stepped_cap_push_pull.py`:加载 `assets/models/geom_extract.step`,自动找到 y=-39 的同轴阶梯环形端盖和 y=-57.5 的多内孔大平面端盖,分别验证 `-39 -> 50``local-shell-rebuild``57.5 -> 92``boundary-shell-rebuild`,并验证多内孔大端面浅范围向内收缩也走 `boundary-shell-rebuild`,深向内切除会快速阻止且说明材料厚度或二级关系原因;成功路径保持单 Solid,原逻辑 Face ID 会继续指向移动后的新端面,一级边界 Edge/Vertex、共享边相邻 Face 和内孔线圈数量不能丢,并同时覆盖隔离子进程路径,避免 UI 操作退回 180s 布尔/全模型结果扫描慢路径。
- `scripts/verify_hole_resize.py`:临时生成通孔和盲孔 STEP,验证孔径扩大、孔径缩小、通孔轴心移动、完整通孔封堵、盲孔加深和盲孔变浅的局部重建链路;盲孔/盲槽深度修改现在会在操作内部确认目标深度和一级邻域,失败会回滚。
- `scripts/verify_isolated_face_edit.py`验证 high-risk Face 的面偏移、面积、面宽/面高、中心和薄壁厚度局部/整体修改可以在隔离子进程里执行,子进程导出结果 STEP 后主流程仍能加载出单 Solid;同时验证复杂大 STEP 多内孔端面的 UI 操作计划会延后完整几何计划,避免按钮点击阶段先卡住主界面
- `scripts/verify_property_editor_specs.py`:轻量验证当前选中对象表的规格生成逻辑,确保平面 Face 显示通用 Face 编辑,而孔、槽、凸台、已有圆角、圆锥、球面和环面不会混入通用 Face 的面积、中心、面宽/面高 或面偏移编辑;同时验证目标值等于当前值时不会误判为需要修改,带内孔 Face 禁用 `只改当前面` 时会把具体原因写进提示,并确认 UI 不再单独暴露容易混淆的 额外的 `移动量`
- `scripts/verify_shell_thickness_resize.py`:临时生成薄板 STEP,验证薄壁厚度局部推拉和整体调整两种语义,目标厚度可以大于或小于当前厚度;同时确认修改后原逻辑 Face ID 仍指向被编辑后的平面 Face,简单全平面薄板不会被整体调整变成 B-spline 曲面
- `scripts/verify_hole_slot_isolated_edit.py`临时生成通孔、盲孔、半圆槽和长圆槽 STEP,通过 `python -m step_editor.isolated_edit_worker` 的真实 worker 入口验证孔径、孔径缩放特征、孔轴心、孔封堵、盲孔深度、槽宽、槽宽缩放特征、槽深、弧长、弧角、槽轴心、长圆槽总长度和中心距可以在隔离执行通道里完成,输出 STEP 能重新加载并达到目标值;同时验证会破坏盲孔圆柱语义的整体缩放会被干净拒绝并回滚。窗口侧会额外验证孔径、孔轴心、槽宽、槽轴心、盲孔/盲槽深度、长圆槽总长度和长圆槽轴心隔离任务不会被误套用通用 Face 参数校验,并会把原逻辑 Face ID 继续绑定到修改后的孔壁、槽壁或长圆槽端部 Face
- `scripts/verify_isolated_face_edit.py`:验证 high-risk Face 的偏移曲面、面积、U向尺寸/V向尺寸、中心和壳体厚度局部/整体修改可以在隔离子进程里执行,子进程导出结果 STEP 后主流程仍能加载出单 Solid;同时验证复杂大 STEP 多内孔端面的 UI 操作计划会延后完整几何计划,避免按钮点击阶段先卡住主界面
- `scripts/verify_property_editor_specs.py`:轻量验证当前选中对象表的规格生成逻辑,确保平面 Face 显示通用 Face 编辑,而孔、槽、凸台、已有圆角、圆锥、球面和环面不会混入通用 Face 的面积、中心、U向尺寸/V向尺寸 或偏移曲面编辑;同时验证目标值等于当前值时不会误判为需要修改,带内孔 Face 禁用 `局部重建` 时会把具体原因写进提示,并确认 UI 不再单独暴露容易混淆的 额外的 `移动量`
- `scripts/verify_shell_thickness_resize.py`:临时生成薄板 STEP,验证壳体厚度局部拉伸/切除和整体调整两种语义,目标厚度可以大于或小于当前厚度;同时确认修改后原逻辑 Face ID 仍指向被编辑后的平面 Face,简单全平面薄板不会被整体调整变成 B-spline 曲面。
- `scripts/verify_slot_resize.py`:临时生成半圆槽和长圆槽 STEP,验证槽宽、槽深、弧长、弧角、半圆槽局部轴心、长圆槽整槽轴心、槽孔总长度和两端中心距的局部重建链路。
- `scripts/verify_cylindrical_first_level_topology.py`:临时生成通孔、盲孔、半圆槽和长圆槽 STEP,验证孔/槽类圆柱特征的修改计划会暴露一级共边拓扑关系,包括圆柱侧壁、边界 Edge、边界 Vertex、直接相邻 Face、底面/开口面/槽边界面,并明确二级、三级关系暂不自动传播。
@@ -1115,7 +1131,7 @@ git diff --check
- `step_editor/window_state.py`:选择状态、按钮启用/禁用、撤销/重做和历史定位。
- `step_editor/model.py``StepModel` 核心状态和 mixin 组合。
- `step_editor/features.py`:可编辑对象和圆柱面/孔/槽/凸台/圆角等识别计划。
- `step_editor/operations.py`拉、孔径、孔深、圆角、倒角、薄壁、边长等真实几何编辑。
- `step_editor/operations.py`:拉伸/切除、孔径、孔深、圆角、倒角、壳体、边长等真实几何编辑。
- `step_editor/export.py`STEP 导出和质量检查。
- `step_editor/polydata.py`OCCT shape/face/edge 到 VTK polydata。
- `step_editor/widgets.py`:Qt 自定义小控件,例如不误吃滚轮的下拉框。
@@ -1133,14 +1149,14 @@ git diff --check
- STEP 后台加载:首次先显示粗略网格,再异步切到精细显示,并可隐藏同域内部边。
- 选择:支持零件、Solid、Face、Edge、特征。鼠标悬停红色预高亮,选中黄色高亮;悬停和普通点选只做轻量拾取,不触发完整特征识别。
- 同域面:同域合并仍用于编辑计划、扫描候选、历史定位和必要的模型修复;普通交互不再实时扫描整片同域区域。
- 编辑:偏移、面积、面宽/面高、中心、孔径/半径、孔/槽轴心、槽宽、槽深、弧长、弧角、开口角、总长度和中心距、盲孔/盲槽深度、凸台直径/半径/高度/轴心、普通完整圆柱高度、已有圆角半径/圆弧长度、圆锥参考半径/直径/半角、球面半径/直径(整体)、环面主/小半径或直径(整体)、Edge圆角、Edge倒角、圆Edge半径/直径、椭圆Edge主半径/小半径、薄壁厚度、修改Edge长度、直线Edge起点/中心/终点坐标、零件/Solid 平移和旋转。
- 编辑语义:Face 的偏移、面积、面宽/面高 和中心通过 `影响范围` 选择“推拉当前面”“只改当前面”“移动整个特征”或“调整整个特征”;圆锥 Face 会优先区分简单圆锥解析重建、嵌入式锥孔局部重切和复杂兜底整体缩放;孔/槽、凸台、盲孔/盲槽、薄壁、圆角和 Edge 长度也会通过 `影响范围` 或计划策略说明本次修改会局部重建、拉、移动还是整体缩放。
- 编辑:偏移曲面、面积、U向尺寸/V向尺寸、中心、孔径/半径、孔/槽轴心、槽宽、槽深、弧长、弧角、开口角、总长度和中心距、盲孔/盲槽深度、凸台直径/半径/高度/轴心、普通完整圆柱高度、已有圆角半径/圆弧长度、圆锥参考半径/直径/半角、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge圆角、Edge倒角、圆Edge半径/直径、椭圆Edge主半径/小半径、壳体厚度、修改Edge长度、直线Edge起点/中心/终点坐标、零件/Solid 平移和旋转。
- 编辑语义:Face 的偏移曲面、面积、U向尺寸/V向尺寸 和中心通过 `影响范围` 选择“拉伸/切除”“局部重建”“移动特征”或“缩放特征”;圆锥 Face 会优先区分简单圆锥解析重建、嵌入式锥孔局部重切和复杂兜底整体缩放;孔/槽、凸台、盲孔/盲槽、壳体、圆角和 Edge 长度也会通过 `影响范围` 或计划策略说明本次修改会局部重建、拉伸/切除、移动还是整体缩放。
- 恢复:编辑前快照、后台执行、失败回滚、撤销/重做、差异预览、热力图和差异报告。
- 导出:完整模型、零件、Solid、Face、特征区域和 Edge,导出前做基础质量检查。
### 编辑语义
所有几何参数修改都必须先明确语义、影响范围和风险。修改 Face、孔、槽、圆角、凸台、Solid 或 Edge 时,都不能只看“目标值能不能算出来”;要区分面推拉、切削补料、特征重建、局部形变、端面移动、对象平移、整体缩放等不同策略,并在确认窗口、属性提示或操作历史里尽量说明本次采用的策略。Edge 的 `长度` 行已经通过 `影响范围` 提供显式改法选择;Face 当前把 `偏移``面积``面宽``面高``中心` 显示为单行属性,并通过 `影响范围` 下拉框表达“推拉当前面”“只改当前面”“移动整个特征”或“调整整个特征”;孔/槽直径和半径也通过 `影响范围` 区分“局部重切/重建孔壁”和“缩放所属对象”;凸台直径、半径、高度和轴心也通过 `影响范围` 区分“局部重建/端盖拉/移动凸台”和“整体缩放/移动所属对象”;普通完整圆柱的直径、半径和高度也通过 `影响范围` 区分“兜底重切/端盖拉”和“整体缩放所属对象”;槽宽、槽深和弧长也通过 `影响范围` 区分“局部重建槽”和“缩放所属对象”;盲孔/盲槽深度也通过 `影响范围` 区分“局部切削/补料”和“轴向缩放所属对象”;薄壁厚度也通过 `影响范围` 区分“局部推拉当前薄壁平面”和“沿厚度方向缩放所属对象”;已有圆角半径/弧长也通过 `影响范围` 区分“移除并重建圆角”和“整体缩放所属对象”;孔、槽的轴心坐标已经开始区分“移动/重建局部特征”和“平移所属对象”;孔、槽、凸台、盲孔/盲槽深度、薄壁厚度现在会显示并记录当前采用的编辑方式,后续还要继续把更多多语义场景做成用户可选策略。
所有几何参数修改都必须先明确语义、影响范围和风险。修改 Face、孔、槽、圆角、凸台、Solid 或 Edge 时,都不能只看“目标值能不能算出来”;要区分 Face 拉伸/切除、切削补料、特征重建、局部形变、端面移动、对象平移、整体缩放等不同策略,并在确认窗口、属性提示或操作历史里尽量说明本次采用的策略。Edge 的 `长度` 行已经通过 `影响范围` 提供显式改法选择;Face 当前把 `偏移曲面``面积``U向尺寸``V向尺寸``中心` 显示为单行属性,并通过 `影响范围` 下拉框表达“拉伸/切除”“局部重建”“移动特征”或“缩放特征”;孔/槽直径和半径也通过 `影响范围` 区分“局部重切/重建孔壁”和“缩放所属对象”;凸台直径、半径、高度和轴心也通过 `影响范围` 区分“局部重建/端盖拉伸/切除/移动凸台”和“整体缩放/移动所属对象”;普通完整圆柱的直径、半径和高度也通过 `影响范围` 区分“兜底重切/端盖拉伸/切除”和“整体缩放所属对象”;槽宽、槽深和弧长也通过 `影响范围` 区分“局部重建槽”和“缩放所属对象”;盲孔/盲槽深度也通过 `影响范围` 区分“局部切削/补料”和“轴向缩放所属对象”;壳体厚度也通过 `影响范围` 区分“局部拉伸/切除当前壳体平面”和“沿厚度方向缩放所属对象”;已有圆角半径/弧长也通过 `影响范围` 区分“移除并重建圆角”和“整体缩放所属对象”;孔、槽的轴心坐标已经开始区分“移动/重建局部特征”和“平移所属对象”;孔、槽、凸台、盲孔/盲槽深度、壳体厚度现在会显示并记录当前采用的编辑方式,后续还要继续把更多多语义场景做成用户可选策略。
必须记住:用户不是只要“数值改掉”,而是要选择建模意图。正方体改一条 Edge 只是例子,实际要求覆盖所有对象:用户应该能选择只改当前边、移动端面/保持垂直、固定某些边/点、让哪些相邻几何跟随变化,或整体缩放/重建所属对象。Face、孔、槽、凸台和圆角也要按同样思路设计。当多种结果都合理时,不要让程序暗自决定;要把“改法、约束、影响对象、跟随关系”做成当前选中对象里的策略行、下拉框或清晰按钮,并用普通用户能理解的话解释。
@@ -1150,9 +1166,9 @@ git diff --check
当前支持的长度修改策略:
1. `auto`:自动策略。按局部边形变、端面拉、相邻圆柱直径、几何缩放的顺序寻找可用路径。
1. `auto`:自动策略。按局部边形变、端面拉伸/切除、相邻圆柱直径、几何缩放的顺序寻找可用路径。
2. `local-edge-only-deform`:只变当前Edge。直线Edge位于简单全平面多面体时,只移动被选Edge的端点并重建所属Solid/特征。对立方体测试模型,默认固定起点移动终点;相邻面会自然变成梯形,必要时非共面四边面拆三角面,而不是把整个正方体缩放成长方体。
3. `move-edge-end-plane-by-push-pull`:移动端面/保持垂直。尝试找端点附近、方向匹配的平面端面,把改边长转成端面拉;端面和相关边跟随移动,相邻平面尽量保持垂直,对正方体会更像变成长方体。
3. `move-edge-end-plane-by-push-pull`:移动端面/保持垂直。尝试找端点附近、方向匹配的平面端面,把改边长转成端面拉伸/切除;端面和相关边跟随移动,相邻平面尽量保持垂直,对正方体会更像变成长方体。
4. `resize-adjacent-cylinder-from-circular-edge-length`:圆形/圆弧Edge尝试复用相邻圆柱孔/槽或凸台直径编辑。
5. `scale-owning-shape-from-edge`:缩放所属对象。圆边、椭圆边和可采样为平面的非直线Edge会优先用 `radial-affine` 围绕曲线平面法向做径向缩放;其他无明确局部路径的Edge才按Edge轴向或整体做几何缩放 fallback,风险高,会影响其它尺寸。
@@ -1168,17 +1184,17 @@ Face 局部移动基线验证:在 `assets/models/cube_10mm.step` 上选上表
Face 一级关系基线验证:在 `assets/models/cube_10mm.step` 上选上表面 Face`face_first_level_topology()` 会返回 `topology_relation_depth=1`、4 条边界 Edge、4 个边界 Vertex、4 个共享边相邻 Face,并明确二级/三级关系暂不递归传播。将上表面中心沿 Z 方向从 `10` 移到 `12` 后,4 个一级侧面中心 Z 从 `5` 变为 `6`,二级底面中心 Z 仍为 `0`,说明当前局部 Face 编辑只处理一级共享边拓扑。
Face 偏移基线验证:在 `assets/models/cube_10mm.step` 上选一个平面 Face,将 `偏移` 正向或负向改变 `1mm``只改当前面` 会走 `local-face-plane-offset-deform`移动当前 Face 顶点并重建相邻面;`移动整个特征` 会走 `translate-owning-shape-from-plane-offset`,整体平移所属对象且所有面尺寸保持不变;`推拉当前面` 会走 `push-pull-planar-face`,正向执行加料,负向执行切削。三条路径都会验证目标平面中心位置,但结果形态不同,不能混为一个“偏移”操作。
Face 偏移曲面基线验证:在 `assets/models/cube_10mm.step` 上选一个平面 Face,将 `偏移曲面` 正向或负向改变 `1mm``局部重建` 会走 `local-face-plane-offset-deform`,移动当前 Face 顶点并重建相邻面;`移动特征` 会走 `translate-owning-shape-from-plane-offset`,整体平移所属对象且所有面尺寸保持不变;`拉伸/切除` 会走 `push-pull-planar-face`,正向执行加料,负向执行切削。三条路径都会验证目标平面中心位置,但结果形态不同,不能混为一个“偏移曲面”操作。
Face 局部面积基线验证:在 `assets/models/cube_10mm.step` 上选上表面 Face,将面积从 `100` 改为 `144`,计划策略为 `local-face-area-only-deform`,面内缩放比例为 `1.2`,内存执行成功。执行后仍是 1 个 Solid、6 个 Face、12 条 Edge、8 个 Vertex,说明只缩放了当前 Face 的 4 个顶点并重建相邻平面,不是缩放整个所属对象。
Face 局部面宽/面高基线验证:在 `assets/models/cube_10mm.step` 上选一个 10mm x 10mm 平面 Face,将 `面宽``面高` 的影响范围设为 `只改当前面` 并从 `10` 改为 `15`,计划策略分别为 `local-face-width-only-deform` / `local-face-height-only-deform`,内存执行成功。执行后能检测到目标尺寸的平面 Face,说明它只沿当前 Face 的一个平面内方向缩放顶点,不是整体拉成长方体。
Face 局部U向尺寸/V向尺寸基线验证:在 `assets/models/cube_10mm.step` 上选一个 10mm x 10mm 平面 Face,将 `U向尺寸``V向尺寸` 的影响范围设为 `局部重建` 并从 `10` 改为 `15`,计划策略分别为 `local-face-width-only-deform` / `local-face-height-only-deform`,内存执行成功。执行后能检测到目标尺寸的平面 Face,说明它只沿当前 Face 的一个平面内方向缩放顶点,不是整体拉成长方体。
Face 整体面宽/面高基线验证:在 `assets/models/cube_10mm.step` 上选一个 10mm x 10mm 平面 Face,将 `面宽``面高` 的影响范围设为 `调整整个特征` 并从 `10` 改为 `15`,计划策略分别为 `axis-scale-owning-shape-from-face-width` / `axis-scale-owning-shape-from-face-height`,简单全平面实体优先使用 `planar-rebuild`。内存执行成功后仍是 1 个 Solid、6 个 Face、12 条 Edge、8 个 Vertex,并能检测到多个面积 `150` 的平面 Face,说明所属特征沿该面内方向整体跟随变化,同时避免通用仿射把平面盒子变成样条面。
Face 整体U向尺寸/V向尺寸基线验证:在 `assets/models/cube_10mm.step` 上选一个 10mm x 10mm 平面 Face,将 `U向尺寸``V向尺寸` 的影响范围设为 `缩放特征` 并从 `10` 改为 `15`,计划策略分别为 `axis-scale-owning-shape-from-face-width` / `axis-scale-owning-shape-from-face-height`,简单全平面实体优先使用 `planar-rebuild`。内存执行成功后仍是 1 个 Solid、6 个 Face、12 条 Edge、8 个 Vertex,并能检测到多个面积 `150` 的平面 Face,说明所属特征沿该面内方向整体跟随变化,同时避免通用仿射把平面盒子变成样条面。
Face 面积基线验证:在 `assets/models/cube_10mm.step` 上选一个 10mm x 10mm 平面 Face,将 `面积``100` 改为 `144``只改当前面` 会走 `local-face-area-only-deform`,只把当前 Face 在自身平面内缩放到 12mm x 12mm 并重建相邻面;`调整整个特征` 会走 `uniform-scale-face-area-fallback`,把所属对象整体等比缩放到所有 10mm x 10mm 面都变为约 12mm x 12mm。
Face 面积基线验证:在 `assets/models/cube_10mm.step` 上选一个 10mm x 10mm 平面 Face,将 `面积``100` 改为 `144``局部重建` 会走 `local-face-area-only-deform`,只把当前 Face 在自身平面内缩放到 12mm x 12mm 并重建相邻面;`缩放特征` 会走 `uniform-scale-face-area-fallback`,把所属对象整体等比缩放到所有 10mm x 10mm 面都变为约 12mm x 12mm。
Face 中心基线验证:在 `assets/models/cube_10mm.step` 上选一个平面 Face,将中心按 `(2, 0, 3)` 偏移。`只改当前面` 会走 `local-face-only-deform`,当前 Face 被移到目标中心并重建相邻面,Face 自身尺寸保持约 10mm x 10mm`移动整个特征` 会走 `translate-part`,整个所属特征平移,所有面尺寸保持 10mm x 10mm。
Face 中心基线验证:在 `assets/models/cube_10mm.step` 上选一个平面 Face,将中心按 `(2, 0, 3)` 偏移。`局部重建` 会走 `local-face-only-deform`,当前 Face 被移到目标中心并重建相邻面,Face 自身尺寸保持约 10mm x 10mm`移动特征` 会走 `translate-part`,整个所属特征平移,所有面尺寸保持 10mm x 10mm。
孔/盲孔局部重建基线验证:`scripts/verify_hole_resize.py` 会临时生成一个通孔板件和一个盲孔块体。当前验证通过六条孔修改路线:通孔孔径 `6 -> 8`、通孔孔径 `6 -> 4`、通孔轴心偏移 `2mm`、完整通孔封堵、盲孔深度 `6 -> 8`、盲孔深度 `6 -> 4`。封堵会检查补料后仍是单 Solid、孔候选消失,并且体积接近完整板件;孔径缩小、孔轴心移动、封堵和盲孔变浅都会先补旧孔/旧底部再重切或补料;这些孔内补料 Fuse 必须关闭 OCCT glue,否则可能只生成一个插在孔里的独立圆柱 Solid,而不是和主体真正合并。
@@ -1188,7 +1204,9 @@ Face 中心基线验证:在 `assets/models/cube_10mm.step` 上选一个平面
Edge 圆角/倒角基线验证:`scripts/verify_edge_round_chamfer.py` 会临时生成普通盒子和已有圆角盒子。当前验证通过五条路线:直线 Edge 新增圆角半径 `1`、对称倒角距离 `1`、不等距倒角 `0.8 / 1.2`、距离+角度倒角 `1 / 45°`、已有圆角半径 `1 -> 1.5`。已有圆角修改走 defeature + refillet;普通 90° 圆角恢复出的锐边距离圆角轴线约 `sqrt(2) * R`,候选搜索半径需要覆盖这个距离。
薄壁厚度基线验证:`scripts/verify_shell_thickness_resize.py` 会临时生成 `30 x 20 x 2` 的薄板。当前验证通过局部和整体两种路线,并在 Face 专项套件里同时覆盖厚度 `2 -> 3``2 -> 1``推拉当前面` 会保持相对面不动,`调整整个特征` 会保持厚度中心基本不动
Edge 隔离执行基线验证:`scripts/verify_edge_isolated_edit.py` 会通过真实 isolated worker 请求覆盖 Edge 长度、端点/中心移动、圆形 Edge 相邻轴心移动、椭圆 Edge 主半径、Edge 圆角、三类倒角和已有圆角半径修改。这个脚本的目标不是替代内存执行套件,而是确认 UI 交给 worker 的操作名和参数能在子进程里独立完成,避免危险 OCCT 倒圆、倒角或局部重建卡死主程序
壳体厚度基线验证:`scripts/verify_shell_thickness_resize.py` 会临时生成 `30 x 20 x 2` 的薄板。当前验证通过局部和整体两种路线,并在 Face 专项套件里同时覆盖厚度 `2 -> 3``2 -> 1``拉伸/切除` 会保持相对面不动,`缩放特征` 会保持厚度中心基本不动。
解析曲面基线验证:`scripts/verify_analytic_surface_resize.py` 会临时生成圆锥、球和环面。当前验证通过五条路线:简单圆锥参考半径 `4 -> 5`、简单圆锥半角 `11.3099° -> 16°`、球面半径 `5 -> 6.25`、环面主半径 `8 -> 10`、环面小半径 `2 -> 3`。球面和环面缩放后仍能识别为解析面;简单圆锥优先解析重建,嵌入式锥孔/沉孔的半角和参考半径由 `scripts/verify_cone_semi_angle_isolation.py` 覆盖局部重切与隔离执行;默认模型里的复杂浅锥/拔模面 `1° -> 10°``1° -> 50°` 会被提前禁用或阻断,避免生成无效 B-Rep。
@@ -1196,7 +1214,7 @@ Edge 圆角/倒角基线验证:`scripts/verify_edge_round_chamfer.py` 会临
新增基线验证:内存构造椭圆Edge,将长度放大 1.25 倍,计划策略为 `radial-affine`,通过预变换Edge长度把缩放比例从 `1.25` 微调到约 `1.25354`,结果边长贴合目标。另一个圆锥圆边先径向缩放后会变成 B-spline,再选这个平面 B-spline Edge 放大 1.1 倍,计划会走 `sampled edge best-fit plane`,采样偏差约 `3.6e-15`,结果边长贴合目标。
后置校验基线:修改Edge长度完成后,结果消息会包含 `match``nearest_edge``nearest_length``target_error``relative_error``endpoint_error` 和校验范围;局部边形变、端面拉和几何缩放会优先按预期端点匹配目标Edge,长度匹配只作为兜底。操作历史摘要也会带上 `actual_length``target_error`,用于提醒用户真实 B-Rep 刷新后的最近Edge长度是否贴近目标值。
后置校验基线:修改Edge长度完成后,结果消息会包含 `match``nearest_edge``nearest_length``target_error``relative_error``endpoint_error` 和校验范围;局部边形变、端面拉伸/切除和几何缩放会优先按预期端点匹配目标Edge,长度匹配只作为兜底。操作历史摘要也会带上 `actual_length``target_error`,用于提醒用户真实 B-Rep 刷新后的最近Edge长度是否贴近目标值。
### 当前边界
+2 -1
View File
@@ -49,6 +49,7 @@ def main() -> int:
state.selected_kind = "feature"
state.selected_part_id = int(root["part_id"])
state.selected_solid_id = int(root["solid_id"])
state.feature_detection_level = "associated-only"
context = state._feature_context_info(SOURCE_FACE_ID)
specs, _used = state._editable_property_specs(context)
rows = state._feature_context_property_specs(specs, context)
@@ -59,7 +60,7 @@ def main() -> int:
}
expected = {
("凸台/外圆候选 · 直径", 394, "resize_boss"),
("凸台/外圆候选 · 高度", 394, "resize_boss_height"),
("凸台/外圆候选 · 高度", 394, "resize_cylindrical_height_owning_scale"),
("圆柱孔候选 · 直径", 1591, "resize_hole"),
("圆柱孔候选 · 盲孔/盲槽深度", 1591, "resize_hole_depth"),
}
@@ -17,6 +17,7 @@ from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Pnt
from step_editor.geometry_utils import _finalize_boolean_result
from step_editor.model import StepModel
from step_editor.operations import _is_effectively_full_cylinder
from step_editor.step_io import _write_step
@@ -113,6 +114,18 @@ def _assert_radii(model: StepModel, expected: tuple[float, ...], label: str) ->
raise SystemExit(f"{label}: radii should be {expected_values}, got {radii}")
def _assert_split_same_domain_cylinder_identity() -> None:
if not _is_effectively_full_cylinder(
{
"angular_span": 3.141592653589793,
"same_domain_angular_span": 6.283185307179586,
}
):
raise SystemExit("split same-domain cylinder should be treated as a full cylinder")
if _is_effectively_full_cylinder({"angular_span": 3.141592653589793}):
raise SystemExit("single partial cylinder should not be treated as a full cylinder")
def _run_isolated_worker(
input_path: Path,
operation: str,
@@ -263,6 +276,7 @@ def _run_prismatic_cap_push_pull_case(path: Path, distance: float, target_height
def main() -> int:
_assert_split_same_domain_cylinder_identity()
with tempfile.TemporaryDirectory(prefix="geom_param_cylinder_height_") as temp_dir:
root = Path(temp_dir)
solid_path = root / "solid_cylinder.step"
+286
View File
@@ -0,0 +1,286 @@
from __future__ import annotations
from pathlib import Path
import sys
import tempfile
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
SCRIPTS_ROOT = PROJECT_ROOT / "scripts"
if str(SCRIPTS_ROOT) not in sys.path:
sys.path.insert(0, str(SCRIPTS_ROOT))
from step_editor.model import StepModel
from verify_edge_coordinate_edit import (
_add,
_distance,
_nearest_expected_edge,
_source_edge_frame,
_tuple3,
)
from verify_edge_length_resize import _edge_length, _line_edge_ids_near_length
from verify_edge_round_chamfer import (
_cylindrical_faces_near_radius,
_first_adjacent_reference_face,
_first_editable_line_edge,
_first_existing_fillet_face,
_verify_chamfer_topology,
_write_box_model,
_write_filleted_box_model,
)
from verify_ellipse_edge_resize import _ellipse_edge_ids, _write_ellipse_face_model
from verify_hole_resize import (
_first_hole_face,
_nearest_hole_by_diameter,
_write_through_hole_model,
)
from verify_hole_slot_isolated_edit import _assert_one_solid, _run_worker, _write_request
DEFAULT_CUBE = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step"
def _load_worker_output(output_path: Path, response: dict[str, object]) -> StepModel:
if not output_path.exists():
raise SystemExit(f"isolated worker reported success but did not write output STEP: response={response}")
return StepModel.load(output_path)
def _verify_edge_length_isolated(temp_dir: Path) -> None:
input_path = DEFAULT_CUBE
output_path = temp_dir / "edge_length_out.step"
request_path = temp_dir / "edge_length_request.json"
model = StepModel.load(input_path)
edge_ids = _line_edge_ids_near_length(model, 10.0, 1e-5)
if not edge_ids:
raise SystemExit("no cube line Edge near length 10 was found")
edge_id = edge_ids[0]
target_length = 15.0
_write_request(
request_path,
input_path,
output_path,
"resize_general_edge_length",
[edge_id, target_length, "keep-start", "local-edge-only-deform"],
)
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
lengths = [_edge_length(result_model, candidate) for candidate in range(len(result_model.edges))]
nearest = min(lengths, key=lambda value: abs(value - target_length))
if abs(nearest - target_length) > 1e-5:
raise SystemExit(f"isolated Edge length failed: nearest={nearest:g}, response={response}")
_assert_one_solid(result_model, "isolated Edge length")
print(f"isolated Edge length ok: edge={edge_id}, nearest={nearest:g}")
def _verify_edge_endpoint_isolated(temp_dir: Path) -> None:
input_path = DEFAULT_CUBE
output_path = temp_dir / "edge_endpoint_out.step"
request_path = temp_dir / "edge_endpoint_request.json"
model = StepModel.load(input_path)
edge_id, start, end, direction, _source_length = _source_edge_frame(model, 10.0, 1e-5)
target_start = _add(start, (direction[0] * -3.0, direction[1] * -3.0, direction[2] * -3.0))
_write_request(request_path, input_path, output_path, "move_edge_endpoint", [edge_id, "start", target_start])
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
matched_edge, endpoint_error, length_error = _nearest_expected_edge(result_model, target_start, end)
if endpoint_error > 1e-5 or length_error > 1e-5:
raise SystemExit(
f"isolated Edge endpoint failed: matched={matched_edge}, "
f"endpoint_error={endpoint_error:g}, length_error={length_error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated Edge endpoint")
print(f"isolated Edge endpoint ok: edge={edge_id}, matched={matched_edge}")
def _verify_edge_center_isolated(temp_dir: Path) -> None:
input_path = DEFAULT_CUBE
output_path = temp_dir / "edge_center_out.step"
request_path = temp_dir / "edge_center_request.json"
model = StepModel.load(input_path)
edge_id, start, end, _direction, _source_length = _source_edge_frame(model, 10.0, 1e-5)
current_center = _tuple3(model.edge_info(edge_id).get("length_center"), "source center")
delta = (0.0, 0.0, 2.0)
target_center = _add(current_center, delta)
target_start = _add(start, delta)
target_end = _add(end, delta)
_write_request(request_path, input_path, output_path, "move_edge_center", [edge_id, target_center])
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
matched_edge, endpoint_error, length_error = _nearest_expected_edge(result_model, target_start, target_end)
if endpoint_error > 1e-5 or length_error > 1e-5:
raise SystemExit(
f"isolated Edge center failed: matched={matched_edge}, "
f"endpoint_error={endpoint_error:g}, length_error={length_error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated Edge center")
print(f"isolated Edge center ok: edge={edge_id}, matched={matched_edge}")
def _verify_circular_edge_axis_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "circular_edge_axis.step"
output_path = temp_dir / "circular_edge_axis_out.step"
request_path = temp_dir / "circular_edge_axis_request.json"
_write_through_hole_model(input_path)
model = StepModel.load(input_path)
hole_face_id = _first_hole_face(model, blind=False)
current_diameter = float(model.face_info(hole_face_id)["diameter"])
edge_id = None
target_edge_center = None
target_axis_center = None
for candidate in range(len(model.edges)):
if model.edge_info(candidate).get("curve") != "circle":
continue
current_edge_center = _tuple3(model.edge_info(candidate).get("center"), f"circle Edge {candidate} center")
candidate_target = (current_edge_center[0] + 2.0, current_edge_center[1], current_edge_center[2])
plan = model.circular_edge_axis_move_plan(candidate, candidate_target)
if plan.get("status") != "blocked":
edge_id = candidate
target_edge_center = candidate_target
target_axis_center = _tuple3(plan.get("target_axis_center"), "target axis center")
break
if edge_id is None or target_edge_center is None or target_axis_center is None:
raise SystemExit("no circular Edge with editable adjacent cylinder axis was found")
_write_request(request_path, input_path, output_path, "move_circular_edge_axis_center", [edge_id, target_edge_center])
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
verified_face, diameter, center, error = _nearest_hole_by_diameter(
result_model,
current_diameter,
target_axis_center,
blind=False,
)
if error > 1e-4 or abs(diameter - current_diameter) > 1e-4:
raise SystemExit(
f"isolated circular Edge axis failed: face={verified_face}, "
f"diameter={diameter:g}, center={center}, error={error:g}, response={response}"
)
_assert_one_solid(result_model, "isolated circular Edge axis")
print(f"isolated circular Edge axis ok: edge={edge_id}, face={verified_face}")
def _verify_ellipse_edge_radius_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "ellipse_edge.step"
output_path = temp_dir / "ellipse_edge_out.step"
request_path = temp_dir / "ellipse_edge_request.json"
_write_ellipse_face_model(input_path)
model = StepModel.load(input_path)
edge_ids = _ellipse_edge_ids(model)
if not edge_ids:
raise SystemExit("no ellipse Edge was recognized")
edge_id = edge_ids[0]
target_radius = 7.5
plan = model.ellipse_edge_axis_radius_plan(edge_id, target_radius, axis_kind="major")
_write_request(
request_path,
input_path,
output_path,
"resize_ellipse_edge_axis_radius",
[edge_id, target_radius, "major"],
)
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
sampled = result_model._ellipse_edge_axis_radius_sampled_result(plan)
if sampled is None:
raise SystemExit(f"isolated ellipse Edge radius could not be sampled: response={response}")
verified_edge, value, other, error = sampled
if error > 5e-3 or abs(other - 2.0) > 5e-3:
raise SystemExit(
f"isolated ellipse Edge radius failed: edge={verified_edge}, "
f"value={value:g}, other={other:g}, error={error:g}, response={response}"
)
print(f"isolated ellipse Edge radius ok: edge={edge_id}, verified={verified_edge}, value={value:g}")
def _verify_edge_fillet_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "edge_fillet.step"
output_path = temp_dir / "edge_fillet_out.step"
request_path = temp_dir / "edge_fillet_request.json"
_write_box_model(input_path)
model = StepModel.load(input_path)
edge_id = _first_editable_line_edge(model)
target_radius = 1.0
_write_request(request_path, input_path, output_path, "fillet_edge", [edge_id, target_radius])
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
matches = _cylindrical_faces_near_radius(result_model, target_radius, 2e-4)
if not matches:
raise SystemExit(f"isolated Edge fillet failed: response={response}")
_assert_one_solid(result_model, "isolated Edge fillet")
print(f"isolated Edge fillet ok: edge={edge_id}, matches={matches[:3]}")
def _verify_edge_chamfers_isolated(temp_dir: Path) -> None:
cases = (
("chamfer_edge", [1.0], "chamfer"),
("chamfer_edge_asymmetric", [0.8, 1.2, None], "asymmetric_chamfer"),
("chamfer_edge_distance_angle", [1.0, 45.0, None], "distance_angle_chamfer"),
)
for operation, tail_args, label in cases:
input_path = temp_dir / f"{label}.step"
output_path = temp_dir / f"{label}_out.step"
request_path = temp_dir / f"{label}_request.json"
_write_box_model(input_path)
model = StepModel.load(input_path)
edge_id = _first_editable_line_edge(model)
reference_face_id = _first_adjacent_reference_face(model, edge_id)
args = [edge_id, *tail_args]
if operation != "chamfer_edge":
args[-1] = reference_face_id
before = model.stats()
_write_request(request_path, input_path, output_path, operation, args)
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
after = result_model.stats()
_verify_chamfer_topology(label, edge_id, {"resize_strategy": operation}, before, after, str(response.get("message")), [])
_assert_one_solid(result_model, f"isolated {label}")
def _verify_existing_fillet_isolated(temp_dir: Path) -> None:
input_path = temp_dir / "existing_fillet.step"
output_path = temp_dir / "existing_fillet_out.step"
request_path = temp_dir / "existing_fillet_request.json"
source_radius = 1.0
target_radius = 1.5
_write_filleted_box_model(input_path, source_radius)
model = StepModel.load(input_path)
face_id = _first_existing_fillet_face(model, source_radius, 2e-4)
_write_request(request_path, input_path, output_path, "resize_existing_fillet", [face_id, target_radius])
response = _run_worker(request_path)
result_model = _load_worker_output(output_path, response)
matches = _cylindrical_faces_near_radius(result_model, target_radius, 2e-4)
old_matches = _cylindrical_faces_near_radius(result_model, source_radius, 2e-4)
if not matches or old_matches:
raise SystemExit(
f"isolated existing fillet failed: matches={matches}, old_matches={old_matches}, response={response}"
)
_assert_one_solid(result_model, "isolated existing fillet")
print(f"isolated existing fillet ok: face={face_id}, matches={matches[:3]}")
def main() -> int:
with tempfile.TemporaryDirectory(prefix="geom_param_edge_isolated_") as temp:
temp_dir = Path(temp)
_verify_edge_length_isolated(temp_dir)
_verify_edge_endpoint_isolated(temp_dir)
_verify_edge_center_isolated(temp_dir)
_verify_circular_edge_axis_isolated(temp_dir)
_verify_ellipse_edge_radius_isolated(temp_dir)
_verify_edge_fillet_isolated(temp_dir)
_verify_edge_chamfers_isolated(temp_dir)
_verify_existing_fillet_isolated(temp_dir)
print("Edge isolated edit suite passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+4
View File
@@ -250,6 +250,9 @@ def _run_existing_fillet_case(source_radius: float, target_radius: float, tolera
plan = model.existing_fillet_resize_plan(face_id, target_radius)
if plan["status"] == "blocked":
raise SystemExit(f"existing fillet plan was blocked: {plan['message']}")
support_face_ids = tuple(plan.get("feature_existing_fillet_support_face_ids", ()))
if len(support_face_ids) < 2:
raise SystemExit(f"existing fillet should expose at least two support Faces, got {support_face_ids}")
result = model.resize_existing_fillet(face_id, target_radius)
after = model.stats()
matches = _cylindrical_faces_near_radius(model, target_radius, tolerance)
@@ -265,6 +268,7 @@ def _run_existing_fillet_case(source_radius: float, target_radius: float, tolera
print(f"strategy={plan.get('resize_strategy')}")
print(f"before={before}")
print(f"after={after}")
print(f"support_face_ids={support_face_ids}")
print(f"source_radius={source_radius:.6f} target_radius={target_radius:.6f}")
print(f"matched_faces={matches}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
+12
View File
@@ -86,6 +86,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = (
"planar Face on curved Solid disables local-only deformation",
("verify_face_mixed_surface_guard.py",),
),
(
"freeform Face stays read-only with a clear blocker",
("verify_face_freeform_guard.py",),
),
(
"cylindrical side Face height edits are checked",
("verify_cylindrical_height_resize.py",),
@@ -130,6 +134,14 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = (
"Face logical selection stays on the edited face",
("verify_face_logical_selection_retention.py",),
),
(
"Face selection identity UI prefers logical IDs",
("verify_selection_identity_ui.py",),
),
(
"Face feature parameters stay stable after full recognition cache",
("verify_face_feature_parameter_consistency.py",),
),
(
"Face invalid positive targets are blocked",
("verify_face_invalid_target_guards.py",),
@@ -0,0 +1,88 @@
from __future__ import annotations
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.model import StepModel
from step_editor.window_state import WindowStateMixin, _feature_dimension_keys
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
FACE_ID = 594
BASE_FACE_KEYS = (
"area",
"local_face_width",
"local_face_height",
"face_center_position",
"face_target_normal_position",
)
class _Probe(WindowStateMixin):
pass
def _feature_rows(model: StepModel, face_id: int) -> tuple[str, ...]:
info = model.quick_face_info(face_id)
probe = object.__new__(_Probe)
probe.model = model
probe.operation_in_progress = False
probe.scan_in_progress = False
probe.load_in_progress = False
probe.selected_face_id = face_id
probe.selected_edge_id = None
probe.selected_kind = "feature"
probe.selected_part_id = int(info["part_id"])
probe.selected_solid_id = int(info["solid_id"])
probe.feature_detection_level = "current-only"
probe.manual_bottom_face_id = None
probe.manual_slot_pair_face_id = None
context = probe._feature_context_info(face_id)
specs, _used = probe._editable_property_specs(context)
rows = probe._feature_context_property_specs(specs, context)
return tuple(str(row.get("key")) for row in rows if row.get("parameter_role") == "dimension")
def _assert_contains(keys: tuple[str, ...], expected: tuple[str, ...], label: str) -> None:
missing = [key for key in expected if key not in keys]
if missing:
raise AssertionError(f"{label} missing {missing}, got {keys}")
def main() -> int:
model = StepModel.load(MODEL_PATH)
if FACE_ID >= len(model.faces):
raise AssertionError(f"test model has no Face {FACE_ID}")
quick_info = model.quick_face_info(FACE_ID)
if quick_info.get("surface") != "plane":
raise AssertionError(f"Face {FACE_ID} should be planar in the baseline model: {quick_info}")
before_cached_rows = _feature_rows(model, FACE_ID)
_assert_contains(before_cached_rows, BASE_FACE_KEYS, "Face 594 before full feature cache")
full_info = model.feature_info(FACE_ID)
if full_info.get("shell_region_status") == "candidate":
dimension_keys = _feature_dimension_keys(full_info)
_assert_contains(dimension_keys, BASE_FACE_KEYS, "shell candidate feature dimensions")
if "shell_thickness_estimate" not in dimension_keys:
raise AssertionError(f"shell candidate should keep shell thickness as an extra dimension: {dimension_keys}")
after_cached_rows = _feature_rows(model, FACE_ID)
_assert_contains(after_cached_rows, BASE_FACE_KEYS, "Face 594 after full feature cache")
if before_cached_rows != after_cached_rows:
raise AssertionError(
"Face 594 current-only feature rows changed after full recognition cache: "
f"before={before_cached_rows}, after={after_cached_rows}"
)
print("Face feature parameter consistency ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+136
View File
@@ -0,0 +1,136 @@
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.GeomAbs import GeomAbs_BSplineSurface
from OCC.Core.GeomAPI import GeomAPI_PointsToBSplineSurface
from OCC.Core.TColgp import TColgp_Array2OfPnt
from OCC.Core.gp import gp_Pnt
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.model import StepModel
from step_editor.step_io import _write_step
from step_editor.window_state import WindowStateMixin
class _PropertySpecProbe(WindowStateMixin):
def __init__(self, face_id: int, selected_kind: str = "face") -> None:
self.model = object()
self.operation_in_progress = False
self.scan_in_progress = False
self.load_in_progress = False
self.selected_face_id = face_id
self.selected_edge_id = None
self.selected_kind = selected_kind
self.selected_part_id = 1
self.selected_solid_id = -1
self.manual_bottom_face_id = None
self.manual_slot_pair_face_id = None
def _write_b_spline_patch(path: Path) -> None:
points = TColgp_Array2OfPnt(1, 4, 1, 4)
for row in range(1, 5):
x = float(row - 1) * 10.0
for column in range(1, 5):
y = float(column - 1) * 10.0
z = 1.2 * ((row - 2.5) ** 2 - (column - 2.5) ** 2)
points.SetValue(row, column, gp_Pnt(x, y, z))
surface = GeomAPI_PointsToBSplineSurface(points).Surface()
face = BRepBuilderAPI_MakeFace(surface, 1e-6).Face()
_write_step(face, path)
def _freeform_face_id(model: StepModel) -> int:
for face_id, face in enumerate(model.faces):
surf = BRepAdaptor_Surface(face)
if surf.GetType() == GeomAbs_BSplineSurface:
return face_id
raise SystemExit("B-spline Face was not found")
def _specs(info: dict[str, object], selected_kind: str = "face") -> list[dict[str, object]]:
probe = _PropertySpecProbe(int(info.get("face_id", 0)), selected_kind=selected_kind)
specs, _used = probe._editable_property_specs(info)
return specs
def _property_rows(info: dict[str, object], selected_kind: str = "face") -> list[dict[str, object]]:
probe = _PropertySpecProbe(int(info.get("face_id", 0)), selected_kind=selected_kind)
return probe._property_editor_specs(info, info)
def _assert_no_editable_actions(specs: list[dict[str, object]], label: str) -> None:
editable = [
str(spec.get("key"))
for spec in specs
if bool(spec.get("editable")) and bool(spec.get("enabled")) and bool(spec.get("action"))
]
if editable:
raise SystemExit(f"{label} should not expose editable actions for a freeform Face: {editable}")
def main() -> int:
with tempfile.TemporaryDirectory(prefix="geom_param_freeform_guard_") as temp_dir:
path = Path(temp_dir) / "freeform_patch.step"
_write_b_spline_patch(path)
model = StepModel.load(path)
face_id = _freeform_face_id(model)
quick_info = model.quick_face_info(face_id)
info = model.face_info(face_id)
for label, selected_info in (("quick", quick_info), ("full", info)):
if selected_info.get("surface") != "b-spline surface":
raise SystemExit(f"{label} info should identify a B-spline surface: {selected_info}")
if selected_info.get("freeform_face_status") != "blocked":
raise SystemExit(f"{label} info should block freeform Face editing: {selected_info}")
if selected_info.get("recognition_risk") != "blocked":
raise SystemExit(f"{label} recognition risk should be blocked: {selected_info}")
blockers = str(selected_info.get("freeform_face_blockers") or "")
if "自由曲面" not in blockers or "历史参数" not in blockers:
raise SystemExit(f"{label} blocker should explain the missing stable history parameter: {selected_info}")
editable_specs = _specs(info)
editable_keys = {str(spec.get("key")) for spec in editable_specs}
if "freeform_surface_edit_semantics" not in editable_keys:
raise SystemExit(f"freeform Face should expose a read-only edit semantics row: {editable_specs}")
forbidden = {
"area",
"local_face_width",
"local_face_height",
"face_center_position",
"face_target_normal_position",
"cone_reference_radius",
"sphere_radius",
"torus_major_radius",
}
leaked = sorted(forbidden & editable_keys)
if leaked:
raise SystemExit(f"freeform Face should not expose parametric edit rows: {leaked}")
_assert_no_editable_actions(editable_specs, "face mode editable specs")
feature_rows = _property_rows(info, selected_kind="feature")
feature_keys = {str(spec.get("key")) for spec in feature_rows}
if "no_editable_feature_dimensions" not in feature_keys:
raise SystemExit(f"feature mode should say there are no reliable dimensions: {feature_rows}")
if "freeform_surface_edit_semantics" not in feature_keys:
raise SystemExit(f"feature mode should keep the freeform limitation explanation: {feature_rows}")
_assert_no_editable_actions(feature_rows, "feature mode rows")
print(
"freeform Face guard ok: "
f"face_id={face_id}, surface={info.get('surface')}, "
f"risk={info.get('recognition_risk')}, specs={sorted(editable_keys)}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -202,7 +202,7 @@ def main() -> int:
specs = _specs(face_id, info)
semantics = _spec(specs, "face_edit_semantics")
if "不能只改当前面" not in str(semantics.get("current_text") or ""):
if "不能局部重建" not in str(semantics.get("current_text") or ""):
raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}")
if "曲面" not in str(semantics.get("disabled_tip") or ""):
raise SystemExit(f"Face edit semantics tip should include the curved-owner blocker: {semantics}")
+161 -28
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import argparse
import ast
import sys
import time
@@ -30,6 +31,41 @@ EXPECTED_FACE_ISOLATED_OPERATIONS = {
"resize_torus_radius",
}
EXPECTED_HOLE_SLOT_ISOLATED_OPERATIONS = {
"resize_cylindrical_hole",
"resize_cylindrical_owning_scale",
"move_cylindrical_hole_axis",
"suppress_cylindrical_hole",
"resize_cylindrical_depth",
"resize_cylindrical_depth_owning_scale",
"move_cylindrical_slot_axis",
"resize_cylindrical_slot_width",
"resize_cylindrical_slot_depth",
"resize_cylindrical_slot_arc_length",
"resize_cylindrical_slot_angular_span",
"resize_cylindrical_slot_total_length",
"resize_cylindrical_slot_center_distance",
}
EXPECTED_EDGE_ISOLATED_OPERATIONS = {
"resize_general_edge_length",
"move_edge_endpoint",
"move_edge_center",
"move_circular_edge_axis_center",
"resize_ellipse_edge_axis_radius",
"resize_existing_fillet",
"fillet_edge",
"chamfer_edge",
"chamfer_edge_asymmetric",
"chamfer_edge_distance_angle",
}
EXPECTED_ISOLATED_OPERATIONS = (
EXPECTED_FACE_ISOLATED_OPERATIONS
| EXPECTED_HOLE_SLOT_ISOLATED_OPERATIONS
| EXPECTED_EDGE_ISOLATED_OPERATIONS
)
def _source_tree(relative_path: str) -> ast.Module:
return ast.parse((PROJECT_ROOT / relative_path).read_text(encoding="utf-8"))
@@ -123,6 +159,37 @@ ACTION_IMPLEMENTATION_METHOD = {
"resize_torus_minor_radius": "_resize_torus_radius",
}
FEATURE_ACTION_TO_ISOLATED_OPERATION = {
"resize_hole": "resize_cylindrical_hole",
"resize_cylindrical_owning_scale": "resize_cylindrical_owning_scale",
"move_cylindrical_hole_axis": "move_cylindrical_hole_axis",
"suppress_hole": "suppress_cylindrical_hole",
"resize_hole_depth": "resize_cylindrical_depth",
"resize_hole_depth_owning_scale": "resize_cylindrical_depth_owning_scale",
"move_cylindrical_slot_axis": "move_cylindrical_slot_axis",
"_resize_slot_metric_owning_scale": "resize_cylindrical_owning_scale",
"resize_slot_width": "resize_cylindrical_slot_width",
"resize_slot_depth": "resize_cylindrical_slot_depth",
"resize_slot_arc_length": "resize_cylindrical_slot_arc_length",
"resize_slot_angular_span": "resize_cylindrical_slot_angular_span",
"resize_slot_total_length": "resize_cylindrical_slot_total_length",
"resize_slot_center_distance": "resize_cylindrical_slot_center_distance",
}
EDGE_ACTION_TO_ISOLATED_OPERATION = {
"resize_existing_fillet": "resize_existing_fillet",
"fillet_edge": "fillet_edge",
"chamfer_edge": "chamfer_edge",
"chamfer_edge_asymmetric": "chamfer_edge_asymmetric",
"chamfer_edge_distance_angle": "chamfer_edge_distance_angle",
"resize_edge_length": "resize_general_edge_length",
"resize_any_edge_length": "resize_general_edge_length",
"_resize_ellipse_edge_axis_radius": "resize_ellipse_edge_axis_radius",
"move_circular_edge_axis_center": "move_circular_edge_axis_center",
"move_edge_center_point": "move_edge_center",
"_move_edge_endpoint": "move_edge_endpoint",
}
def _property_spec_actions_for_keys(specs: list[dict[str, object]], keys: set[str]) -> set[str]:
actions: set[str] = set()
@@ -285,7 +352,7 @@ def _assert_property_face_actions_are_isolated(actions_tree: ast.Module) -> None
raise SystemExit(f"Face property action contract mismatch: missing={missing}, stale={stale}")
for action, isolated_operation in sorted(PROPERTY_FACE_ACTION_TO_ISOLATED_OPERATION.items()):
if isolated_operation not in EXPECTED_FACE_ISOLATED_OPERATIONS:
if isolated_operation not in EXPECTED_ISOLATED_OPERATIONS:
raise SystemExit(f"{action} maps to non-worker isolated operation {isolated_operation!r}")
method_name = ACTION_IMPLEMENTATION_METHOD.get(action, action)
actual_operations = _method_isolation_operations(actions_tree, method_name)
@@ -296,17 +363,74 @@ def _assert_property_face_actions_are_isolated(actions_tree: ast.Module) -> None
)
def main() -> int:
def _assert_property_face_mapping_methods_are_isolated(actions_tree: ast.Module) -> None:
for action, isolated_operation in sorted(PROPERTY_FACE_ACTION_TO_ISOLATED_OPERATION.items()):
if isolated_operation not in EXPECTED_ISOLATED_OPERATIONS:
raise SystemExit(f"{action} maps to non-worker isolated operation {isolated_operation!r}")
method_name = ACTION_IMPLEMENTATION_METHOD.get(action, action)
actual_operations = _method_isolation_operations(actions_tree, method_name)
if isolated_operation not in actual_operations:
raise SystemExit(
f"{action} should enter isolated operation {isolated_operation!r} through {method_name}; "
f"actual={sorted(actual_operations)}"
)
def _assert_hole_slot_actions_are_isolated(actions_tree: ast.Module) -> None:
for method_name, isolated_operation in sorted(FEATURE_ACTION_TO_ISOLATED_OPERATION.items()):
if isolated_operation not in EXPECTED_ISOLATED_OPERATIONS:
raise SystemExit(f"{method_name} maps to non-worker isolated operation {isolated_operation!r}")
actual_operations = _method_isolation_operations(actions_tree, method_name)
if isolated_operation not in actual_operations:
raise SystemExit(
f"{method_name} should enter isolated operation {isolated_operation!r}; "
f"actual={sorted(actual_operations)}"
)
def _assert_edge_actions_are_isolated(actions_tree: ast.Module) -> None:
for method_name, isolated_operation in sorted(EDGE_ACTION_TO_ISOLATED_OPERATION.items()):
if isolated_operation not in EXPECTED_ISOLATED_OPERATIONS:
raise SystemExit(f"{method_name} maps to non-worker isolated operation {isolated_operation!r}")
actual_operations = _method_isolation_operations(actions_tree, method_name)
if isolated_operation not in actual_operations:
raise SystemExit(
f"{method_name} should enter isolated operation {isolated_operation!r}; "
f"actual={sorted(actual_operations)}"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Verify UI/worker isolation contracts for risky geometry edits.")
parser.add_argument(
"--static-only",
action="store_true",
help="Only check AST-level operation contracts; skip OCCT-dependent property/model probes.",
)
args = parser.parse_args(argv)
worker_execute = _function_node(_source_tree("step_editor/isolated_edit_worker.py"), "_execute")
worker_ops = _worker_operation_set(worker_execute)
_assert_same("isolated worker operations", worker_ops, EXPECTED_FACE_ISOLATED_OPERATIONS)
_assert_same("isolated worker operations", worker_ops, EXPECTED_ISOLATED_OPERATIONS)
actions_tree = _source_tree("step_editor/window_actions.py")
isolation_func = _function_node(actions_tree, "_isolation_for_plan")
ui_ops = _constant_string_set(isolation_func, "isolated_face_operations")
_assert_same("UI isolation operations", ui_ops, EXPECTED_FACE_ISOLATED_OPERATIONS)
ui_ops = _constant_string_set(isolation_func, "isolated_geometry_operations")
_assert_same("UI isolation operations", ui_ops, EXPECTED_ISOLATED_OPERATIONS)
_assert_same("UI/worker isolation operation contract", ui_ops, worker_ops)
if args.static_only:
_assert_property_face_mapping_methods_are_isolated(actions_tree)
else:
_assert_property_face_actions_are_isolated(actions_tree)
_assert_hole_slot_actions_are_isolated(actions_tree)
_assert_edge_actions_are_isolated(actions_tree)
if args.static_only:
print(
"Static UI/worker isolation contract passed: "
f"{len(EXPECTED_ISOLATED_OPERATIONS)} operations are shared by UI and worker."
)
return 0
from step_editor.window_actions import WindowActionMixin
@@ -315,7 +439,7 @@ def main() -> int:
probe = _Probe()
for risk in ("low", "medium", "high"):
for operation in sorted(EXPECTED_FACE_ISOLATED_OPERATIONS):
for operation in sorted(EXPECTED_ISOLATED_OPERATIONS):
isolation = probe._isolation_for_plan(
{"risk": risk},
operation,
@@ -330,13 +454,13 @@ def main() -> int:
raise SystemExit(f"{operation}/{risk} returned wrong args: {isolation}")
if float(isolation.get("timeout_seconds") or 0.0) != 7.5:
raise SystemExit(f"{operation}/{risk} returned wrong timeout: {isolation}")
if str(isolation.get("reason") or "") != f"{risk}-risk-face-occ-edit":
if str(isolation.get("reason") or "") != f"{risk}-risk-isolated-occ-edit":
raise SystemExit(f"{operation}/{risk} returned wrong reason: {isolation}")
if probe._isolation_for_plan({"risk": "blocked"}, "push_pull_face", [0, 1]) is not None:
raise SystemExit("blocked plans should not enter isolated execution")
if probe._isolation_for_plan({"risk": "high"}, "resize_cylindrical_hole", [0, 20]) is not None:
raise SystemExit("non-Face feature operations should not be covered by the Face isolation contract")
if probe._isolation_for_plan({"risk": "high"}, "resize_cylindrical_hole", [0, 20]) is None:
raise SystemExit("hole/slot feature operations should use the shared isolated geometry contract")
if probe._isolation_for_plan({"risk": "high"}, "unknown_operation", []) is not None:
raise SystemExit("unknown operations should not enter isolated execution")
@@ -418,30 +542,35 @@ def main() -> int:
multi_boundary_probe.step_path = complex_path
multi_boundary_probe.selected_face_id = multi_boundary_face_id
multi_boundary_probe.current_info_values = model.quick_face_info(multi_boundary_face_id)
outward_plan = multi_boundary_probe._push_pull_plan_for_action(multi_boundary_face_id, 32.5)
if abs(float(outward_plan.get("current_plane_position") or 0.0) - 57.5) > 1e-9:
raise SystemExit(f"multi-boundary outward quick plan should use the displayed Face offset: {outward_plan}")
if abs(float(outward_plan.get("target_plane_position") or 0.0) - 90.0) > 1e-9:
raise SystemExit(f"multi-boundary outward quick plan should target Face offset 90: {outward_plan}")
if tuple(float(item) for item in outward_plan.get("outward_direction", ())) != (0.0, -1.0, 0.0):
raise SystemExit(f"multi-boundary outward quick plan should use normal, not reversed oriented_normal: {outward_plan}")
started = time.perf_counter()
inward_plan = multi_boundary_probe._push_pull_plan_for_action(multi_boundary_face_id, -1.0)
elapsed = time.perf_counter() - started
if elapsed > 0.05:
raise SystemExit(f"multi-boundary inward push/pull UI preflight should be quick, elapsed={elapsed:.3f}s")
if inward_plan.get("status") != "blocked":
raise SystemExit(f"multi-boundary inward push/pull UI preflight should be blocked: {inward_plan}")
if not inward_plan.get("ui_quick_blocked_push_pull_plan"):
raise SystemExit(f"multi-boundary inward push/pull should be marked as a quick blocker: {inward_plan}")
message = str(inward_plan.get("message") or "")
if "二级关系" not in message or "通用 OCCT 布尔" not in message:
if inward_plan.get("status") != "caution":
raise SystemExit(f"multi-boundary shallow inward push/pull UI preflight should be caution: {inward_plan}")
if inward_plan.get("risk") not in {"medium", "high"}:
raise SystemExit(f"multi-boundary shallow inward push/pull should be medium/high risk: {inward_plan}")
if inward_plan.get("ui_quick_blocked_push_pull_plan"):
raise SystemExit(f"multi-boundary shallow inward push/pull should not be marked as blocked: {inward_plan}")
message = str(inward_plan.get("message") or "") + " " + str(inward_plan.get("warnings") or "")
if "多内孔" not in message or ("一级边界侧壁" not in message and "后台" not in message):
raise SystemExit(
f"multi-boundary inward push/pull blocker should explain topology depth and Boolean risk: "
f"multi-boundary shallow inward push/pull should explain boundary-shell rebuild semantics: "
f"{inward_plan}"
)
for required in ("Face 594", "内边界", "一级边界 Edge", "一级相邻 Face"):
if required not in message:
raise SystemExit(
f"multi-boundary inward push/pull blocker should include readable topology evidence "
f"{required!r}: {inward_plan}"
)
if inward_plan.get("target_plane_position") != 56.5:
raise SystemExit(f"multi-boundary shallow inward push/pull should target 56.5: {inward_plan}")
diagnostics = multi_boundary_probe._edit_failure_diagnostics(
{
"operation_name": "拉平面",
"operation_name": "伸/切除平面",
"target": f"Face {multi_boundary_face_id}",
"parameters": {
"surface": inward_plan.get("surface"),
@@ -467,19 +596,23 @@ def main() -> int:
"operation": "push_pull_face",
"args": [multi_boundary_face_id, -1.0],
"timeout_seconds": 180.0,
"reason": "high-risk-face-occ-edit",
"reason": f"{inward_plan.get('risk')}-risk-isolated-occ-edit",
},
}
)
for required in ("诊断信息", "操作: 拉平面 / Face 594", "一级关系证据", "可能原因", "二级", "隔离保护"):
for required in ("诊断信息", "操作: 拉伸/切除平面 / Face 594", "一级关系证据", "隔离保护"):
if required not in diagnostics:
raise SystemExit(f"edit failure diagnostics should include {required!r}: {diagnostics}")
if multi_boundary_probe._isolation_for_plan(inward_plan, "push_pull_face", [multi_boundary_face_id, -1.0]) is not None:
raise SystemExit(f"blocked multi-boundary inward push/pull should not enter isolated execution: {inward_plan}")
isolation = multi_boundary_probe._isolation_for_plan(inward_plan, "push_pull_face", [multi_boundary_face_id, -1.0])
if isolation is None:
raise SystemExit(f"multi-boundary shallow inward push/pull should enter isolated execution: {inward_plan}")
expected_reason = f"{inward_plan.get('risk')}-risk-isolated-occ-edit"
if str(isolation.get("reason") or "") != expected_reason:
raise SystemExit(f"multi-boundary shallow inward push/pull returned wrong isolation reason: {isolation}")
print(
"Face UI isolation contract ok: "
f"{len(EXPECTED_FACE_ISOLATED_OPERATIONS)} operations are shared by UI and worker."
f"{len(EXPECTED_ISOLATED_OPERATIONS)} operations are shared by UI and worker."
)
return 0
+18 -1
View File
@@ -53,9 +53,26 @@ def main() -> int:
},
("existing_fillet_radius_estimate",),
)
assert_keys(
{"surface": "plane"},
(
"area",
"local_face_width",
"local_face_height",
"face_center_position",
"face_target_normal_position",
),
)
assert_keys(
{"surface": "plane", "shell_region_status": "candidate"},
("shell_thickness_estimate",),
(
"area",
"local_face_width",
"local_face_height",
"face_center_position",
"face_target_normal_position",
"shell_thickness_estimate",
),
)
assert_keys(
{
@@ -129,9 +129,9 @@ def _verify_holed_planar_summary(root: Path) -> None:
_assert(not str(info.get("recognition_blockers") or ""), f"holed Face blockers should stay empty: {info}")
ready_actions = str(info.get("recognition_ready_actions") or "")
limited_actions = str(info.get("recognition_limited_actions") or "")
_assert("平面" in ready_actions, f"holed Face should expose push/pull as ready: {info}")
_assert("平面拉伸/切除" in ready_actions, f"holed Face should expose push/pull as ready: {info}")
_assert(
"当前面局部尺寸/中心/偏移" in limited_actions,
"局部重建尺寸/中心/偏移" in limited_actions,
f"holed Face should expose local deformation as limited: {info}",
)
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -374,49 +374,49 @@ def main() -> int:
input_path=hollow_path,
)
_run_worker_case(
label="偏移(当前面)",
label="偏移曲面(局部重建)",
operation="move_face_plane_offset_local",
args=[0, 10.0],
validator=_assert_plane_position,
)
_run_worker_case(
label="面积(当前面)",
label="面积(局部重建)",
operation="resize_face_area_local",
args=[0, 225.0],
validator=_assert_face_area,
)
_run_worker_case(
label="面积(整体)",
label="面积(缩放特征)",
operation="resize_face_area",
args=[0, 400.0],
validator=lambda label, model: _assert_face_area(label, model, 400.0),
)
_run_worker_case(
label="面宽(当前面)",
label="U向尺寸(局部重建)",
operation="resize_face_size_local",
args=[0, 25.0, "width"],
validator=_assert_face_width,
)
_run_worker_case(
label="面高(当前面)",
label="V向尺寸(局部重建)",
operation="resize_face_size_local",
args=[0, 25.0, "height"],
validator=_assert_face_height,
)
_run_worker_case(
label="面宽(整体)",
label="U向尺寸(缩放特征)",
operation="resize_face_size_owning_scale",
args=[0, 25.0, "width"],
validator=lambda label, model: _assert_face_width(label, model, 25.0),
)
_run_worker_case(
label="面高(整体)",
label="V向尺寸(缩放特征)",
operation="resize_face_size_owning_scale",
args=[0, 25.0, "height"],
validator=lambda label, model: _assert_face_height(label, model, 25.0),
)
_run_worker_case(
label="中心(当前面)",
label="中心(局部重建)",
operation="move_face_center_local",
args=[0, [15.0, 5.0, 0.0]],
validator=_assert_face_center,
@@ -430,7 +430,7 @@ def main() -> int:
if str(shell_plan.get("risk")) != "high":
raise SystemExit(f"shell thickness isolation case should be high risk, got {shell_plan}")
_run_worker_case(
label="薄壁厚度(当前面)",
label="壳体厚度(拉伸/切除)",
operation="resize_shell_thickness",
args=[shell_face_id, 4.0],
validator=_assert_shell_thickness,
@@ -440,7 +440,7 @@ def main() -> int:
if str(shell_owning_plan.get("risk")) != "high":
raise SystemExit(f"shell thickness owning isolation case should be high risk, got {shell_owning_plan}")
_run_worker_case(
label="薄壁厚度(整体)",
label="壳体厚度(缩放特征)",
operation="resize_shell_thickness_owning_scale",
args=[shell_face_id, 4.0],
validator=_assert_shell_thickness,
@@ -534,16 +534,16 @@ def main() -> int:
delattr(sys, "frozen")
for title in (
"面宽(当前面)",
"面高(当前面)",
"面宽(整体)",
"面高(整体)",
"薄壁厚度(整体)缩放所属对象",
"U向尺寸(局部重建)",
"V向尺寸(局部重建)",
"U向尺寸(缩放特征)",
"V向尺寸(缩放特征)",
"壳体厚度(缩放特征)缩放所属对象",
):
if not probe._quick_edit_title_supports_isolation(title):
raise SystemExit(f"{title}: quick edit title should support isolated execution")
context = {
"operation_name": "面宽(当前面)",
"operation_name": "U向尺寸(局部重建)",
"target": "Face 0",
"parameters": {"part_id": 1, "face_id": 0},
"target_kind": "face",
@@ -596,7 +596,7 @@ def main() -> int:
owning_result = owning_probe._run_isolated_edit_job(
context={
**context,
"operation_name": "面宽(整体)",
"operation_name": "U向尺寸(缩放特征)",
},
isolation=owning_isolation,
snapshot=owning_probe.model.snapshot(),
@@ -616,7 +616,7 @@ def main() -> int:
raise SystemExit("isolated owning window job replaced the window model before returning to the UI thread")
if owning_result.get("model_polydata") is None or owning_result.get("edge_polydata") is None:
raise SystemExit("isolated owning window job should return prebuilt display polydata")
_assert_face_width("面宽(整体窗口任务)", owning_after_model, 25.0)
_assert_face_width("U向尺寸(整体窗口任务)", owning_after_model, 25.0)
if not _logical_region_has_width(owning_after_model, 0, 25.0):
raise SystemExit("isolated owning window job did not preserve the original logical Face ID on the edited width")
print("isolated owning window job ok")
+83 -5
View File
@@ -19,6 +19,11 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.model import StepModel
from step_editor.window_actions import WindowActionMixin
class _WindowActionProbe(WindowActionMixin):
pass
def _wire_count(face) -> int:
@@ -240,17 +245,54 @@ def main() -> int:
raise SystemExit(
f"multi-boundary cap inward push/pull plan should be quick: {multi_inward_elapsed:.3f}s"
)
if multi_inward_plan.get("status") != "blocked":
if multi_inward_plan.get("status") == "blocked":
raise SystemExit(
f"multi-boundary cap inward push/pull should be blocked until second-level propagation exists: "
f"multi-boundary cap shallow inward push/pull should be allowed by boundary shell rebuild: "
f"{multi_inward_plan}"
)
multi_inward_message = str(multi_inward_plan.get("message") or "")
if "二级关系" not in multi_inward_message or "通用 OCCT 布尔" not in multi_inward_message:
if multi_inward_plan.get("planar_cap_extension_method") != "boundary-shell-rebuild":
raise SystemExit(
f"multi-boundary cap inward blocker should explain topology depth and slow Boolean risk: "
f"multi-boundary cap shallow inward should use boundary shell rebuild: {multi_inward_plan}"
)
multi_inward_message = str(multi_inward_plan.get("message") or "")
if "向内收缩" not in multi_inward_message and "重建侧壁" not in multi_inward_message:
raise SystemExit(
f"multi-boundary cap shallow inward message should explain boundary shell semantics: "
f"{multi_inward_plan}"
)
multi_inward_result_model = StepModel.load(path)
started = time.perf_counter()
multi_inward_result = multi_inward_result_model.push_pull_face(multi_face_id, -1.0)
multi_inward_result_elapsed = time.perf_counter() - started
if multi_inward_result_elapsed > 15.0:
raise SystemExit(
f"multi-boundary cap shallow inward push/pull took too long: "
f"{multi_inward_result_elapsed:.3f}s; result={multi_inward_result}"
)
if "boundary-shell rebuild" not in multi_inward_result or "actual=56.5" not in multi_inward_result:
raise SystemExit(
f"multi-boundary cap shallow inward should move to 56.5 by boundary shell rebuild: {multi_inward_result}"
)
if multi_inward_result_model.stats().solids != 1:
raise SystemExit(f"multi-boundary cap shallow inward should keep one Solid: {multi_inward_result_model.stats()}")
inward_retained_ids = multi_inward_result_model.face_ids_for_logical_id(multi_logical_id)
if len(inward_retained_ids) != 1:
raise SystemExit(f"multi-boundary cap shallow inward logical Face should follow the moved cap: {inward_retained_ids}")
inward_info = multi_inward_result_model.quick_face_info(inward_retained_ids[0])
if int(inward_info.get("inner_boundary_wires", 0) or 0) < 5:
raise SystemExit(f"multi-boundary cap shallow inward should keep inner wires: {inward_info}")
multi_deep_inward_plan = multi_model.push_pull_plan(multi_face_id, -77.5)
if multi_deep_inward_plan.get("status") != "blocked":
raise SystemExit(
f"multi-boundary cap deep inward push/pull should still be blocked: {multi_deep_inward_plan}"
)
multi_deep_inward_message = str(multi_deep_inward_plan.get("message") or "")
if "材料厚度" not in multi_deep_inward_message and "二级关系" not in multi_deep_inward_message:
raise SystemExit(
f"multi-boundary cap deep inward blocker should explain cut-through or topology depth: "
f"{multi_deep_inward_plan}"
)
started = time.perf_counter()
multi_result = multi_model.push_pull_face(multi_face_id, 34.5)
@@ -351,6 +393,42 @@ def main() -> int:
raise SystemExit(
f"large multi-boundary cap isolated worker used the wrong path: {multi_isolated_message}"
)
isolated_output_model = StepModel.load(output_path)
probe = _WindowActionProbe()
direction = multi_plan.get("outward_direction") or multi_plan.get("plane_direction")
context = {
"operation_name": "拉伸/切除平面",
"target_kind": "face",
"target_id": multi_face_id,
"target_logical_id": multi_face_id,
"parameters": {
"part_id": multi_plan.get("part_id"),
"solid_id": multi_plan.get("solid_id"),
"surface": "plane",
"outward_direction": direction,
"current_plane_position": multi_plan.get("current_plane_position"),
"target_plane_position": multi_plan.get("target_plane_position"),
"bbox_diagonal": multi_plan.get("bbox_diagonal"),
"resize_strategy": multi_plan.get("resize_strategy"),
},
}
blocker = probe._face_target_integrity_blocker(isolated_output_model, context)
if blocker:
raise SystemExit(f"window-layer isolated target check should accept the moved cap: {blocker}")
probe._preserve_isolated_face_logical_id(isolated_output_model, context, multi_isolated_message)
retained_after_isolation = isolated_output_model.face_ids_for_logical_id(multi_face_id)
if not retained_after_isolation:
raise SystemExit("window-layer isolated logical Face retention lost the moved cap")
if not probe._face_target_plane_position_matches(
isolated_output_model,
retained_after_isolation,
float(multi_plan["target_plane_position"]),
direction,
float(multi_plan["bbox_diagonal"]),
):
raise SystemExit(
f"window-layer isolated logical Face retention points at the wrong cap: {retained_after_isolation}"
)
print(
"large stepped cap push/pull ok: "
+103 -9
View File
@@ -9,7 +9,7 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.window_state import WindowStateMixin
from step_editor.ui_helpers import _format_value
from step_editor.ui_helpers import INFO_LABELS, _format_value
class _PropertySpecProbe(WindowStateMixin):
@@ -181,7 +181,20 @@ def _assert_actionable_rows_first(info: dict[str, object], expected_keys: tuple[
def _collect_legacy_face_terms(value: object, path: str = "specs") -> list[str]:
legacy_terms = ("面内尺寸 1/2", "面内尺寸 1", "面内尺寸 2", "面位置", "偏移距离")
legacy_terms = (
"面内尺寸 1/2",
"面内尺寸 1",
"面内尺寸 2",
"面位置",
"偏移距离",
"面偏移",
"面宽",
"面高",
"推拉当前面",
"只改当前面",
"调整整个特征",
"移动整个特征",
)
hits: list[str] = []
if isinstance(value, dict):
for key, child in value.items():
@@ -229,6 +242,13 @@ def _assert_no_legacy_face_source_terms() -> None:
re.compile(r"面内尺寸 2"),
re.compile(r"(?<![端平底])面位置"),
re.compile(r"偏移距离"),
re.compile(r"面偏移"),
re.compile(r"面宽"),
re.compile(r"(?<!侧)面高(?!度)"),
re.compile(r"推拉当前面"),
re.compile(r"只改当前面"),
re.compile(r"调整整个特征"),
re.compile(r"移动整个特征"),
)
hits: list[str] = []
for file_path in files:
@@ -309,13 +329,19 @@ def _assert_holed_plane_local_scopes_disabled() -> None:
raise SystemExit("Face plane-offset push/pull scope should remain available for a holed planar Face")
semantics = _spec(specs, "face_edit_semantics")
if "不能只改当前面" not in str(semantics.get("current_text") or ""):
if "不能局部重建" not in str(semantics.get("current_text") or ""):
raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}")
if "has inner boundary" not in str(semantics.get("disabled_tip") or ""):
raise SystemExit(f"Face edit semantics tip should include the blocker: {semantics}")
def main() -> int:
if INFO_LABELS.get("face_id") != "当前拓扑 Face ID":
raise SystemExit("raw face_id label should make clear that it is the current topological Face ID")
for key in ("selection_title", "selection_display_id", "selection_topological_face_id"):
if key not in INFO_LABELS:
raise SystemExit(f"{key} should have a user-facing label")
relation_depths_text = _format_value(("second-level", "third-level", "deeper"))
if "second-level" not in relation_depths_text or "deeper" not in relation_depths_text:
raise SystemExit(f"relation-depth tuple should render as text: {relation_depths_text!r}")
@@ -353,9 +379,9 @@ def main() -> int:
},
"plane Face",
)
_assert_label(plane_specs, "local_face_width", "面宽")
_assert_label(plane_specs, "local_face_height", "面高")
_assert_label(plane_specs, "face_target_normal_position", "偏移")
_assert_label(plane_specs, "local_face_width", "U向尺寸")
_assert_label(plane_specs, "local_face_height", "V向尺寸")
_assert_label(plane_specs, "face_target_normal_position", "偏移曲面")
_assert_actionable_rows_first(
plane_info,
(
@@ -378,9 +404,18 @@ def main() -> int:
for fragment in ("Face 区域 1 个", "边界 Edge 4 条", "共享边相邻 Face 4 个"):
if fragment not in topology_text:
raise SystemExit(f"plane feature topology row should explain first-level counts, got {topology_spec}")
if "face_target_normal_position" not in plane_feature_keys:
expected_plane_feature_keys = {
"area",
"local_face_width",
"local_face_height",
"face_center_position",
"face_target_normal_position",
}
missing_plane_feature_keys = expected_plane_feature_keys - plane_feature_keys
if missing_plane_feature_keys:
raise SystemExit(
"plane feature mode should expose the current Face offset parameter; "
"plain plane feature mode should expose the same first-level editable Face parameters; "
f"missing {sorted(missing_plane_feature_keys)}, "
f"got {sorted(plane_feature_keys)}"
)
if "no_editable_feature_dimensions" in plane_feature_keys:
@@ -470,6 +505,12 @@ def main() -> int:
mode,
("5%", "5 倍", "会被阻止"),
)
shell_local = _scope_mode(shell_specs, "shell_thickness_estimate", "local")
if shell_local.get("label") != "拉伸/切除":
raise SystemExit(f"shell thickness local scope should expose an edit semantic, got {shell_local}")
shell_owning = _scope_mode(shell_specs, "shell_thickness_estimate", "owning")
if shell_owning.get("label") != "缩放特征":
raise SystemExit(f"shell thickness owning scope should expose an edit semantic, got {shell_owning}")
cylinder_keys = _spec_keys(
{
@@ -549,9 +590,62 @@ def main() -> int:
raise SystemExit(f"generic cylinder height should default to owning-axis resize: {generic_height}")
if generic_height.get("action") != "resize_cylindrical_height_owning_scale":
raise SystemExit(f"generic cylinder height should use owning-axis action by default: {generic_height}")
if generic_height.get("scope_text") != "调整整个特征":
if generic_height.get("scope_text") != "缩放特征":
raise SystemExit(f"generic cylinder height should show owning scope by default: {generic_height}")
split_full_cylinder_specs = _specs(
{
"surface": "cylinder",
"feature_guess": "",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 3.141592653589793,
"same_domain_angular_span": 6.283185307179586,
"is_full_cylinder": True,
"height_estimate": 11.0,
"same_domain_height_estimate": 11.0,
"area": 207.0,
"area_center": (0.0, 0.0, 5.5),
"bbox_center": (0.0, 0.0, 5.5),
"axis": (0.0, 0.0, 1.0),
"axis_point": (0.0, 0.0, 0.0),
"axis_center": (0.0, 0.0, 5.5),
}
)
split_full_height_local = _scope_mode(split_full_cylinder_specs, "cylinder_height", "local")
if not bool(split_full_height_local.get("enabled")):
raise SystemExit(
"split same-domain full cylinder should keep local height push/pull enabled: "
f"{split_full_height_local}"
)
split_full_hole_keys = _spec_keys(
{
"surface": "cylinder",
"feature_guess": "hole/groove candidate",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 3.141592653589793,
"same_domain_angular_span": 6.283185307179586,
"is_full_cylinder": True,
"height_estimate": 11.0,
"same_domain_height_estimate": 11.0,
"area": 188.0,
"area_center": (0.0, 0.0, 5.5),
"bbox_center": (0.0, 0.0, 5.5),
"axis": (0.0, 0.0, 1.0),
"axis_point": (0.0, 0.0, 0.0),
"axis_center": (0.0, 0.0, 5.5),
"slot_chord_width_estimate": 6.0,
"slot_sagitta_depth_estimate": 3.0,
"slot_arc_length_estimate": 9.42477796076938,
"feature_bottom_face_ids": (),
}
)
_assert_contains(split_full_hole_keys, {"diameter", "hole_cylinder_radius"}, "split full cylindrical hole")
if "slot_chord_width_estimate" in split_full_hole_keys:
raise SystemExit(f"split full cylindrical hole should not be shown as a slot: {split_full_hole_keys}")
slot_keys = _spec_keys(
{
"surface": "cylinder",
+252
View File
@@ -0,0 +1,252 @@
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.info_panel import InfoPanelMixin
from step_editor.ui_helpers import INFO_LABELS
from step_editor.window_state import WindowStateMixin
class _SelectionProbe(InfoPanelMixin, WindowStateMixin):
pass
class _LogicalFaceModel:
def __init__(self, logical_id: int) -> None:
self.logical_id = logical_id
def face_region_logical_id(self, _face_id: int) -> int:
return self.logical_id
def face_logical_id(self, _face_id: int) -> int:
return self.logical_id
class _FeatureContextModel(_LogicalFaceModel):
faces = [object()] * 800
face_part_ids = [1] * 800
face_solid_ids = [0] * 800
def quick_face_info(self, face_id: int) -> dict[str, object]:
return {
"kind": "face",
"face_id": face_id,
"topological_face_id": face_id,
"logical_face_id": self.logical_id,
"surface": "plane",
"part_id": 1,
"solid_id": 0,
"area": 100.0,
"area_center": (5.0, 5.0, 0.0),
"local_face_width": 10.0,
"local_face_height": 10.0,
"feature_source_face_id": face_id,
"feature_highlight_face_ids": (face_id,),
}
def cached_feature_info(self, _face_id: int) -> dict[str, object] | None:
return None
def face_first_level_topology(self, face_id: int) -> dict[str, object]:
return {
"topology_relation_depth": 1,
"topology_relation_model": "STEP/B-Rep shared-edge first-level",
"topology_relation_scope": "selected same-domain region + direct shared-edge adjacent Faces",
"topology_relation_boundary": "shared-edge",
"topology_ignored_relation_depths": ("second-level", "third-level", "deeper"),
"topology_ignored_relation_note": "当前阶段只传播一级关系;二级、三级拓扑暂不自动递归编辑。",
"same_domain_face_ids": (face_id,),
"same_domain_face_count": 1,
"same_domain_region_kind": "single-face",
"first_level_boundary_edge_ids": (1, 2, 3, 4),
"first_level_boundary_edge_count": 4,
"first_level_boundary_vertex_count": 4,
"first_level_adjacent_face_ids": (10, 11, 12, 13),
"first_level_adjacent_face_count": 4,
"first_level_face_ids": (face_id, 10, 11, 12, 13),
"first_level_face_count": 5,
"first_level_topology_note": "已识别当前 Face 区域 1 个 Face、边界 Edge 4 条、边界 Vertex 4 个、共享边一级相邻 Face 4 个。",
}
def face_first_level_facts(self, face_id: int, scope: str = "auto") -> dict[str, object]:
return {
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
"first_level_fact_status": "ready",
"first_level_fact_relation_depth": 1,
"first_level_fact_scope": scope,
"first_level_fact_relation_boundary": "shared-edge",
"first_level_fact_subject_face_ids": (face_id,),
"first_level_fact_subject_face_count": 1,
"first_level_fact_boundary_edge_count": 4,
"first_level_fact_boundary_vertex_count": 4,
"first_level_fact_adjacent_face_ids": (10, 11, 12, 13),
"first_level_fact_adjacent_face_count": 4,
"first_level_fact_included_face_count": 5,
"first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"),
"first_level_fact_summary": "一级事实=当前 Face、边界和共享边相邻 Face。",
}
def _recognition_summary_fields(self, _info: dict[str, object]) -> dict[str, object]:
return {"recognition_summary": "识别摘要包含一级事实。"}
class _FeatureRefreshProbe(WindowStateMixin):
def __init__(self) -> None:
self.model = _FeatureContextModel(594)
self.selected_kind = "feature"
self.selected_face_id = 637
self.selected_edge_id = None
self.selected_part_id = 1
self.selected_solid_id = 0
self.selected_pick_position = (1.0, 2.0, 3.0)
self.feature_detection_level = "current-only"
self.set_info_payload: dict[str, object] | None = None
self.highlighted_face_ids: tuple[int, ...] = ()
self.synced_id: tuple[str, int] | None = None
def set_info(self, info: dict[str, object]) -> None:
self.set_info_payload = dict(info)
self.current_info_values = dict(info)
def _highlight_faces(self, face_ids: list[int] | tuple[int, ...] | None = None, **_kwargs) -> None:
self.highlighted_face_ids = tuple(int(item) for item in (face_ids or ()))
def _update_selected_object_title(self) -> None:
return
def _sync_id_picker(self, kind: str, target_id: int) -> None:
self.synced_id = (kind, int(target_id))
def _probe(kind: str, selected_face_id: int, info: dict[str, object]) -> _SelectionProbe:
probe = object.__new__(_SelectionProbe)
probe.model = None
probe.selected_kind = kind
probe.selected_part_id = None
probe.selected_solid_id = None
probe.selected_face_id = selected_face_id
probe.selected_edge_id = None
probe.current_info_values = dict(info)
return probe
def _assert(condition: bool, message: str) -> None:
if not condition:
raise AssertionError(message)
def main() -> int:
for key in ("selection_title", "selection_display_id", "selection_topological_face_id"):
_assert(key in INFO_LABELS, f"{key} should have a readable UI label")
_assert(INFO_LABELS.get("face_id") == "当前拓扑 Face ID", "raw face_id should be labelled as topological")
info = {
"feature_type": "可拉伸/切除平面候选",
"selection_display_id": 594,
"selection_topological_face_id": 637,
"face_region_logical_id": 594,
"logical_face_id": 594,
}
feature_probe = _probe("feature", 637, info)
_assert(
feature_probe._selected_id_text() == "face 594",
f"feature copy ID should prefer logical Face ID, got {feature_probe._selected_id_text()!r}",
)
feature_title = feature_probe._selected_object_title_suffix()
_assert(
"Face 594" in feature_title and "拓扑 637" in feature_title,
f"feature title should show logical and topological IDs: {feature_title}",
)
face_probe = _probe("face", 637, info)
_assert(
face_probe._selected_id_text() == "face 594",
f"Face copy ID should prefer logical Face ID, got {face_probe._selected_id_text()!r}",
)
face_title = face_probe._selected_object_title_suffix()
_assert(
"Face 594" in face_title and "拓扑 637" in face_title,
f"Face title should show logical and topological IDs: {face_title}",
)
fallback_probe = _probe("feature", 637, {"face_region_logical_id": 594})
_assert(fallback_probe._selected_id_text() == "face 594", "logical Face ID fallback should be copied")
selection_only_probe = _probe("feature", 637, {"selection_display_id": 594})
_assert(
selection_only_probe._selected_id_text() == "face 594",
"selection_display_id should be enough for copied Face ID",
)
selection_only_title = selection_only_probe._selected_object_title_suffix()
_assert(
"Face 594" in selection_only_title and "拓扑 637" in selection_only_title,
f"selection_display_id should be enough for feature title: {selection_only_title}",
)
face_selection_only_probe = _probe("face", 637, {"selection_display_id": 594})
face_selection_only_title = face_selection_only_probe._selected_object_title_suffix()
_assert(
"Face 594" in face_selection_only_title and "拓扑 637" in face_selection_only_title,
f"selection_display_id should be enough for Face title: {face_selection_only_title}",
)
raw_probe = _probe("feature", 637, {})
_assert(raw_probe._selected_id_text() == "face 637", "topological Face ID fallback should still work")
helper_probe = _probe("feature", 637, {})
helper_probe.model = _LogicalFaceModel(594)
fields = helper_probe._selection_identity_fields(637, "特征来源 Face")
_assert(fields.get("selection_display_id") == 594, f"helper should expose logical Face ID: {fields}")
_assert(
fields.get("selection_topological_face_id") == 637,
f"helper should keep current topological Face ID: {fields}",
)
_assert(
fields.get("selection_title") == "特征来源 Face 594(当前拓扑 Face 637",
f"helper title should explain both IDs: {fields}",
)
refresh_probe = _FeatureRefreshProbe()
context = refresh_probe._feature_context_info(637)
_assert(context.get("selection_display_id") == 594, f"feature context should include logical ID: {context}")
_assert(
context.get("selection_topological_face_id") == 637,
f"feature context should include topological ID: {context}",
)
refresh_probe._on_feature_detection_level_changed()
payload = refresh_probe.set_info_payload or {}
_assert(payload.get("selection_display_id") == 594, f"detection refresh should call set_info with identity: {payload}")
_assert(payload.get("pick_position") == (1.0, 2.0, 3.0), f"detection refresh should preserve pick: {payload}")
_assert(refresh_probe.highlighted_face_ids == (637,), f"detection refresh should rehighlight source Face")
source_probe = _FeatureRefreshProbe()
source_probe.selected_face_id = 100
source_probe.selected_pick_position = (9.0, 9.0, 9.0)
source_probe._activate_property_source_feature(
{
"source_face_id": 637,
"source_feature_info": {
"feature_type": "可拉伸/切除平面候选",
"part_id": 1,
"solid_id": 0,
"feature_highlight_face_ids": (637,),
},
}
)
source_payload = source_probe.set_info_payload or {}
_assert(source_probe.synced_id == ("Feature", 594), f"associated jump should sync logical ID: {source_probe.synced_id}")
_assert(source_payload.get("selection_display_id") == 594, f"associated jump should keep logical ID: {source_payload}")
_assert("pick_position" not in source_payload, f"associated jump should not reuse stale pick position: {source_payload}")
print("selection identity UI ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+12 -8
View File
@@ -151,6 +151,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.edge_actor = None
self.highlight_actor = None
self.edge_highlight_actor = None
self.highlight_signature: tuple[object, ...] | None = None
self.hover_face_actor = None
self.hover_edge_actor = None
self.hover_signature: tuple[str, int] | None = None
@@ -238,6 +239,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.last_id_kind = "Feature"
self.feature_detection_level = "current-only"
self.property_editor_updating = False
self._control_state_cache: dict[int, tuple[bool, str]] = {}
self._selected_action_info_cache_key: tuple[object, ...] | None = None
self._selected_action_info_cache_value: dict[str, object] | None = None
self.property_editor_specs: list[dict[str, object]] = []
self.property_table_expanded = False
self.property_table_collapsed_rows = 6
@@ -835,7 +839,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.show_internal_edges_checkbox = QCheckBox("显示同域内部边")
help_tip(
self.show_internal_edges_checkbox,
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔拉后常见的视觉接缝线。",
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔拉伸/切除后常见的视觉接缝线。",
)
self.show_internal_edges_checkbox.toggled.connect(self._on_internal_edges_toggled)
view_layout.addWidget(self.show_internal_edges_checkbox)
@@ -981,9 +985,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
edit_layout = QGridLayout(edit_box)
edit_layout.addWidget(QLabel("移动距离"), 0, 0)
self.offset_input = QLineEdit("5.0")
help_tip(self.offset_input, "平面拉距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
help_tip(self.offset_input, "平面拉伸/切除距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
edit_layout.addWidget(self.offset_input, 0, 1)
self.push_button = QPushButton("拉平面")
self.push_button = QPushButton("伸/切除平面")
help_tip(self.push_button, "移动当前选中的平面区域:正数加料,负数切削。会先显示半透明预览,再后台执行。")
self.push_button.clicked.connect(self.push_pull_face)
edit_layout.addWidget(self.push_button, 1, 0, 1, 2)
@@ -1081,10 +1085,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(self.face_area_input, "目标面面积。当前选中对象表会使用这个隐藏输入执行所属特征或 Solid 的均匀缩放。")
self.face_width_input = QLineEdit("", edit_box)
self.face_width_input.setVisible(False)
help_tip(self.face_width_input, "目标面宽。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
help_tip(self.face_width_input, "目标U向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
self.face_height_input = QLineEdit("", edit_box)
self.face_height_input.setVisible(False)
help_tip(self.face_height_input, "目标面高。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
help_tip(self.face_height_input, "目标V向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
self.resize_boss_button.clicked.connect(self.resize_boss)
@@ -1214,14 +1218,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2)
edit_layout.addWidget(QLabel("薄壁厚度"), 20, 0)
edit_layout.addWidget(QLabel("壳体厚度"), 20, 0)
self.shell_thickness_input = QLineEdit("")
help_tip(
self.shell_thickness_input,
"薄壁/壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
"壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
)
edit_layout.addWidget(self.shell_thickness_input, 20, 1)
self.resize_shell_thickness_button = QPushButton("调整薄壁厚度")
self.resize_shell_thickness_button = QPushButton("调整壳体厚度")
help_tip(
self.resize_shell_thickness_button,
"移动当前平面区域来达到目标厚度。当前版本基于相对平面估算,执行前会预览并可回滚。",
+9
View File
@@ -42,6 +42,15 @@ SNAPSHOT_FACE_LOGICAL_IDS_KEY = "__face_logical_ids__"
FACE_SELECTION_FEATURE_INFO_SURFACES = {"plane", "cylinder", "cone", "sphere", "torus"}
FREEFORM_FACE_SURFACES = {
"bezier surface",
"b-spline surface",
"surface of revolution",
"surface of extrusion",
"offset surface",
"other surface",
}
CURVE_TYPES = {
GeomAbs_Line: "line",
GeomAbs_Circle: "circle",
+6 -6
View File
@@ -536,7 +536,7 @@ class FeatureMixin:
"confidence": item["confidence"],
"note": (
"这是完整圆柱凸台候选;点击后会选中该 face,可在当前选中对象里修改凸台高度。"
"执行时会拉识别到的端盖 Face。"
"执行时会拉伸/切除识别到的端盖 Face。"
),
}
)
@@ -681,11 +681,11 @@ class FeatureMixin:
confidence = "pending"
risk = "medium"
status = "caution"
note = "快速扫描:拉方向会在选中Face或执行编辑前再详细判断。"
note = "快速扫描:拉伸/切除方向会在选中Face或执行编辑前再详细判断。"
candidates.append(
{
"operation_key": "push_pull_plane",
"operation": "拉平面",
"operation": "伸/切除平面",
"target_kind": "face",
"target_id": face_id,
"face_id": face_id,
@@ -710,7 +710,7 @@ class FeatureMixin:
candidates.append(
{
"operation_key": "resize_shell_thickness",
"operation": "调整薄壁厚度",
"operation": "调整壳体厚度",
"target_kind": "face",
"target_id": face_id,
"face_id": face_id,
@@ -725,7 +725,7 @@ class FeatureMixin:
"confidence": shell_confidence,
"note": (
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
"执行时会移动当前平面区域来改变局部薄壁/壳体厚度。"
"执行时会移动当前平面区域来改变局部壳体厚度。"
),
}
)
@@ -2591,7 +2591,7 @@ class FeatureMixin:
if not bottom_face_ids:
return {
"context_status": "blocked",
"context_message": "当前版本的孔深调整需要疑似底面;如果自动识别失败,请手动填写底面Face ID。",
"context_message": "当前版本的孔深调整需要疑似底面;如果自动识别失败,请手动填写底面 Face ID。",
}
cyl = surf.Cylinder()
+12
View File
@@ -22,6 +22,8 @@ from .workers import EditWorker, ScanWorker
class InfoPanelMixin:
def set_info(self, info: dict[str, object]) -> None:
if hasattr(self, "_invalidate_selected_action_info_cache"):
self._invalidate_selected_action_info_cache()
self.current_info_values = dict(info)
self.current_info_text = _info_to_text(info)
self.info_text.setPlainText(self.current_info_text)
@@ -44,6 +46,8 @@ class InfoPanelMixin:
self._update_action_states()
def set_plain_info(self, text: str) -> None:
if hasattr(self, "_invalidate_selected_action_info_cache"):
self._invalidate_selected_action_info_cache()
self.current_info_values = {}
self.current_info_text = text
self.info_tree.clear()
@@ -262,6 +266,14 @@ class InfoPanelMixin:
if self.selected_kind == "solid" and self.selected_solid_id is not None:
return f"solid {self.selected_solid_id}"
if self.selected_kind in {"face", "feature"} and self.selected_face_id is not None:
for key in ("selection_display_id", "face_region_logical_id", "logical_face_id"):
value = self.current_info_values.get(key)
if value in {None, ""}:
continue
try:
return f"face {int(value)}"
except (TypeError, ValueError):
continue
return f"face {self.selected_face_id}"
if self.selected_kind == "edge" and self.selected_edge_id is not None:
return f"edge {self.selected_edge_id}"
+80
View File
@@ -8,6 +8,19 @@ from pathlib import Path
from .model import StepModel
def _optional_int(value: object) -> int | None:
if value is None or value == "":
return None
return int(value)
def _point3(value: object, operation: str) -> tuple[float, float, float]:
point = list(value) if isinstance(value, (list, tuple)) else []
if len(point) != 3:
raise ValueError(f"{operation} requires a 3D target center.")
return (float(point[0]), float(point[1]), float(point[2]))
def _execute(model: StepModel, operation: str, args: list[object]) -> str:
if operation == "push_pull_face":
return model.push_pull_face(int(args[0]), float(args[1]))
@@ -47,6 +60,73 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
return model.resize_spherical_radius(int(args[0]), float(args[1]))
if operation == "resize_torus_radius":
return model.resize_toroidal_radius(int(args[0]), float(args[1]), str(args[2]))
if operation == "resize_cylindrical_hole":
return model.resize_cylindrical_hole(int(args[0]), float(args[1]))
if operation == "resize_cylindrical_owning_scale":
return model.resize_cylindrical_owning_scale(int(args[0]), float(args[1]))
if operation == "move_cylindrical_hole_axis":
return model.move_cylindrical_hole_axis(int(args[0]), _point3(args[1], operation))
if operation == "suppress_cylindrical_hole":
return model.suppress_cylindrical_hole(int(args[0]))
if operation == "resize_cylindrical_depth":
bottom_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_depth(int(args[0]), float(args[1]), bottom_face_id=bottom_face_id)
if operation == "resize_cylindrical_depth_owning_scale":
bottom_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_depth_owning_scale(int(args[0]), float(args[1]), bottom_face_id=bottom_face_id)
if operation == "move_cylindrical_slot_axis":
return model.move_cylindrical_slot_axis(int(args[0]), _point3(args[1], operation))
if operation == "resize_cylindrical_slot_width":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_width(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_depth":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_depth(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_arc_length":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_arc_length(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_angular_span":
return model.resize_cylindrical_slot_angular_span(int(args[0]), float(args[1]))
if operation == "resize_cylindrical_slot_total_length":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_total_length(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_center_distance":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_center_distance(
int(args[0]),
float(args[1]),
pair_face_id=pair_face_id,
)
if operation == "resize_general_edge_length":
anchor_mode = str(args[2]) if len(args) > 2 else "auto"
strategy_mode = str(args[3]) if len(args) > 3 else "auto"
return model.resize_general_edge_length(
int(args[0]),
float(args[1]),
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
if operation == "move_edge_endpoint":
return model.move_edge_endpoint(int(args[0]), str(args[1]), _point3(args[2], operation))
if operation == "move_edge_center":
return model.move_edge_center(int(args[0]), _point3(args[1], operation))
if operation == "move_circular_edge_axis_center":
return model.move_circular_edge_axis_center(int(args[0]), _point3(args[1], operation))
if operation == "resize_ellipse_edge_axis_radius":
axis_kind = str(args[2]) if len(args) > 2 else "major"
return model.resize_ellipse_edge_axis_radius(int(args[0]), float(args[1]), axis_kind=axis_kind)
if operation == "resize_existing_fillet":
return model.resize_existing_fillet(int(args[0]), float(args[1]))
if operation == "fillet_edge":
return model.fillet_edge(int(args[0]), float(args[1]))
if operation == "chamfer_edge":
return model.chamfer_edge(int(args[0]), float(args[1]))
if operation == "chamfer_edge_asymmetric":
reference_face_id = _optional_int(args[3]) if len(args) > 3 else None
return model.chamfer_edge_asymmetric(int(args[0]), float(args[1]), float(args[2]), reference_face_id)
if operation == "chamfer_edge_distance_angle":
reference_face_id = _optional_int(args[3]) if len(args) > 3 else None
return model.chamfer_edge_distance_angle(int(args[0]), float(args[1]), float(args[2]), reference_face_id)
raise ValueError(f"Unsupported isolated edit operation: {operation}")
+70 -34
View File
@@ -74,7 +74,7 @@ from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
from .constants import CURVE_TYPES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
from .constants import CURVE_TYPES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
from .export import ExportMixin
from .features import FeatureMixin
from .operations import OperationMixin
@@ -352,19 +352,20 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["normal"] = _dir_tuple(direction)
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
info["push_pull_confidence"] = "unchecked"
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行拉时会重新计算。"
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
if cap_direction is not None:
info.update(cap_direction)
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行拉伸/切除时会重新计算。"
# Keep ordinary selection cheap. Cylindrical-cap direction can
# scan adjacent surfaces and cost hundreds of ms on large STEP
# files; the full push/pull plan recomputes it when the user
# actually edits.
info["push_pull_status"] = "candidate"
info["feature_type"] = "拉平面候选"
info["feature_type"] = "可拉伸/切除平面候选"
info["feature_source_face_id"] = face_id
info["feature_highlight_face_ids"] = (face_id,)
info["feature_edit_actions"] = "拉平面"
info["feature_edit_actions"] = "伸/切除平面"
if bool(info.get("has_inner_boundaries")):
info["local_face_deform_ready"] = False
info["local_face_deform_face_count"] = 1
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用推拉当前面、孔或槽的专门修改入口。"
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用拉伸/切除、孔或槽的专门修改入口。"
else:
info["local_face_deform_ready"] = True
info["local_face_deform_face_count"] = 1
@@ -430,6 +431,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["minor_radius"] = torus.MinorRadius()
info["feature_torus_major_radius"] = torus.MajorRadius()
info["feature_torus_minor_radius"] = torus.MinorRadius()
elif surface_label in FREEFORM_FACE_SURFACES:
info.update(self._freeform_face_limit_fields(surface_label))
info.update(self._recognition_summary_fields(info))
self._quick_face_info_cache[face_id] = dict(info)
@@ -528,10 +531,33 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["axis"] = _dir_tuple(torus.Axis().Direction())
info["major_radius"] = torus.MajorRadius()
info["minor_radius"] = torus.MinorRadius()
elif str(info.get("surface") or "") in FREEFORM_FACE_SURFACES:
info.update(self._freeform_face_limit_fields(str(info.get("surface") or "")))
info.update(self._recognition_summary_fields(info))
self._face_info_cache[face_id] = dict(info)
return dict(info)
def _freeform_face_limit_fields(self, surface: str) -> dict[str, object]:
blocker = (
"当前 Face 是自由曲面/非基础解析曲面;STEP 里通常没有可直接修改的历史参数,"
"当前一级阶段不开放面积、尺寸、半径或偏移类参数化修改。"
"请先保留为只读诊断,后续需要专门的曲面控制点或曲面替换语义。"
)
return {
"feature_type": "自由曲面 Face(暂不支持参数化编辑)",
"feature_edit_actions": "只读诊断;不开放参数化修改",
"freeform_face_status": "blocked",
"freeform_face_risk": "blocked",
"freeform_face_blockers": blocker,
"local_face_deform_ready": False,
"local_face_deform_face_count": 1,
"local_face_deform_blocker": blocker,
"recognition_confidence": "low",
"recognition_risk": "blocked",
"recognition_blockers": blocker,
"note": f"{surface} 当前按自由曲面处理,不会伪装成平面、圆柱、圆锥、球面或环面参数。",
}
def face_surface_kind(self, face_id: int) -> str:
if face_id < 0 or face_id >= len(self.faces):
return ""
@@ -588,12 +614,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
confidence = "high"
elif surface == "torus" and _float_or_none(info.get("major_radius")) is not None and _float_or_none(info.get("minor_radius")) is not None:
confidence = "high"
if surface in FREEFORM_FACE_SURFACES:
confidence = "low"
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
capability_specs = (
("resize_status", "resize_risk", "resize_blockers", "孔/槽/圆柱直径"),
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面"),
("shell_status", "shell_risk", "shell_blockers", "薄壁厚度"),
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面拉伸/切除"),
("shell_status", "shell_risk", "shell_blockers", "壳体厚度"),
("cylinder_resize_status", "cylinder_resize_risk", "cylinder_resize_blockers", "圆柱直径/半径"),
("boss_resize_status", "boss_resize_risk", "boss_resize_blockers", "圆柱凸台直径/高度/轴心"),
("depth_status", "depth_risk", "depth_blockers", "盲孔/盲槽深度"),
@@ -611,8 +639,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
has_planar_push_pull_candidate = (
surface == "plane"
and (
"" in feature_type_text
or "" in feature_actions_text
"伸/切除" in feature_type_text
or "伸/切除" in feature_actions_text
or str(info.get("push_pull_status") or "").strip() == "candidate"
)
)
@@ -632,10 +660,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
for key in (
"risk",
"first_level_topology_risk",
"freeform_face_risk",
):
value = str(info.get(key) or "").strip()
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
risk = value
if surface in FREEFORM_FACE_SURFACES:
risk = "blocked"
for status_key, risk_key, _blocker_key, _label in capability_specs:
value = str(info.get(risk_key) or "").strip()
if value == "blocked" and has_available_capability:
@@ -696,11 +727,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
bottom_count = len(tuple(info.get("feature_bottom_face_ids") or ()))
except TypeError:
bottom_count = 0
add("bottom_faces", f"疑似底面Face={bottom_count}")
add("bottom_faces", f"疑似底面 Face={bottom_count}")
if info.get("slot_status") == "candidate":
add("slot_geometry", "部分圆柱槽/半孔几何")
if info.get("shell_region_status") == "candidate":
add("shell_opposite_face", "找到相对平面/薄壁候选")
add("shell_opposite_face", "找到相对平面/壳体候选")
ready_actions: list[str] = []
limited_actions: list[str] = []
@@ -724,13 +755,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
add_action(limited_actions, label)
if has_planar_push_pull_candidate:
add_action(ready_actions, "平面")
add_action(ready_actions, "平面拉伸/切除")
if has_local_face_deform:
add_action(ready_actions, "当前面面积/面宽/面高/中心/偏移")
add_action(ready_actions, "当前面面积/U向尺寸/V向尺寸/中心/偏移")
elif str(info.get("local_face_deform_blocker") or "").strip():
add_action(limited_actions, "当前面局部尺寸/中心/偏移")
add_action(limited_actions, "局部重建尺寸/中心/偏移")
if has_shell_candidate:
add_action(ready_actions, "薄壁厚度")
add_action(ready_actions, "壳体厚度")
if has_slot_candidate:
add_action(ready_actions, "槽/半孔宽度/深度/弧长")
if surface == "cone" and feature_type_text:
@@ -1079,25 +1110,25 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
shell_info = self._planar_shell_region_info(face_id, coplanar_face_ids, info)
prismatic_info = self._planar_rectangular_profile_info(face_id, coplanar_face_ids, info, shell_info)
if len(coplanar_face_ids) > 1:
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face拉时会作为同一片平面区域处理。"
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face,拉伸/切除时会作为同一片平面区域处理。"
else:
scope_note = "当前 face 没有检测到可一起拉的共面相接/重叠邻居。"
edit_actions = "拉平面"
scope_note = "当前 face 没有检测到可一起拉伸/切除的共面相接/重叠邻居。"
edit_actions = "伸/切除平面"
if shell_info.get("shell_region_status") == "candidate":
edit_actions += ";调整薄壁/壳体厚度"
edit_actions += ";调整壳体厚度"
if prismatic_info.get("prismatic_profile_status") == "candidate":
edit_actions = "调整规则矩形轮廓长度/宽度"
if prismatic_info.get("prismatic_extrusion_status") == "candidate":
edit_actions += ";调整棱柱高度/凹槽深度"
else:
edit_actions += ";沿法向"
edit_actions += ";沿法向拉伸/切除"
highlight_face_ids = set(coplanar_face_ids)
highlight_face_ids.update(_int_values(prismatic_info.get("prismatic_highlight_face_ids")))
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "拉平面候选",
"feature_type": "可拉伸/切除平面候选",
"feature_source_face_id": face_id,
"feature_face_ids": tuple(coplanar_face_ids),
"feature_highlight_face_ids": tuple(coplanar_face_ids),
@@ -1363,7 +1394,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
except Exception:
return {
"shell_region_status": "not-detected",
"shell_region_note": "无法读取当前平面,不能估算薄壁/壳体区域。",
"shell_region_note": "无法读取当前平面,不能估算壳体区域。",
}
if source_surf.GetType() != GeomAbs_Plane:
return {}
@@ -1382,7 +1413,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if source_interval is None:
return {
"shell_region_status": "not-detected",
"shell_region_note": "当前平面区域缺少稳定投影范围,不能估算薄壁/壳体厚度。",
"shell_region_note": "当前平面区域缺少稳定投影范围,不能估算壳体厚度。",
}
def interval_length(interval: tuple[float, float]) -> float:
@@ -1440,7 +1471,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if best is None:
return {
"shell_region_status": "not-detected",
"shell_region_note": "未找到与当前平面投影重叠的相对平面;暂不能估算局部壳体/薄壁厚度。",
"shell_region_note": "未找到与当前平面投影重叠的相对平面;暂不能估算局部壳体厚度。",
}
thickness = float(best["shell_thickness_estimate"])
@@ -1454,7 +1485,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"shell_opposite_face_id": best["shell_opposite_face_id"],
"shell_thickness_estimate": thickness,
"shell_overlap_ratio_estimate": overlap_ratio,
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按薄壁厚度处理。",
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按壳体厚度处理。",
}
thin_ratio = thickness / diagonal
if overlap_ratio >= 0.55 and thin_ratio <= 0.08:
@@ -1471,7 +1502,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"shell_source_face_ids": tuple(valid_region_ids),
**best,
"shell_note": (
"通过同一 solid 内投影重叠的相对平面估算薄壁/壳体厚度;"
"通过同一 solid 内投影重叠的相对平面估算壳体厚度;"
"这是 B-Rep 几何近似,不等同于原 CAD 壳命令参数。"
),
}
@@ -1501,18 +1532,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
opening_face_ids = end_faces["opening_face_ids"]
guess = str(info.get("feature_guess", "cylindrical face"))
has_two_axial_caps = bool(end_faces["start_end_face_ids"] and end_faces["end_end_face_ids"])
support_face_ids_for_round = sorted(set(adjacent_face_ids) - set(end_face_ids))
material_toward = str(info.get("material_toward_axis", "") or "")
material_away = str(info.get("material_away_axis", "") or "")
complete_cylinder_with_caps = combined_angular_span >= math.tau * 0.92 and has_two_axial_caps
if (
guess == "round/fillet candidate"
and has_two_axial_caps
and material_toward == "inside"
and "outside" in material_away
and (complete_cylinder_with_caps or len(support_face_ids_for_round) < 2)
):
info = dict(info)
info["feature_guess"] = "boss/outer-round candidate"
info["confidence"] = "medium"
info["note"] = "partial cylinder has material inside its axis and explicit planar caps at both ends"
info["note"] = (
"cocylindrical region has material inside its axis and explicit planar caps at both ends"
)
domain_info["feature_guess"] = info["feature_guess"]
slot_info = self._cylindrical_slot_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
fillet_info = self._cylindrical_existing_fillet_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
@@ -2261,7 +2297,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": 1,
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做“只改当前面”的局部重建。",
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做局部重建。",
}
)
solid_face_ids = [index for index, item in enumerate(self.face_solid_ids) if item == solid_id]
@@ -2273,7 +2309,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放“只改当前面”的局部重建。",
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放局部重建。",
}
)
@@ -2296,7 +2332,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放“只改当前面”",
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放局部重建",
}
)
wire_info = self._face_boundary_wire_info(face)
@@ -2305,7 +2341,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或拉等专门修改方式。",
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或拉伸/切除等专门修改方式。",
}
)
try:
+232 -161
View File
File diff suppressed because it is too large Load Diff
+56 -47
View File
@@ -18,6 +18,9 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"file",
"part_id",
"solid_id",
"selection_title",
"selection_display_id",
"selection_topological_face_id",
"logical_face_id",
"face_region_logical_id",
"topological_face_id",
@@ -392,10 +395,13 @@ INFO_LABELS = {
"file": "文件",
"part_id": "零件 ID",
"solid_id": "Solid ID",
"selection_title": "当前选中对象",
"selection_display_id": "逻辑选择 ID",
"selection_topological_face_id": "当前拓扑 Face ID",
"logical_face_id": "逻辑 Face ID",
"face_region_logical_id": "面区域逻辑 ID",
"topological_face_id": "拓扑 Face ID",
"face_id": "Face ID",
"face_id": "当前拓扑 Face ID",
"face_region_ids": "面区域 Face",
"face_region_count": "面区域 Face 数",
"edge_id": "Edge ID",
@@ -544,24 +550,24 @@ INFO_LABELS = {
"center_of_mass": "重心",
"surface_center": "表面积中心",
"area_center": "面积中心",
"local_face_width": "当前面宽",
"local_face_height": "当前面高",
"local_face_width_direction": "面宽 方向",
"local_face_height_direction": "面高 方向",
"local_face_size_center": "面宽/面高 中心",
"face_size_axis": "面宽/面高 方向",
"face_size_label": "面宽/面高 名称",
"current_face_width": "当前面宽",
"target_face_width": "目标面宽",
"current_face_height": "当前面高",
"target_face_height": "目标面高",
"current_face_size": "当前面宽/面高",
"target_face_size": "目标面宽/面高",
"face_size_delta": "面宽/面高 变化量",
"face_size_delta_ratio": "面宽/面高 变化比例",
"face_size_scale": "面宽/面高 缩放比例",
"face_size_center": "面宽/面高 缩放中心",
"face_size_axis_direction": "面宽/面高 缩放方向",
"local_face_width": "U向尺寸",
"local_face_height": "V向尺寸",
"local_face_width_direction": "U向方向",
"local_face_height_direction": "V向方向",
"local_face_size_center": "U/V 尺寸中心",
"face_size_axis": "U/V 尺寸方向",
"face_size_label": "U/V 尺寸名称",
"current_face_width": "当前 U向尺寸",
"target_face_width": "目标 U向尺寸",
"current_face_height": "当前 V向尺寸",
"target_face_height": "目标 V向尺寸",
"current_face_size": "当前 U/V 尺寸",
"target_face_size": "目标 U/V 尺寸",
"face_size_delta": "U/V 尺寸变化量",
"face_size_delta_ratio": "U/V 尺寸变化比例",
"face_size_scale": "U/V 尺寸缩放比例",
"face_size_center": "U/V 尺寸缩放中心",
"face_size_axis_direction": "U/V 尺寸缩放方向",
"owning_face_size_rebuild_mode": "所属对象尺寸重建模式",
"length_center": "长度中心",
"bbox_min": "包围盒最小点",
@@ -574,7 +580,7 @@ INFO_LABELS = {
"end_face_id": "端面 Face",
"end_face_label": "端面位置",
"end_face_plane_distance": "端面匹配距离",
"push_pull_distance": "端面推拉距离",
"push_pull_distance": "伸/切除距离",
"normal": "几何法向",
"oriented_normal": "拓扑修正法向",
"plane_origin": "平面原点",
@@ -585,37 +591,37 @@ INFO_LABELS = {
"rotation_axis": "旋转轴",
"end_face_outward_direction": "端面向外方向",
"desired_movement_vector": "目标移动向量",
"push_pull_outward_direction": "拉向外方向",
"push_pull_inward_direction": "推拉向内方向",
"push_pull_outward_direction": "向外方向",
"push_pull_inward_direction": "切除向内方向",
"push_pull_plus_side": "原始法向侧",
"push_pull_minus_side": "反向法向侧",
"push_pull_confidence": "拉方向置信度",
"push_pull_note": "拉方向说明",
"push_pull_status": "拉状态",
"push_pull_risk": "拉风险",
"push_pull_message": "拉说明",
"push_pull_scope_face_ids": "拉共面区域 Face",
"push_pull_scope_face_count": "拉共面区域 Face 数",
"push_pull_scope_area": "拉共面区域面积",
"push_pull_scope_note": "拉共面区域说明",
"shell_region_kind": "壳体/薄壁候选类型",
"shell_region_status": "壳体/薄壁识别状态",
"shell_confidence": "壳体/薄壁置信度",
"shell_thickness": "薄壁厚度",
"shell_source_face_ids": "壳体/薄壁源平面 Face",
"push_pull_confidence": "伸/切除方向置信度",
"push_pull_note": "伸/切除方向说明",
"push_pull_status": "伸/切除状态",
"push_pull_risk": "伸/切除风险",
"push_pull_message": "伸/切除说明",
"push_pull_scope_face_ids": "伸/切除共面区域 Face",
"push_pull_scope_face_count": "伸/切除共面区域 Face 数",
"push_pull_scope_area": "伸/切除共面区域面积",
"push_pull_scope_note": "伸/切除共面区域说明",
"shell_region_kind": "壳体候选类型",
"shell_region_status": "壳体识别状态",
"shell_confidence": "壳体置信度",
"shell_thickness": "壳体厚度",
"shell_source_face_ids": "壳体源平面 Face",
"shell_opposite_face_id": "相对平面 Face",
"shell_thickness_estimate": "薄壁厚度估算",
"shell_current_thickness": "当前薄壁厚度",
"shell_target_thickness": "目标薄壁厚度",
"shell_delta_thickness": "薄壁厚度变化量",
"shell_delta_ratio": "薄壁厚度变化比例",
"shell_signed_thickness": "有符号薄壁厚度",
"shell_thickness_estimate": "壳体厚度估算",
"shell_current_thickness": "当前壳体厚度",
"shell_target_thickness": "目标壳体厚度",
"shell_delta_thickness": "壳体厚度变化量",
"shell_delta_ratio": "壳体厚度变化比例",
"shell_signed_thickness": "有符号壳体厚度",
"shell_overlap_ratio_estimate": "相对平面重叠率估算",
"shell_opposite_normal_dot": "相对平面法向点积",
"shell_desired_movement_vector": "薄壁目标移动向量",
"shell_movement_alignment": "薄壁移动方向匹配度",
"shell_note": "壳体/薄壁识别说明",
"shell_region_note": "壳体/薄壁识别说明",
"shell_desired_movement_vector": "壳体目标移动向量",
"shell_movement_alignment": "壳体移动方向匹配度",
"shell_note": "壳体识别说明",
"shell_region_note": "壳体识别说明",
"u_range": "U 参数范围",
"v_range": "V 参数范围",
"first_parameter": "起始参数",
@@ -700,6 +706,9 @@ INFO_LABELS = {
"existing_fillet_arc_length_estimate": "已有圆角圆弧长度估算",
"existing_fillet_note": "已有圆角识别说明",
"feature_edit_actions": "当前可用操作",
"freeform_face_status": "自由曲面状态",
"freeform_face_risk": "自由曲面风险",
"freeform_face_blockers": "自由曲面限制",
"resize_status": "切削状态",
"resize_strategy": "编辑策略",
"edit_strategy_label": "编辑策略说明",
@@ -940,7 +949,7 @@ def _edit_quality_warnings(before_part_stats, after_part_stats) -> list[str]:
warnings.append(
"目标零件Solid数发生变化:"
f"{before_part_stats.solids} -> {after_part_stats.solids}"
"如果这是一次局部拉/孔径修改,请重点检查导出后是否仍是一体实体。"
"如果这是一次局部拉伸/切除/孔径修改,请重点检查导出后是否仍是一体实体。"
)
return warnings
File diff suppressed because it is too large Load Diff
+46 -19
View File
@@ -149,9 +149,9 @@ class WindowCoreMixin:
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)
self.interactor.SetDesiredUpdateRate(60.0)
if hasattr(self.interactor, "SetStillUpdateRate"):
self.interactor.SetStillUpdateRate(0.2)
self.interactor.SetStillUpdateRate(1.0)
light = vtk.vtkLight()
light.SetLightTypeToHeadlight()
@@ -252,7 +252,8 @@ class WindowCoreMixin:
"show_internal_edges": show_internal_edges,
}
if build_polydata:
result["model_polydata"] = new_model.build_face_polydata(deflection=deflection)
face_polydata = new_model.build_face_polydata(deflection=deflection)
result["model_polydata"] = _smooth_surface_polydata(face_polydata)
result["edge_polydata"] = new_model.build_edge_polydata(
deflection=deflection,
show_same_domain_internal_edges=show_internal_edges,
@@ -297,8 +298,8 @@ class WindowCoreMixin:
@Slot(object)
def _start_initial_load_worker(self, expected_path: Path) -> None:
# Qt/VTK/OCCT visualization objects are not safe to construct from this
# worker path in the current PySide build. Keep the old entry point as a
# main-thread fallback so stale signal paths cannot crash the process.
# worker path in the current PySide build. Keep this as a main-thread
# fallback so stale signal paths cannot crash the process.
self._run_deferred_initial_load(expected_path)
def _load_step_sync(
@@ -348,7 +349,7 @@ class WindowCoreMixin:
if polydata is None:
return None
copied = vtk.vtkPolyData()
copied.DeepCopy(polydata)
copied.ShallowCopy(polydata)
return copied
def _detach_worker_polydata_result(self, result: dict[str, object]) -> dict[str, object]:
@@ -1027,8 +1028,14 @@ class WindowCoreMixin:
self.renderer.RemoveAllViewProps()
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
self.scene_isolated = False
self.highlight_signature = None
self.model_polydata = _smooth_surface_polydata(model_polydata)
point_normals = None
try:
point_normals = model_polydata.GetPointData().GetNormals()
except Exception:
point_normals = None
self.model_polydata = model_polydata if point_normals is not None else _smooth_surface_polydata(model_polydata)
self.model_face_id_array = self.model_polydata.GetCellData().GetArray("face_id")
self.model_part_id_array = self.model_polydata.GetCellData().GetArray("part_id")
self.model_solid_id_array = self.model_polydata.GetCellData().GetArray("solid_id")
@@ -1569,7 +1576,7 @@ class WindowCoreMixin:
elif action == "resize_shell_thickness":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整薄壁厚度候选Face {target_id}")
self.statusBar().showMessage(f"已选择可调整壳体厚度候选Face {target_id}")
elif action == "suppress_cylinder":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
@@ -1601,7 +1608,7 @@ class WindowCoreMixin:
else:
self._set_selection_mode("Face")
self.select_face(int(target_id))
self.statusBar().showMessage(f"已选择可拉平面Face {target_id}")
self.statusBar().showMessage(f"已选择可拉伸/切除平面Face {target_id}")
def select_by_id(self, kind: str | None = None) -> None:
if self.model is None:
@@ -1712,18 +1719,23 @@ class WindowCoreMixin:
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
else:
highlight_face_ids = [face_id]
logical_id = int(info.get("logical_face_id", face_id))
selection_fields = self._selection_identity_fields(
face_id,
"特征来源 Face" if feature_mode else "Face",
)
logical_id = int(selection_fields["selection_display_id"])
input_info = dict(info)
input_info.update(selection_fields)
self._sync_cylindrical_edit_inputs(input_info)
self.selected_part_id = int(info["part_id"])
self.selected_solid_id = int(info["solid_id"]) if int(info["solid_id"]) >= 0 else None
self._highlight_faces(face_ids=highlight_face_ids or [face_id])
self._show_pick_marker(pick_position)
self._sync_id_picker("Feature" if feature_mode else "Face", face_id if feature_mode else logical_id)
self._sync_id_picker("Feature" if feature_mode else "Face", logical_id)
self.set_info(self._with_pick_info(input_info, pick_position))
self._update_action_states()
raw_note = f"(拓扑Face {face_id}" if not feature_mode and logical_id != face_id else ""
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源Face {face_id}"
raw_note = f"当前拓扑 Face {face_id}" if logical_id != face_id else ""
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源 Face {logical_id}{raw_note}"
self.statusBar().showMessage(self._selection_status(message, pick_position))
def select_feature(self, face_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
@@ -1733,6 +1745,9 @@ class WindowCoreMixin:
return
info = self._feature_context_info(face_id)
info["kind"] = "feature"
selection_fields = self._selection_identity_fields(face_id, "特征来源 Face")
logical_id = int(selection_fields["selection_display_id"])
info.update(selection_fields)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "feature"
self.selected_face_id = face_id
@@ -1743,11 +1758,12 @@ class WindowCoreMixin:
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids"))
self._highlight_faces(face_ids=highlight_face_ids or [face_id])
self._show_pick_marker(pick_position)
self._sync_id_picker("Feature", face_id)
self._sync_id_picker("Feature", logical_id)
self.set_info(self._with_pick_info(info, pick_position))
feature_type = str(info.get("feature_type", "特征候选"))
self._update_action_states()
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源Face {face_id}", pick_position))
raw_note = f"(当前拓扑 Face {face_id}" if logical_id != face_id else ""
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源 Face {logical_id}{raw_note}", pick_position))
def _sync_cylindrical_edit_inputs(self, info: dict[str, object]) -> None:
fillet_radius_suggestion: str | None = None
@@ -1927,8 +1943,13 @@ class WindowCoreMixin:
def _highlight_faces(self, face_ids=None, part_ids=None) -> None:
if self.model is None:
return
face_key = _int_tuple_or_none(face_ids)
part_key = _int_tuple_or_none(part_ids)
signature = ("faces", face_key, part_key)
if signature == getattr(self, "highlight_signature", None) and self.highlight_actor is not None:
return
self._clear_highlight()
polydata = self._cached_face_overlay_polydata(face_ids=face_ids, part_ids=part_ids, smooth=True)
polydata = self._cached_face_overlay_polydata(face_ids=face_key, part_ids=part_key, smooth=True)
if polydata is None:
return
mapper = vtk.vtkPolyDataMapper()
@@ -1945,12 +1966,16 @@ class WindowCoreMixin:
actor.GetProperty().SetLineWidth(2)
self._offset_overlay_actor_toward_camera(actor, scale=0.00035)
self.highlight_actor = actor
self.highlight_signature = signature
self.renderer.AddActor(actor)
self.render_window.Render()
def _highlight_edge(self, edge_id: int) -> None:
if self.model is None:
return
signature = ("edge", int(edge_id))
if signature == getattr(self, "highlight_signature", None) and self.edge_highlight_actor is not None:
return
self._clear_highlight()
polydata = self._cached_edge_overlay_polydata(edge_id)
if polydata is None:
@@ -1965,6 +1990,7 @@ class WindowCoreMixin:
actor.GetProperty().SetDiffuse(0.8)
actor.GetProperty().SetLineWidth(5)
self.edge_highlight_actor = actor
self.highlight_signature = signature
self.renderer.AddActor(actor)
self.render_window.Render()
@@ -2090,6 +2116,7 @@ class WindowCoreMixin:
if self.pick_marker_actor is not None:
self.renderer.RemoveActor(self.pick_marker_actor)
self.pick_marker_actor = None
self.highlight_signature = None
def clear_edit_preview(self, render: bool = True) -> None:
if self.edit_preview_timer is not None:
@@ -2138,7 +2165,7 @@ class WindowCoreMixin:
if polydata is None:
raise RuntimeError("当前显示网格里没有可复用的选中 Face 预览数据")
except Exception as exc:
self.statusBar().showMessage(f"拉预览不可用:{exc}")
self.statusBar().showMessage(f"伸/切除预览不可用:{exc}")
return
if distance >= 0:
color = (0.0, 0.86, 0.34)
@@ -2172,9 +2199,9 @@ class WindowCoreMixin:
if polydata is None:
polydata = self._cached_face_overlay_polydata(face_ids=[face_id], smooth=False)
if polydata is None:
raise RuntimeError("当前显示网格里没有可复用的薄壁预览数据")
raise RuntimeError("当前显示网格里没有可复用的壳体预览数据")
except Exception as exc:
self.statusBar().showMessage(f"薄壁厚度调整预览不可用:{exc}")
self.statusBar().showMessage(f"壳体厚度调整预览不可用:{exc}")
return
movement = _triple_or_none(plan.get("shell_desired_movement_vector")) if isinstance(plan, dict) else None
if movement is None and isinstance(plan, dict):
File diff suppressed because it is too large Load Diff