From 57d75541c3d359bb521c507f761877e38f6dc5de Mon Sep 17 00:00:00 2001 From: laluo <2135665716@qq.com> Date: Wed, 5 Aug 2026 18:06:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=20Edge=20=E4=B8=80?= =?UTF-8?q?=E7=BA=A7=E7=BC=96=E8=BE=91=E4=B8=8E=20CAD=20=E5=BB=BA=E6=A8=A1?= =?UTF-8?q?=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + Creo软件具备哪些建模形式.md | 311 ++++++++++++ Face一级关系专项测试说明.md | 59 +++ README.md | 171 +++---- scripts/verify_edge_edit_suite.py | 12 + scripts/verify_edge_first_level_topology.py | 153 ++++++ scripts/verify_edge_intent_specs.py | 229 +++++++++ scripts/verify_edge_length_resize.py | 74 +++ scripts/verify_edge_target_guards.py | 118 +++++ ...rify_face_feature_parameter_consistency.py | 65 ++- scripts/verify_isolated_face_edit.py | 2 +- scripts/verify_property_editor_specs.py | 232 +++++++-- step_editor/app.py | 14 +- step_editor/model.py | 299 +++++++++++- step_editor/operations.py | 447 ++++++++++++++---- step_editor/ui_helpers.py | 44 +- step_editor/window_actions.py | 128 ++--- step_editor/window_core.py | 1 + step_editor/window_state.py | 371 +++++++++++++-- 19 files changed, 2365 insertions(+), 367 deletions(-) create mode 100644 Creo软件具备哪些建模形式.md create mode 100644 Face一级关系专项测试说明.md create mode 100644 scripts/verify_edge_first_level_topology.py create mode 100644 scripts/verify_edge_intent_specs.py create mode 100644 scripts/verify_edge_target_guards.py diff --git a/.gitignore b/.gitignore index 6150b93..93e8d03 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,7 @@ assets/screenshots/ local/ +中文版Creo+4.0从入门到精通.pdf + .DS_Store Thumbs.db diff --git a/Creo软件具备哪些建模形式.md b/Creo软件具备哪些建模形式.md new file mode 100644 index 0000000..265d273 --- /dev/null +++ b/Creo软件具备哪些建模形式.md @@ -0,0 +1,311 @@ +Creo(原 Pro/ENGINEER)支持的参数化建模操作非常丰富,远不止"拉伸、倒圆"。如果按建模功能分类,大约有 **40~60 种常用特征(Feature)**。 + +下面按类别整理。 + +--- + +# 一、草绘(Sketch) + +二维参数化基础。 + +* 直线 +* 圆 +* 圆弧 +* 椭圆 +* 样条曲线(Spline) +* 点 +* 中心线 +* 构造线 +* 倒角 +* 圆角 +* 文本 +* 尺寸约束 +* 几何约束(平行、垂直、相切、重合等) + +--- + +# 二、实体基础特征(Solid Feature) + +最常用。 + +| 功能 | Creo名称 | +| ------ | ---------------------- | +| 拉伸 | Extrude | +| 旋转 | Revolve | +| 扫描 | Sweep | +| 可变截面扫描 | Variable Section Sweep | +| 螺旋扫描 | Helical Sweep | +| 混合 | Blend | +| 边界混合 | Boundary Blend | +| 放样 | Loft(Blend实现) | +| 扫掠混合 | Swept Blend | +| 壳体 | Shell | +| 厚度 | Thicken | + +--- + +# 三、布尔运算 + +* 合并(Merge) +* 切除(Cut) +* 相交(Intersect) +* 继承体(Inheritance) +* 布尔组合(Boolean) + +--- + +# 四、孔(Hole) + +Creo孔功能非常丰富。 + +例如: + +* 简单孔 +* 通孔 +* 盲孔 +* 沉头孔 +* 沉孔 +* 螺纹孔 +* 标准孔 +* 自定义孔 +* 多深度孔 + +支持: + +* ANSI +* ISO +* DIN +* JIS + +等标准。 + +--- + +# 五、圆角和倒角 + +圆角(Round) + +包括: + +* 恒定圆角 +* 可变半径圆角 +* 全圆角 +* 面圆角 +* 边圆角 +* 相切传播 + +倒角(Chamfer) + +支持: + +* D×D +* D×Angle +* Angle×D +* 顶点倒角 + +--- + +# 六、筋板(Rib) + +例如: + +* Rib +* Web +* Thin Rib + +--- + +# 七、拔模(Draft) + +包括: + +* 中性面拔模 +* 分型面拔模 +* 可变拔模 +* 分段拔模 + +--- + +# 八、镜像和阵列 + +Mirror + +支持: + +* 特征镜像 +* 几何镜像 + +Pattern + +支持: + +* 方向阵列 +* 轴阵列 +* 表阵列 +* 曲线阵列 +* 填充阵列 +* 尺寸驱动阵列 +* 引用阵列 +* 用户定义阵列 + +--- + +# 九、复制 + +* Copy Feature +* Move +* Rotate +* Scale +* Shrink + +--- + +# 十、曲面(Surface) + +Creo曲面功能非常强。 + +包括: + +* 拉伸曲面 +* 旋转曲面 +* 扫描曲面 +* 边界曲面 +* 混合曲面 +* 填充曲面(Fill) +* 偏移曲面(Offset) +* 延伸曲面(Extend) +* 修剪曲面(Trim) +* 合并曲面(Merge) +* 替换曲面(Replace) +* 删除曲面(Delete) +* 缝合曲面(Quilt) +* 相交曲面(Intersect) +* 镜像曲面 +* 复制曲面 + +--- + +# 十一、工程特征 + +* 坐标系(CSYS) +* 基准点 +* 基准轴 +* 基准面 +* 基准曲线 +* 包络体 +* 骨架模型(Skeleton) +* 发布几何(Publish Geometry) + +--- + +# 十二、曲线(Curve) + +* 投影曲线 +* 相交曲线 +* 边界曲线 +* 样条曲线 +* 方程曲线(Equation Curve) +* 螺旋曲线 +* 基准曲线 +* 偏移曲线 +* 组合曲线 + +--- + +# 十三、扫描高级功能 + +例如: + +* 普通扫描 +* 可变截面扫描 +* 沿轨迹扫描 +* 双轨扫描 +* 法向扫描 +* 扭转扫描 +* 导向扫描 + +--- + +# 十四、螺纹 + +* Cosmetic Thread +* 实体螺纹 +* 内螺纹 +* 外螺纹 +* 锥螺纹 + +--- + +# 十五、柔性与变形 + +* Flex +* Warp +* Bend +* Twist +* Offset Geometry +* Morph + +--- + +# 十六、钣金特征(Sheetmetal) + +* Wall +* Flange +* Bend +* Unbend +* Flat Pattern +* Corner Relief +* Rip +* Hem +* Punch +* Form + +--- + +# 十七、自由曲面(ISDX/Style) + +* Style Surface +* Boundary Curve +* Bézier 曲面 +* NURBS 曲面 +* G0/G1/G2 连续 +* 自由形状编辑 + +--- + +# 十八、参数化能力 + +Creo 的核心优势在于所有特征均可参数驱动,包括: + +* 尺寸参数(Dimension) +* 关系式(Relation) +* Family Table +* UDF(用户定义特征) +* Program(Pro/PROGRAM) +* Mapkey +* 参数(Parameter) +* 层级引用(Reference) +* 外部参数关联 + +--- + +## 与 STEP AP242 特征的对应关系 + +如果你的目标是开发类似 **Creo → STEP → 自研 CAD/CAE** 的参数化建模系统,可以重点支持以下高频特征(约占工业模型的 90% 以上): + +| Creo 操作 | STEP 表达 | 建议优先级 | +| -------------- | -------------------------- | ----- | +| 拉伸(Extrude) | Extruded Area Solid | ★★★★★ | +| 旋转(Revolve) | Revolved Area Solid | ★★★★★ | +| 扫描(Sweep) | Swept Area Solid | ★★★★★ | +| 放样(Blend/Loft) | Lofted/B-Spline Surface | ★★★★☆ | +| 孔(Hole) | B-Rep + 圆柱面 | ★★★★★ | +| 倒圆(Round) | B-Rep + 圆环面/圆柱面 | ★★★★★ | +| 倒角(Chamfer) | B-Rep + 平面 | ★★★★★ | +| 壳体(Shell) | Offset Surface + B-Rep | ★★★★☆ | +| 筋(Rib) | Extrude + Thin Feature | ★★★★☆ | +| 阵列(Pattern) | Transformation + Instances | ★★★★☆ | +| 镜像(Mirror) | Transformation | ★★★★☆ | +| 拔模(Draft) | Drafted Faces | ★★★★☆ | + +**如果按工业软件(Creo、NX、SolidWorks、CATIA)的实际使用统计,前 15~20 类参数化特征(拉伸、旋转、扫描、放样、孔、倒圆、倒角、壳体、筋、拔模、镜像、阵列、曲面等)即可覆盖绝大多数机械零件的建模需求。** diff --git a/Face一级关系专项测试说明.md b/Face一级关系专项测试说明.md new file mode 100644 index 0000000..563b9f5 --- /dev/null +++ b/Face一级关系专项测试说明.md @@ -0,0 +1,59 @@ +# Face 一级关系专项测试说明 + +这份文档用于模型工程师专项测试当前 Face 阶段能力。当前版本还不能说是完整 CAD 级 Face 参数化,但已经可以围绕 STEP/B-Rep 的一级关系做集中验证。 + +## 当前目标 + +当前阶段只处理 Face 的一级拓扑关系: + +- 当前选中 Face 或同域共面/同圆柱面区域。 +- 当前 Face 边界上的 Edge / Vertex。 +- 与当前 Face 共享边的一级相邻 Face。 + +二级、三级关系暂不自动递归传播。也就是说,相邻 Face 再连接出去的孔壁、槽底、台阶、其它特征,目前只记录为后续能力目标,不会强行一起修改。 + +## 建议重点测试 + +1. 简单平面 Face + - 测试 `面积`、`U向尺寸`、`V向尺寸`、`中心`、`偏移曲面`。 + - 分别测试 `局部重建`、`移动特征`、`缩放特征`、`拉伸/切除`。 + - 简单正方体/长方体应优先通过。 + +2. 共面碎面 + - 如果一个大平面被 STEP 拆成多个共面 Face,`拉伸/切除` 应尽量按同一片平面区域处理。 + - 修改后要观察是否仍是单 Solid,边界是否明显丢失。 + +3. 圆柱端盖 / 空心圆柱环形端面 + - 可重点测试 `偏移曲面` 的 `拉伸/切除`。 + - 简单圆柱上底面、下底面和同轴空心圆柱环形端面应能改变高度。 + - 如果完整圆柱侧面被 STEP 拆成多个同域圆柱 Face,属性表仍应按完整圆柱显示 `高度`、`直径/半径`,不能误显示成槽/半孔参数。 + +4. 默认模型里的 Face 594 + - 当前已补充回归:Face 594 的 `偏移曲面(拉伸/切除)` 从 `57.5` 改到 `90` 应能成功。 + - 该操作会重建目标端盖,旧拓扑 Face ID 可能变成新 Face,例如测试中会定位到新目标面 `Face 1805`。 + - 这是正常现象。判断是否成功应看目标偏移曲面是否达到 `90`、内边界是否保留、逻辑选中对象是否继续指向修改后的新面,而不是死盯旧 Face ID。 + - 这次修复过一个 UI 回归:属性表显示的方向和按钮执行时的快速计划方向必须一致,不能把 `normal` 和反向的 `oriented_normal` 混用。 + +5. 壳体厚度 + - 识别到相对平面时,可以测试 `壳体厚度`。 + - 当前有 `拉伸/切除` 和 `缩放特征` 两种语义,测试时要区分它们。 + +## 暂时不要当作失败的情况 + +- 修改后 Face ID 变化:STEP/B-Rep 重建后拓扑编号变化是正常的,应看逻辑 Face 是否跟到新面。 +- 复杂多内孔端盖向内切除被阻止:如果会涉及孔壁、槽底、台阶背后的二级/三级关系,当前版本可能会快速阻止,这是预期保护。 +- 自由曲面、B-spline 面、复杂壳体局部区域不能改:这些暂时不是 Face 一级稳定能力。 +- STEP 原 CAD 历史无法恢复:当前参数是 B-Rep 几何语义,不等于原 CAD 软件里的草图尺寸或建模历史参数。 + +## 测试时重点观察 + +- 软件是否卡死、闪退、长时间无法关闭。 +- 操作失败后原模型是否保持不变。 +- 修改成功后是否仍是有效 B-Rep,是否保持单 Solid。 +- 当前选中对象是否继续指向被修改后的新 Face。 +- 属性表里的当前值是否能回读到新几何。 +- 失败提示是否能说明原因,而不是只报无效 shape。 + +## 当前判断 + +Face 一级关系可以继续交给模型工程师做专项测试,但还不能宣布完全完成。若简单平面、共面碎面、简单圆柱端盖、Face 594 这类基线场景仍失败,应优先修复;若是二级/三级关系、复杂自由曲面或复杂内孔向内切除,暂时可以记录为后续能力。 diff --git a/README.md b/README.md index 95f33ee..d950a13 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ git diff --check - 用户可以做基础测量,例如把两个拾取点或对象中心设为 A/B 并计算距离。 - 程序可以识别孔、圆角、凸台、槽、壳体局部区域等候选特征。 - 识别结果会区分 `当前可改`、`当前受限修改`、`识别限制` 和 `受限能力`:比如一个通孔可以改孔径,但盲孔深度属于受限能力;一个带内孔的平面可以拉伸/切除,但“局部重建尺寸/中心/偏移”会被标成受限修改。 -- 用户可以修改可控的特征,比如 Face 的面积、U向尺寸/V向尺寸、中心、偏移曲面和壳体厚度,并通过 `影响范围` 选择拉伸/切除、局部重建、移动特征或缩放特征;还可以做孔径/半径调整、孔/槽轴心并通过 `影响范围` 选择移动局部特征或移动特征、槽宽/槽深/弧长并通过 `影响范围` 选择局部重建或整体缩放、弧角、开口角和槽孔总长度调整、盲孔/盲槽深度(局部切补或整体缩放)、壳体厚度(局部拉伸/切除或整体缩放)、凸台直径/半径/高度/轴心并通过 `影响范围` 选择局部修改或整体调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度并通过 `影响范围` 选择重建圆角或整体调整、圆锥参考半径/直径/半角(简单圆锥解析重建,嵌入式锥孔优先局部重切)、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge倒圆、Edge倒角、圆Edge半径/直径并通过 `影响范围` 选择自动/相邻圆柱/缩放所属、修改Edge长度并通过 `影响范围` 选择自动/只改当前Edge/移动端面/相邻圆柱/缩放所属,以及直线Edge起点/终点坐标。 +- 用户可以修改可控的特征,比如 Face 的面积、U向尺寸/V向尺寸、中心、偏移变换和壳体厚度,并通过 `建模意图` 选择拉伸/切除、局部重建、移动特征或缩放特征;还可以做孔径/半径调整、孔/槽轴心并通过 `建模意图` 选择移动局部特征或移动特征、槽宽/槽深/弧长并通过 `建模意图` 选择局部重建或整体缩放、弧角、开口角和槽孔总长度调整、盲孔/盲槽深度(局部切补或整体缩放)、壳体厚度(局部拉伸/切除或整体缩放)、凸台直径/半径/高度/轴心并通过 `建模意图` 选择局部修改或整体调整、普通完整圆柱高度兜底修改、已有圆角半径/圆弧长度并通过 `建模意图` 选择重建圆角或整体调整、圆锥参考半径/直径/半角(简单圆锥解析重建,嵌入式锥孔优先局部重切)、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge倒圆、Edge倒角、圆Edge半径/直径并通过 `建模意图` 选择自动/相邻圆柱/缩放所属、修改Edge长度并通过 `建模意图` 选择自动/只改当前Edge/移动端面/相邻圆柱/缩放所属,以及直线Edge起点/终点坐标。 - 用户可以撤销/重做修改,避免一步编辑做坏。 - 用户可以导出修改后的完整模型。 - 如果 STEP 文件里存在多个互不关联的零件,用户可以只导出选中的某个零件。 @@ -138,9 +138,11 @@ git diff --check 所有可修改参数都要先明确“这次修改到底代表什么”,不能只因为程序能算出一个结果就直接放行。这个原则适用于 Face、Edge、孔、槽、凸台、圆角、Solid 和整件特征,不是 Edge 专属。比如修改 Face 可能代表 Face 拉伸/切除、局部补料、局部切削、平移所属对象或整体缩放;修改孔/槽可能代表重切侧壁、移动轴心、调整端面深度或重新配对长圆槽端部;修改圆角可能代表移除旧圆角后重建;修改 Edge 可能代表只动这条边、移动相关端面,或者高风险地缩放所属几何。 -所以每个可编辑行都应尽量说明本次采用的策略、影响范围和风险;当同一个目标值有多种合理语义时,应逐步做成用户可选择的策略,而不是程序悄悄替用户决定。高风险 fallback 必须提示用户确认,不能把整体尺寸变化、局部形变、Face 拉伸/切除、切削补料、对象平移、对象缩放和特征重建混成一个简单的“修改成功”。 +所以每个可编辑行都应尽量说明本次采用的建模意图、执行影响和风险;当同一个目标值有多种合理语义时,应逐步做成用户可选择的策略,而不是程序悄悄替用户决定。高风险 fallback 必须提示用户确认,不能把整体尺寸变化、局部形变、Face 拉伸/切除、切削补料、对象平移、对象缩放和特征重建混成一个简单的“修改成功”。 -当前 Face 参数化修改已经接入隔离子进程执行,包括 `拉伸/切除`、`偏移曲面(局部重建)`、`面积(局部重建)`、`U向尺寸/V向尺寸(局部重建)`、`中心(局部重建)`、`壳体厚度(拉伸/切除)`、`面积(缩放特征)`、`U向尺寸/V向尺寸(缩放特征)`、`壳体厚度(缩放特征)`、普通圆柱高度、圆柱凸台高度,以及圆锥参考半径/直径/半角、球面半径/直径、环面主/小半径或直径:主程序先导出临时 STEP,让 helper 进程执行危险 OCCT 计算并导出结果 STEP,再由主程序加载成功结果;如果 helper 卡死、超时或崩溃,主界面进程和原模型保持不变。关闭窗口时如果正在运行隔离子进程,主程序会请求终止该子进程,避免用户被长时间计算锁住。打包后的 `main.exe` 也会通过 `--isolated-edit-worker` 进入同一条 worker 通道,避免子进程误打开第二个 GUI。无法识别为简单圆锥或锥孔/沉孔的复杂圆锥/拔模面参考半径、参考直径和半角暂不开放,不再退回整体径向缩放;复杂浅锥/拔模面会在计划阶段快速阻止,避免界面线程长时间等待或生成无效 B-Rep。仍未归类到明确 Face、孔槽或 Edge 语义的复杂 fallback 会继续被稳定性保护阻止,后续按特征逐步开放。 +Creo 资料的参考价值不只是统一术语,还要用来组织功能入口和操作流程。界面不应优先暴露底层 B-Rep 字段,而应先把选中对象映射到 CAD 用户熟悉的建模形式,例如拉伸/切除、偏移变换、孔、槽、倒圆角、倒角、抽壳、拔模、移动几何和缩放特征;只有当识别结果足够稳定,才把对应的可改尺寸放出来。 + +当前 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,但已经丢失目标圆柱/盲孔语义,会自动回滚并提示原因。 @@ -152,7 +154,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 如果轻量预检查已经能判断出风险不可接受,比如复杂大模型里的多内孔端盖向内切除已经接近切穿,或需要判断孔底、槽底、台阶背后的二级关系,界面会直接阻止并说明原因,不再提交隔离子进程等待失败返回。浅范围内的多内孔端盖向内收缩会走后台/隔离子进程里的一级边界侧壁重建。 -复杂大 STEP 中带内孔/内边界的平面 Face,如果用户选择 `面积(局部重建)`、`U向尺寸/V向尺寸(局部重建)`、`中心(局部重建)` 或 `偏移曲面(局部重建)` 这类“只改当前 Face 顶点并重建相邻面”的局部变形语义,UI 会在轻量预检查阶段直接阻止并说明原因,不再先进入完整局部重建计划。`壳体厚度(拉伸/切除)` 在这类复杂面上会先标记为后台高风险计划,把相对面识别和拉伸/切除距离计算放进隔离进程,避免主界面等待复杂拓扑扫描。 +复杂大 STEP 中带内孔/内边界的平面 Face,如果用户选择 `面积(局部重建)`、`U向尺寸/V向尺寸(局部重建)`、`中心(局部重建)` 或 `偏移变换(局部重建)` 这类“只改当前 Face 顶点并重建相邻面”的局部变形语义,UI 会在轻量预检查阶段直接阻止并说明原因,不再先进入完整局部重建计划。`壳体厚度(拉伸/切除)` 在这类复杂面上会先标记为后台高风险计划,把相对面识别和拉伸/切除距离计算放进隔离进程,避免主界面等待复杂拓扑扫描。 普通平面 Face 的核心修改路径现在还会在模型层做结果守门:执行后必须通过 B-Rep 校验、能重新找到目标 Face 的目标值,并且能重新识别该 Face 的一级共享边邻域。矩形、多边形 Face 会检查边界 Edge 和 Vertex;圆形端盖这类解析边界会允许 1 条闭合圆 Edge。圆锥参考半径/半角、球面半径、环面半径这类解析 Face 也会强制校验目标值和 B-Rep。若目标值没达到、一级邻域丢失或 B-Rep 无效,模型层会恢复操作前 shape,不把坏结果交给界面或导出流程。 @@ -168,7 +170,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 设计上优先把选择做成当前选中对象里的清晰属性行或策略控件,而不是弹出一堆工程术语。每个可选策略都要用用户能看懂的话说明结果,例如“只改当前边,邻面会变斜”“移动端面,整体变成长方体”“缩放所属对象,所有尺寸同比例变化”“重切孔壁,只改孔径”。默认策略可以存在,但必须可见、可解释、可切换。 -界面里的 `当前选中对象` 会优先显示 `编辑方式` 行,用来告诉你当前对象的修改会走哪类几何语义。比如孔径是同轴重切,槽/半孔是局部扇形槽或配对长圆槽重建,凸台是包络重建或端盖拉伸/切除,已有圆角是先移除旧圆角再重建。对于圆柱直径/半径,当前也开始提供独立的 `缩放所属对象` 行,让你明确选择“重切/重建局部特征”还是“按目标尺寸比例均匀缩放所属特征或 Solid”。 +界面里的 `当前选中对象` 会优先显示 `建模形式` 和 `推荐操作` 行:前者告诉你当前对象更像哪类 Creo/CAD 建模入口,后者告诉你现在最适合从哪个参数或策略开始改。比如孔径是孔特征的同轴重切,槽/半孔是槽特征的局部扇形槽或配对长圆槽重建,凸台是包络重建或端盖拉伸/切除,已有圆角是先移除旧圆角再重建。对于圆柱直径/半径,当前也开始提供独立的 `缩放所属对象` 行,让你明确选择“重切/重建局部特征”还是“按目标尺寸比例均匀缩放所属特征或 Solid”。 ## 开源复用原则 @@ -199,7 +201,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 | 对象 | 现在能改哪些 | 程序大概怎么改 | 暂时还不能稳定改 | | --- | --- | --- | --- | | Edge / 边 | 直线边的长度、起点、中心、终点;直线边新增圆角/倒角;圆形或圆弧边的半径/直径、圆心/相邻轴心;椭圆边的主半径/小半径。 | 简单模型优先局部重建当前边和相邻面;圆边可复用相邻圆柱改孔/槽/凸台直径或轴心;椭圆边可沿主轴或小轴单轴缩放所属特征。 | 任意复杂曲线边、B-spline 边、需要复杂约束跟随的边。 | -| Face / 面 | 面积、U向尺寸/V向尺寸、中心、偏移曲面、壳体厚度;圆柱端盖或完整圆柱侧面的高度。 | 拉伸/切除当前平面或圆柱端盖;移动当前面顶点并重建相邻面;或平移/缩放所属特征。 | 自由曲面会明确识别为只读,不开放伪参数化修改;带复杂内孔的面、复杂壳体局部区域、非完整圆柱侧面高度。 | +| Face / 面 | 面积、U向尺寸/V向尺寸、中心、偏移变换、壳体厚度;圆柱端盖或完整圆柱侧面的高度。 | 拉伸/切除当前平面或圆柱端盖;移动当前面顶点并重建相邻面;或平移/缩放所属特征。 | 自由曲面会明确识别为只读,不开放伪参数化修改;带复杂内孔的面、复杂壳体局部区域、非完整圆柱侧面高度。 | | 孔 / 圆柱孔 | 规则孔的直径/半径、完整孔轴心、完整孔封堵、可识别盲孔/盲槽深度。 | 改孔径时同轴重切孔壁;移动孔时先补旧孔再切新孔;改深度时切削或补料孔底。 | 螺纹孔、锥孔、复杂孔组、底面识别不可靠的盲孔。 | | 槽 / 半孔 | 槽宽、槽深、弧长、弧角、开口角、简单轴心、长圆槽总长度/中心距。 | 按圆柱槽或长圆槽胶囊区域,先补旧槽,再切出新槽。 | 复杂草图槽、非圆柱槽、多槽联动、跨复杂面的槽。 | | 凸台 / 外圆 | 圆柱凸台的直径/半径、高度、轴心。 | 移除旧凸台包络后重建;或拉伸/切除端盖、轴向缩放。 | 异形凸台、多台阶凸台、复杂相邻拓扑。 | @@ -212,12 +214,12 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 | 对象 / 特征 | 现在可以修改 | 暂不能保证或还不能修改 | | --- | --- | --- | -| Face | 平面 Face 可以改 `面积`、`U向尺寸`、`V向尺寸`、`中心` 和 `偏移曲面`;这些行会在 `影响范围` 里选择 `局部重建`、`拉伸/切除`、`移动特征` 或 `缩放特征`。识别到相对平面时还可以改 `壳体厚度`,并通过 `影响范围` 选择 `拉伸/切除` 或 `缩放特征`。圆柱上/下端盖、同轴空心圆柱的环形端面可以通过 `拉伸/切除` 改高度;选中完整圆柱侧面时,也可以把 `高度` 换算成端盖拉伸/切除,或者选择沿轴向 `缩放特征`。简单端盖会解析重建,复杂同轴筒体会优先只处理旧端盖到新端盖之间的局部段,端盖边界上有槽口/缺口时会按真实端盖轮廓向外拉伸或在安全范围内向内切削。 | `U向尺寸/V向尺寸` 是这个 Face 自身平面里的两个方向尺寸,不是面积,也不是模型高度;`偏移曲面` 是沿当前面垂直方向测到的位置,不再单独显示容易混淆的额外的“本次移动量”提示。`拉伸/切除` 会加料或切削;`局部重建` 会移动当前 Face 顶点并重建相邻平面,相邻面可能变斜;`移动特征` 会平移所属特征或 Solid;`缩放特征` 会缩放所属特征或 Solid 并影响其它尺寸;自由曲面会显示为只读受限对象,不把面积、中心、包围盒或底层 UV 参数伪装成可修改尺寸;复杂壳体局部区域、非完整圆柱侧面、非同轴多孔环形端面、端盖边界本身连着槽/缺口/台阶且向内切除越过槽底/台阶终点的面和其它带复杂边界的面仍不能稳定做局部尺寸编辑,界面会禁用或快速阻止并在提示里说明具体原因,建议改用孔/槽专门入口、`拉伸/切除` 的简单端盖路径或整体调整。 | -| 圆柱孔 / 圆柱面 | 孔或圆柱的 `直径`、`半径`,并通过 `影响范围` 选择 `只改孔/槽壁` 或 `缩放特征`;完整圆柱孔可以改轴心坐标;完整孔可以尝试封堵。普通孔径语义是同轴圆柱重切,孔轴心语义是先填旧孔再切新孔。 | 复杂孔、螺纹孔、锥孔、不完整圆柱孔和拓扑不稳定的孔可能失败并回滚;`缩放特征` 会影响同一对象上的高度、厚度和其它尺寸,不适合只想改孔壁的场景。 | -| 盲孔 / 盲槽 | 识别到疑似底面 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 历史参数,复杂相邻拓扑可能失败或回滚。 | | Solid / 特征整体 | 可以对选中 Solid 或当前单一模型做平移、旋转和按包围盒尺寸 / 体积 / 表面积的缩放类修改。 | 还没有装配约束、零件间关联约束或完整多零件装配编辑。 | @@ -261,6 +263,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 - 圆角/倒圆候选会额外显示已有圆角半径估算、圆弧角度、圆弧长度和相邻支撑Face。 - 属性面板会统一显示 `识别摘要`、`识别候选`、`识别置信度`、`识别风险`、`识别评分`、`识别结论`、`识别依据`、`识别限制` 和 `受限能力`。这些字段来自当前已知的 B-Rep 几何事实、一级拓扑关系、同域区域、材料采样和阻止原因,用来说明程序为什么把对象当作某类候选,而不是把猜测隐藏在代码里。`识别限制` 表示这个候选整体不能稳定编辑;`受限能力` 表示对象本身可识别、某些修改可用,但另一些子能力暂不开放。低置信解析曲面不会开放可编辑按钮,避免把“底层曲面类型像圆锥/球/环面”误当成稳定 CAD 设计参数。 - 模型层提供统一的一级拓扑事实包 `face_first_level_facts()`:它把当前主体 Face 区域、边界 Edge、边界 Vertex、共享边直接相邻 Face、角色分组和暂不传播的二/三级关系整理成同一份事实证据。当前 Face、孔、槽等编辑计划也会携带这份事实包;`只改当前 Face` 这类局部重建计划会在执行前先检查一级事实图是否 ready,若无法确认当前 Face、边界 Edge/Vertex 和共享边相邻 Face,会直接 blocked 并说明原因。Face 编辑成功后还会重建目标 Face 的一级事实图做结果校验,若目标值虽然达到但一级事实图不再成立,会自动回滚。后续孔、槽、凸台、圆角等候选识别应优先使用这份事实包,再叠加各自的语义判断和置信度。 +- Edge 阶段新增统一的一级拓扑事实包 `edge_first_level_topology()` / `edge_first_level_facts()`:它把当前 Edge、端点 Vertex、共享端点的相邻 Edge、直接包含该 Edge 的相邻 Face、角色分组和暂不传播的二/三级关系整理成同一份事实证据。当前 Edge 长度、起点/中心/终点坐标、圆角、倒角、圆边轴心和椭圆半径等编辑计划会携带这份事实包,后续 Edge 一级策略应优先用它判断“只改当前边、移动端面、相邻圆柱或缩放所属对象”的执行影响。 - 支持显示选中对象信息,并提供 `属性表` / `原始文本` 两种查看方式: - 零件名称 - Solid 数量 @@ -378,7 +381,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 - 输入目标厚度后,程序会把厚度变化换算成当前平面区域的拉伸/切除距离,保留相对平面不动。 - 会先显示半透明拉伸/切除预览,并记录当前厚度、目标厚度、相对平面Face、重叠率和拉伸/切除距离。 - 这是局部 B-Rep 平面拉伸/切除近似,不是 CAD 壳命令或完整壳体参数编辑。 - - 当前选中对象表的 `壳体厚度` 会通过 `影响范围` 选择 `拉伸/切除` 或 `缩放特征`;后者会沿厚度方向缩放所属特征或 Solid,让厚度接近目标值,同一对象上的其它厚度方向尺寸也会跟随变化。 + - 当前选中对象表的 `壳体厚度` 会通过 `建模意图` 选择 `拉伸/切除` 或 `缩放特征`;后者会沿厚度方向缩放所属特征或 Solid,让厚度接近目标值,同一对象上的其它厚度方向尺寸也会跟随变化。 - 对简单全平面薄板/盒体,`缩放特征` 会优先用平面重建路线,避免把原本的平面 Face 变成 B-spline 曲面,并保持原选中 Face 的逻辑 ID 继续指向修改后的面。 - 当前版本支持已有圆角/倒圆候选结构化识别: - 对局部小半径圆柱面,会标记为 `round/fillet candidate`。 @@ -387,7 +390,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 - `可编辑对象` 会把已有圆角候选单独列为 `修改已有圆角半径`,点击后会选中该圆角 Face,并把圆角半径输入框预填为当前估算半径的参考目标值。 - 选中已有圆角候选时,不会再误把它填入 `孔直径`,避免把圆角当孔径修改。 - 当前版本的已有圆角半径修改会先尝试移除圆角Face,再在恢复出的锐边上重新倒圆;也可以在当前选中对象表中输入已有圆角圆弧长度,程序会按当前圆弧角度换算成目标半径。只对较规整的圆柱圆角候选开放,复杂 blend 可能失败并回滚。 - - 当前选中对象表的 `圆角半径` / `圆角弧长` 会通过 `影响范围` 选择 `重建圆角` 或 `缩放特征`。后者不会移除并重建圆角,而是按目标半径或弧长比例整体缩放所属特征或 Solid,同一对象上的其它尺寸也会变化。 + - 当前选中对象表的 `圆角半径` / `圆角弧长` 会通过 `建模意图` 选择 `重建圆角` 或 `缩放特征`。后者不会移除并重建圆角,而是按目标半径或弧长比例整体缩放所属特征或 Solid,同一对象上的其它尺寸也会变化。 - 当前版本支持圆柱孔封堵: - 只对 `hole/groove candidate` 且角度跨度接近完整圆柱的孔开放。 - 不对半孔/槽开放,避免用完整圆柱补料时把不该补的位置也补上。 @@ -399,7 +402,7 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 - 不处理局部外圆角、圆角面或未明确圆柱面,避免把圆角当凸台修改。 - 目标直径变大时,会在同域圆柱侧壁范围内生成目标直径圆柱并 Fuse。 - 目标直径变小时,会先用旧外形包络体移除原凸台范围,再 Fuse 目标直径圆柱重建,避免环形 cutter 在部分场景下把实体切空。 - - 当前选中对象表的 `直径` / `半径` 会通过 `影响范围` 选择 `只改凸台` 或 `缩放特征`;后者用于按目标尺寸比例均匀缩放所属特征或 Solid,不是凸台包络重建,会连带影响其它尺寸。 + - 当前选中对象表的 `直径` / `半径` 会通过 `建模意图` 选择 `只改凸台` 或 `缩放特征`;后者用于按目标尺寸比例均匀缩放所属特征或 Solid,不是凸台包络重建,会连带影响其它尺寸。 - 会先显示半透明预览:绿色表示扩大/重建补料范围,红色表示缩小移除范围。 - 当前默认模型可能没有明确凸台候选;包含完整圆柱凸台的 STEP 文件可以使用这个能力。 - 当前版本支持圆柱凸台高度和轴心坐标调整: @@ -423,15 +426,15 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 - STEP 没有建模历史,某些边仍可能被 OCCT 拒绝;失败时会自动尝试恢复编辑前状态。 - 当前版本支持修改Edge长度: - 选中Edge后,`长度` 会自动填入当前Edge长度。 - - 对圆形/圆弧Edge,当前选中对象表会额外提供圆边半径和圆边直径可编辑行,内部会换算为等比例Edge长度,并通过 `影响范围` 选择自动、相邻圆柱或缩放所属。 + - 对圆形/圆弧Edge,当前选中对象表会额外提供圆边半径和圆边直径可编辑行,内部会换算为等比例Edge长度,并通过 `建模意图` 选择自动、相邻圆柱或缩放所属。 - `长度基准` 行可以选择自动、中心、固定起点、固定终点。 - - `长度` 行的 `影响范围` 可以选择 `自动`、`只改当前Edge`、`移动端面`、`相邻圆柱`、`缩放所属`。 - - `只变当前Edge` 会强制使用局部边形变;如果当前Edge不满足简单全平面多面体条件,会直接阻止,不会偷偷改成整体缩放。 + - `长度` 行的 `建模意图` 可以选择 `自动`、`只改当前Edge`、`移动端面`、`相邻圆柱`、`缩放所属`。 + - `只改当前Edge` 会强制使用局部边形变;如果当前Edge不满足简单全平面多面体条件,会直接阻止,不会偷偷改成整体缩放。 - `移动端面` 会强制找端点附近、方向匹配的平面端面并做端面拉伸/切除;对正方体这类模型,会得到更像“变成长方体”的结果。如果找不到端面拉伸/切除路径,会直接阻止。 - `缩放所属` 会强制对所属特征或 Solid 做轴向/整体缩放,是高风险兜底语义,会影响其它尺寸。 - 对直线Edge,当前选中对象表会额外提供 `起点`、`中心` 和 `终点`,可以直接输入目标 X/Y/Z 坐标来移动单个端点或整条边。 - 对正方体这类全平面多面体,直线Edge会优先走“局部边形变”:只移动被选Edge的一个端点或两个端点,并重建周边平面。 - - 局部边形变不会把整个端面拉走,因此正方体不会默认变成长方体;相邻面会按被移动的边自然变成梯形或拆成三角面。 + - 局部边形变不会把整个端面拉走,因此正方体不会默认变成长方体;相邻面会按被移动的边自然变形,四点非平面面会优先重建成连续曲面,避免从面中间出现对角折线。 - 直线Edge端点/中心坐标修改复用同一套局部边形变;当前只对简单全平面多面体开放,复杂曲面、带内孔Face或大型复杂Solid会阻止执行。 - `自动` 默认固定起点、移动终点;`固定起点` / `固定终点` 会明确控制哪一端不动;`中心` 会让两端各移动一半。 - 如果模型不是简单全平面多面体,直线Edge会继续尝试寻找端点附近、方向匹配的平面端面,并把“改边长”转换成该端面的平面拉伸/切除操作。 @@ -462,11 +465,11 @@ Face 编辑失败、超时或被稳定性保护阻止时,弹窗不应只显示 - 如果后台编辑失败,程序会自动尝试恢复到编辑前快照,避免出现“提示失败但模型已经被部分改动”的状态。 - 当前版本支持圆柱孔/圆柱面扩大切削。 - 当前版本支持圆柱孔/圆柱面缩小孔径:先补料,再用目标直径重切。 -- 当前版本支持完整圆柱孔轴心坐标修改:在当前选中对象表里修改 `轴心`,并把 `影响范围` 设为 `移动孔` 时,程序会先补旧孔,再按同直径在目标轴心位置重切新孔;半孔/开放槽会走 `移动槽/半孔` 语义,避免整圆柱工具误补材料。把 `影响范围` 设为 `移动特征` 时,会平移所属特征或 Solid。 -- 当前版本支持孔/槽 `直径` / `半径` 的 `缩放特征` 影响范围: +- 当前版本支持完整圆柱孔轴心坐标修改:在当前选中对象表里修改 `轴心`,并把 `建模意图` 设为 `移动孔` 时,程序会先补旧孔,再按同直径在目标轴心位置重切新孔;半孔/开放槽会走 `移动槽/半孔` 语义,避免整圆柱工具误补材料。把 `建模意图` 设为 `移动特征` 时,会平移所属特征或 Solid。 +- 当前版本支持孔/槽 `直径` / `半径` 的 `缩放特征` 建模意图: - 这条路线不会重切孔壁,而是按目标直径或半径与当前值的比例均匀缩放所属特征或 Solid。 - 高度、厚度和同一对象上的其它尺寸会同比例变化。 - - 如果你只想让孔径变大/变小,优先在 `直径` 或 `半径` 行把 `影响范围` 设为 `只改孔/槽壁`;如果你的建模意图是让所属圆柱区域整体缩放,再设为 `缩放特征`。 + - 如果你只想让孔径变大/变小,优先在 `直径` 或 `半径` 行把 `建模意图` 设为 `只改孔/槽壁`;如果你的建模意图是让所属圆柱区域整体缩放,再设为 `缩放特征`。 - 圆柱孔/圆柱面孔径调整现在会先生成切削计划: - 目标直径必须大于 0;如果目标直径与当前直径几乎相同,会直接阻止。 - 会计算直径变化比例;变化超过当前直径 35% 会提高到中风险,超过 100% 会提高到高风险。 @@ -526,7 +529,7 @@ vertices: 3262 - 复杂特征识别:已经有平面、圆柱、边、孔/槽候选、槽/半孔候选、已有圆角候选、圆柱凸台候选等基础候选列表,但还不是完整 CAD 语义级识别。 - 盲孔/盲槽深度:已经能对部分可识别候选做深度估算和编辑,并会优先使用疑似底面 Face的轴向位置计算当前深度;但还不能保证所有模型都能恢复真实 CAD 语义深度。 -- 槽、凸台、壳体局部区域:已经有圆柱孔、槽/半孔候选结构化识别、槽/半孔宽度/深度/圆弧长度/圆弧角度/开口角度调整、槽宽/槽深/弧长的整体缩放所属对象语义、长圆槽总长度/中心距执行时自动配对和手动配对端 Face ID、完整圆柱凸台直径/半径/高度/轴心坐标修改、普通完整圆柱高度兜底拉伸/切除或通过 `影响范围` 做轴向整体缩放,以及平面壳体相对面厚度估算、局部厚度拉伸/切除和通过 `影响范围` 做厚度方向整体缩放;复杂槽、复杂凸台和完整壳体区域仍需要继续做。 +- 槽、凸台、壳体局部区域:已经有圆柱孔、槽/半孔候选结构化识别、槽/半孔宽度/深度/圆弧长度/圆弧角度/开口角度调整、槽宽/槽深/弧长的整体缩放所属对象语义、长圆槽总长度/中心距执行时自动配对和手动配对端 Face ID、完整圆柱凸台直径/半径/高度/轴心坐标修改、普通完整圆柱高度兜底拉伸/切除或通过 `建模意图` 做轴向整体缩放,以及平面壳体相对面厚度估算、局部厚度拉伸/切除和通过 `建模意图` 做厚度方向整体缩放;复杂槽、复杂凸台和完整壳体区域仍需要继续做。 - 已有圆角/倒圆面半径:已经有当前版本的受限修改实现,使用 defeature + refillet 路线,但复杂 blend、支撑面不明确或恢复锐边失败时仍会阻止或回滚。 - 局部偏差分析:已经有编辑前后体积、面积、包围盒、距离热力图和差异报告,但还不是工程公差级的局部区域偏差报告。 - 错误恢复和模型修复:已经有编辑前快照、失败回滚、基础 B-Rep 校验、同域面合并、布尔结果自动修复,以及一键修复当前模型或选中零件/solid,但还不是完整的模型修复流水线。 @@ -541,9 +544,9 @@ vertices: 3262 建议下一步按这个顺序推进: -1. 先集中完成 Face 一级拓扑关系编辑:面积、U向尺寸/V向尺寸、中心、偏移曲面和壳体厚度,都要明确 `局部重建`、`拉伸/切除`、`移动特征`、`缩放特征` 等语义;当前阶段只传播当前 Face/同域碎片和共享边相邻 Face,不递归处理二级、三级关系。 -2. Face 一级关系闭环稳定后,再集中做槽/孔类特征的一级关系:槽宽、槽深、弧长、弧角、开口角、轴心、长圆槽总长度、中心距、孔径、孔轴心和盲孔/盲槽深度要统一验证。 -3. 槽/孔一级关系稳定后,再集中做 Edge 一级关系:直线 Edge 长度、起点、中心、终点和倒圆/倒角语义先做完整,再处理圆弧 Edge、椭圆 Edge 和高风险 fallback。 +1. 先集中完成 Face 一级拓扑关系编辑:面积、U向尺寸/V向尺寸、中心、偏移变换和壳体厚度,都要明确 `局部重建`、`拉伸/切除`、`移动特征`、`缩放特征` 等语义;当前阶段只传播当前 Face/同域碎片和共享边相邻 Face,不递归处理二级、三级关系。 +2. Face 一级关系闭环稳定后,先集中做 Edge 一级关系:直线 Edge 长度、起点、中心、终点和倒圆/倒角语义先做完整,再处理圆弧 Edge、椭圆 Edge 和高风险 fallback;当前 Edge 编辑计划已经开始携带统一的一级端点/相邻边/相邻 Face 事实包。 +3. Edge 一级关系稳定后,再集中做槽/孔类特征的一级关系:槽宽、槽深、弧长、弧角、开口角、轴心、长圆槽总长度、中心距、孔径、孔轴心和盲孔/盲槽深度要统一验证。 4. Edge 闭环稳定后,再集中做凸台、已有圆角、圆锥/球/环面、壳体局部区域等对象;每类都先补一级关系能力和验证,再进入下一类。 5. 把更多“同一目标值有多种合理语义”的场景做成用户可选策略。每个策略都要说明改法、固定约束、影响对象、哪些相邻几何会跟随变化。 6. 继续做性能和稳定性优化,重点是扫描分批刷新、局部显示重建缓存,以及高风险 OCC 操作隔离。 @@ -552,12 +555,12 @@ Face 阶段的当前验收口径: - 当前阶段的 `一级关系` 定义为:选中 Face 本身、必要的同域/共面碎片 Face、这些 Face 的边界 Edge/Vertex,以及与该区域共享边的直接相邻 Face。只共享顶点的对象、相邻 Face 再连出去的 Face 都不作为当前阶段的自动传播范围。 - 选中平面 Face 时,`当前选中对象` 的参数表会显示 `一级关系` 说明行,直接列出当前 Face 区域数量、边界 Edge/Vertex 数量、共享边相邻 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_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 一级关系闭环后,再进入槽/孔一级关系阶段;不要在 Face 还没有稳定解释和验证时提前分散到 Edge、圆锥/球/环面等对象。 +- 对不能稳定做 `局部重建` 的 Face,要禁用该建模意图,并在提示中说明原因;不能让复杂内孔、曲面 Solid 或复杂边界静悄悄走高风险局部变形。 +- Face 一级关系闭环后,当前先进入 Edge 一级关系阶段;Edge 的一级端点/相邻边/相邻 Face 事实包和验证稳定后,再回到槽/孔一级关系阶段。 ## 怎么使用 @@ -629,7 +632,7 @@ Face 阶段的当前验收口径: - 点击 `深度扫描` 会用更高上限重新扫描,可能需要等待更久。 - 为了避免打开大模型时界面卡住,程序不会在加载 STEP 时自动扫描;需要编辑时再手动点击扫描。 - 当前默认显示一批代表性的可编辑对象,详细几何判断会尽量延后到选中对象或执行编辑前,避免扫描过慢。 -- `拉伸/切除平面` 行表示这个Face是平面;当前主要在 `当前选中对象` 的 `偏移曲面` 行里把 `影响范围` 选为 `拉伸/切除` 来执行。 +- `拉伸/切除平面` 行表示这个Face是平面;当前主要在 `当前选中对象` 的 `偏移变换` 行里把 `建模意图` 选为 `拉伸/切除` 来执行。 - `调整圆柱孔径` 行表示这个 Face 是圆柱面候选,可以配合 `孔直径` 和 `调整圆柱孔径` 使用。 - `调整槽/半孔宽度` / `调整槽/半孔深度` / `调整槽/半孔圆弧长度` 行表示这个Face是槽/半孔候选,可以在当前选中对象表里修改槽宽、槽深或圆弧长度。 - `调整圆柱凸台直径` / `调整圆柱凸台高度` 行表示这个Face是较明确的完整圆柱凸台候选,可以在当前选中对象表里修改凸台直径、半径、高度或轴心坐标。 @@ -680,22 +683,22 @@ Face 阶段的当前验收口径: 编辑: - 编辑按钮会根据当前选择自动启用或禁用。例如未选中平面时不能点击 `拉伸/切除平面`,未选中直线Edge时不能点击 `给Edge添加圆角` / `给Edge添加倒角`,未选中明确孔/凸台/盲孔候选时对应圆柱编辑按钮会禁用。 -- `偏移曲面`: +- `偏移变换`: - 先选择一个平面 Face。 - - 在当前选中对象表里修改 `偏移曲面`,再用 `影响范围` 选择改法。 - - `偏移曲面` 是沿当前面垂直方向测到的位置,不是面积,不是移动距离,也不是 X/Y/Z 坐标。 + - 在当前选中对象表里修改 `偏移变换`,再用 `建模意图` 选择改法。 + - `偏移变换` 是沿当前面垂直方向测到的位置,不是面积,不是移动距离,也不是 X/Y/Z 坐标。 - `拉伸/切除` 会把目标位置换算成这次要移动的距离,然后沿当前面的垂直方向加料或切削,适合改变厚度、拉出凸起或切入凹槽。 - `局部重建` 会把当前 Face 移到目标位置,并重建相邻平面;相邻面可能变斜。 - `移动特征` 会沿当前面垂直方向平移所属特征或 Solid;形状和内部尺寸不变。 - 如果选中Face周围有共面且相接/重叠的碎面,`拉伸/切除` 会自动把这些Face合成同一片拉伸/切除区域,属性表和历史记录会显示 `拉伸/切除共面区域 Face 数`。 - 当前拉伸/切除方向判断通过Solid的 inside / outside 采样得到;如果采样不明确,会退回到拓扑方向法向,并在属性里显示低置信度说明。 - `面积` / `U向尺寸` / `V向尺寸`: - - 先选择一个 Face,在当前选中对象表里修改目标值,再用 `影响范围` 选择改法。 + - 先选择一个 Face,在当前选中对象表里修改目标值,再用 `建模意图` 选择改法。 - `局部重建` 会在当前 Face 所在平面内移动顶点并重建周边相邻平面;当前只对简单全平面实体开放。 - `缩放特征` 会缩放所属特征或 Solid;同一对象上的孔、槽、凸台、厚度和其它间距会跟着变化。 - `U向尺寸` / `V向尺寸` 是当前 Face 在自身平面内两个稳定方向上的投影尺寸,不是面积,不是模型整体高度,也不是全局 X/Y/Z 包围盒尺寸。 - `中心`: - - 在当前选中对象表里输入目标 X, Y, Z 坐标,再用 `影响范围` 选择改法。 + - 在当前选中对象表里输入目标 X, Y, Z 坐标,再用 `建模意图` 选择改法。 - `局部重建` 会移动当前 Face 的顶点并重建相邻平面;相邻面可能自然变斜,必要时非共面面会拆成三角面。 - `移动特征` 会把目标中心换算成平移向量,并平移这个 Face 所属的特征或 Solid;Face 自身形状和所属对象内部尺寸不变。 - `孔直径` + `调整圆柱孔径`: @@ -718,7 +721,7 @@ Face 阶段的当前验收口径: - 端部类型是通过轴线端部 inside / outside 采样得到的估算,不等于 CAD 原始建模历史里的“孔深”参数。 - 如果选到的圆柱面不是孔,而是柱或圆角,结果可能不是你想要的,所以它目前仍是实验功能。 - `直径` / `半径` 的 `缩放特征`: - - 先选择一个圆柱 Face,然后在当前选中对象表里修改 `直径` 或 `半径`,并把 `影响范围` 设为 `缩放特征`。 + - 先选择一个圆柱 Face,然后在当前选中对象表里修改 `直径` 或 `半径`,并把 `建模意图` 设为 `缩放特征`。 - 它会按目标直径或半径比例均匀缩放所属特征或 Solid,适合“整个所属对象一起放大/缩小”的意图。 - 它不适合只想改孔壁的场景,因为高度、厚度和同一对象上的其它尺寸会一起变化。 - `槽/半孔宽度` + `调整槽/半孔宽度`: @@ -737,7 +740,7 @@ Face 阶段的当前验收口径: - 程序会按当前圆弧角度把目标圆弧长度换算成目标圆柱直径,再复用圆柱孔/槽调整流程。 - `槽宽` / `槽深` / `弧长` 的 `缩放特征`: - 先选择一个槽/半孔候选 Face。 - - 在当前选中对象表里修改对应属性,并把 `影响范围` 设为 `缩放特征`。 + - 在当前选中对象表里修改对应属性,并把 `建模意图` 设为 `缩放特征`。 - 程序会把目标槽宽、槽深或弧长换算成目标圆柱直径,然后按比例整体缩放所属特征或 Solid。 - 这不是局部重切槽;同一所属对象上的高度、厚度和其它尺寸也会跟随变化。 - `槽/半孔圆弧角度` / `槽/半孔开口角度`: @@ -747,12 +750,12 @@ Face 阶段的当前验收口径: - 程序会保持当前圆柱半径,先补旧扇形槽,再按目标角度重切局部扇形槽。 - `轴心` 的 `移动槽/半孔`: - 先选择一个槽/半孔候选 Face。 - - 在当前选中对象表里修改目标坐标,格式是 `X, Y, Z`,并把 `影响范围` 设为 `移动槽/半孔`。 + - 在当前选中对象表里修改目标坐标,格式是 `X, Y, Z`,并把 `建模意图` 设为 `移动槽/半孔`。 - 程序会先填充旧的局部扇形槽,再在目标轴心位置按同宽度、同圆弧角度切出新槽。 - 这一步只移动选中的局部扇形槽面,不等于完整 CAD 草图里的槽整体迁移;长圆槽、链式槽和复杂槽需要确认结果,失败会回滚。 - `轴心` 的 `移动特征`: - 同样输入目标 X, Y, Z 坐标。 - - 把 `影响范围` 设为 `移动特征`。 + - 把 `建模意图` 设为 `移动特征`。 - 程序会把目标轴心换算成平移向量,并平移所属特征或 Solid;不会填旧槽,也不会切新槽。 - 这适合“整个对象搬动到新位置”的意图,不适合只想移动槽本身的场景。 - `槽孔配对端 Face ID` / `槽孔总长度` / `槽孔中心距`: @@ -761,14 +764,14 @@ Face 阶段的当前验收口径: - 修改 `槽孔总长度` 或 `槽孔中心距` 时,会保持槽宽不变并重建完整长圆槽体。 - `壳体厚度` 的 `拉伸/切除`: - 先选择一个已识别到相对平面的壳体平面 Face。 - - 在当前选中对象表里修改 `壳体厚度`,并把 `影响范围` 设为 `拉伸/切除`。 + - 在当前选中对象表里修改 `壳体厚度`,并把 `建模意图` 设为 `拉伸/切除`。 - 输入目标厚度后点击该行的 `可修改` 或点击 `参数化建模`。 - 程序会移动当前平面区域,让它和相对平面的距离接近目标厚度;相对平面保持不动。 - 如果相对平面重叠率低、厚度变化过大或拉伸/切除方向不稳定,会提高风险或阻止执行。 - 这一步仍是局部 B-Rep 平面拉伸/切除近似,不是完整 CAD 壳体特征参数编辑。 - `壳体厚度` 的 `缩放特征`: - 先选择一个已识别到相对平面的壳体平面 Face。 - - 在当前选中对象表里修改 `壳体厚度`,并把 `影响范围` 设为 `缩放特征`。 + - 在当前选中对象表里修改 `壳体厚度`,并把 `建模意图` 设为 `缩放特征`。 - 程序会沿壳体厚度方向缩放所属特征或 Solid,让估算厚度接近目标值。 - 这不是拉伸/切除;同一对象上的孔距、台阶位置和其它厚度方向尺寸也会跟随变化。 - `凸台直径` + `调整圆柱凸台直径`: @@ -780,34 +783,34 @@ Face 阶段的当前验收口径: - 这不是 CAD 历史里的“修改凸台特征参数”,而是基于当前 B-Rep 的受限几何布尔编辑。 - `直径` / `半径` 的 `缩放特征`: - 先选择一个圆柱凸台 Face,然后在当前选中对象表里修改这一行。 - - 把 `影响范围` 设为 `缩放特征`。 + - 把 `建模意图` 设为 `缩放特征`。 - 它会按目标直径或半径比例均匀缩放所属特征或 Solid,不走凸台包络移除/重建路线。 - 这会连带影响所属对象上的高度、厚度和其它尺寸,需要确认这正是你的建模意图。 - `凸台高度`: - 先选择一个完整圆柱凸台 Face。 - - 在当前选中对象表里修改 `高度`,并把 `影响范围` 设为 `拉伸/切除端盖`,点击该行的 `可修改` 或点击 `参数化建模`。 + - 在当前选中对象表里修改 `高度`,并把 `建模意图` 设为 `拉伸/切除端盖`,点击该行的 `可修改` 或点击 `参数化建模`。 - 程序会尝试找到凸台外端盖 Face,并通过拉伸/切除端盖改变高度。 - `高度` 的 `缩放特征`: - 先选择一个完整圆柱凸台或普通完整圆柱 Face。 - - 在当前选中对象表里修改 `高度`,并把 `影响范围` 设为 `缩放特征`。 + - 在当前选中对象表里修改 `高度`,并把 `建模意图` 设为 `缩放特征`。 - 程序会沿圆柱轴向缩放所属特征或 Solid,让圆柱高度接近目标值。 - 这不是端盖拉伸/切除;同一对象上的孔距、台阶位置和其它轴向尺寸也会跟随变化。 - `轴心`: - 先选择一个完整圆柱凸台 Face。 - - 在当前选中对象表里修改 `轴心`,并把 `影响范围` 设为 `移动凸台`,点击该行的 `可修改` 或点击 `参数化建模`。 + - 在当前选中对象表里修改 `轴心`,并把 `建模意图` 设为 `移动凸台`,点击该行的 `可修改` 或点击 `参数化建模`。 - 程序会切掉旧凸台包络,再按原直径和原高度范围在目标轴心位置补出新凸台;目标位置离基体太远时风险会升高。 - `轴心` 的 `移动特征`: - 同样输入目标 X, Y, Z 坐标。 - - 把 `影响范围` 设为 `移动特征`。 + - 把 `建模意图` 设为 `移动特征`。 - 程序会整体平移凸台所属特征或 Solid;不会移除旧凸台,也不会补新凸台。 - 这适合移动整个所属对象,不适合只改凸台相对基体的位置。 - `轴心` 的 `移动孔`: - 先选择一个完整圆柱孔 Face。 - - 在当前选中对象表里修改目标 X, Y, Z 坐标,并把 `影响范围` 设为 `移动孔`。 + - 在当前选中对象表里修改目标 X, Y, Z 坐标,并把 `建模意图` 设为 `移动孔`。 - 程序会先填补旧孔,再按同直径在目标轴心位置切出新孔。 - `轴心` 的 `移动特征`: - 同样输入目标 X, Y, Z 坐标。 - - 把 `影响范围` 设为 `移动特征`。 + - 把 `建模意图` 设为 `移动特征`。 - 程序会整体平移孔所属特征或 Solid;不会填旧孔,也不会切新孔。 - 这适合移动整个所属对象,不适合只改孔相对基体的位置。 - `封堵圆柱孔`: @@ -819,7 +822,7 @@ Face 阶段的当前验收口径: - 这个功能适合封堵通孔或盲孔,但仍是基于 B-Rep 几何的受限补料,不是 CAD 历史里的“删除孔特征”。 - `盲孔/盲槽深度` 的 `改底面深度`: - 先选择一个盲孔或盲槽的圆柱面,建议从 `可编辑对象` 中点击 `调整盲孔深度` 行进入。 - - 在当前选中对象表里修改 `盲孔/盲槽深度`,并把 `影响范围` 设为 `改底面深度`。 + - 在当前选中对象表里修改 `盲孔/盲槽深度`,并把 `建模意图` 设为 `改底面深度`。 - 输入目标深度后点击该行的 `可修改` 或点击 `参数化建模`。 - 目标深度大于当前估算深度时,程序会用有限长度 cutter 沿孔方向加深切削,预览显示为红色。 - 目标深度小于当前估算深度时,程序会在旧底面方向补料,让孔变浅,预览显示为绿色。 @@ -830,7 +833,7 @@ Face 阶段的当前验收口径: - 操作历史会记录当前深度、目标深度、深度变化比例、底面 Face、开口端 Face和工具参数。 - `盲孔/盲槽深度` 的 `缩放特征`: - 先选择一个已经识别出底面的盲孔或盲槽圆柱面;自动底面不准时,可以先填写 `底面 Face`。 - - 在当前选中对象表里修改 `盲孔/盲槽深度`,并把 `影响范围` 设为 `缩放特征`。 + - 在当前选中对象表里修改 `盲孔/盲槽深度`,并把 `建模意图` 设为 `缩放特征`。 - 程序会沿孔/槽轴向缩放所属特征或 Solid,让深度接近目标值。 - 这不是加深切削,也不是变浅补料;同一对象上的壁厚、孔距和其它轴向尺寸会跟随变化。 - `圆角半径` + `给Edge添加圆角`: @@ -844,7 +847,7 @@ Face 阶段的当前验收口径: - 由于 STEP 不带建模历史,某些边会被 OCCT 判断为不适合倒圆;失败时程序会提示并尝试恢复编辑前状态。 - `圆角半径` / `圆角弧长` 的 `重建圆角`: - 先切换到 `特征` 选择模式并选择一个已有圆角/倒圆候选 Face,或从 `可编辑对象` 中点击 `修改已有圆角半径` 行进入。 - - 在当前选中对象表里修改 `圆角半径` 或 `圆角弧长`,并把 `影响范围` 设为 `重建圆角`。 + - 在当前选中对象表里修改 `圆角半径` 或 `圆角弧长`,并把 `建模意图` 设为 `重建圆角`。 - 输入目标值后点击该行的 `可修改` 或点击 `参数化建模`。 - 程序会先检查该Face是否为 `round/fillet candidate`、是否识别到至少两个支撑Face、目标半径是否有效,以及当前零件是否为单Solid。 - 通过检查后,会显示蓝色半透明预览,表示将尝试移除并重建的已有圆角面。 @@ -852,7 +855,7 @@ Face 阶段的当前验收口径: - 这是 B-Rep 几何重建,不是 CAD 历史圆角参数编辑;复杂 blend、支撑面不明确或恢复锐边失败时会失败并回滚。 - `圆角半径` / `圆角弧长` 的 `缩放特征`: - 先选择一个已有圆角/倒圆候选 Face。 - - 在当前选中对象表里修改 `圆角半径` 或 `圆角弧长`,并把 `影响范围` 设为 `缩放特征`。 + - 在当前选中对象表里修改 `圆角半径` 或 `圆角弧长`,并把 `建模意图` 设为 `缩放特征`。 - 程序会按目标圆角半径或圆弧长度换算出的比例整体缩放所属特征或 Solid,不会移除旧圆角再重建。 - 这会连带改变同一对象上的其它尺寸。 - `倒角距离` + `给Edge添加倒角`: @@ -867,17 +870,17 @@ Face 阶段的当前验收口径: - 真实编辑会在后台调用 OCCT 倒角 API,完成后刷新模型。 - 当前支持对称距离倒角、两侧不同距离倒角和距离+角度倒角。 - 由于 STEP 不带建模历史,某些边会被 OCCT 判断为不适合倒角;失败时程序会提示并尝试恢复编辑前状态。 -- `长度` + `影响范围`: +- `长度` + `建模意图`: - 先切换到 `Edge` 选择模式并选择一条Edge,或从 `可编辑对象` 中点击Edge长度相关行进入。 - 选中Edge后,`长度` 会自动填入当前Edge长度。 - 当前选中对象表中的 `长度基准` 行可以选择 `自动`、`中心`、`固定起点` 或 `固定终点`。 - - `长度` 行的 `影响范围` 下拉框可以选择 `自动`、`只改当前Edge`、`移动端面`、`相邻圆柱` 或 `缩放所属`。 + - `长度` 行的 `建模意图` 下拉框可以选择 `自动`、`只改当前Edge`、`移动端面`、`相邻圆柱` 或 `缩放所属`。 - 把 `长度` 改成目标值后点击这一行的按钮。 - 如果选中的是直线Edge,也可以在当前选中对象表中直接修改 `起点`、`中心` 或 `终点`,格式是 `X, Y, Z`,然后点击该行的 `可修改` 按钮。 - 修改 `中心` 会保持这条直线Edge长度不变,两个端点一起移动;修改起点或终点则只移动对应端点。 - - 对 `assets/models/cube_10mm.step` 这类全平面多面体,选择 `只变当前Edge` 时会只移动被选Edge的端点并重建周边平面。这时正方体不会变成长方体;相邻面会跟着被移动的边自然变成梯形,必要时非共面的面会拆成三角面。 + - 对 `assets/models/cube_10mm.step` 这类全平面多面体,选择 `只改当前Edge` 时会只移动被选Edge的端点并重建周边面。这时正方体不会变成长方体;相邻面会跟着被移动的边自然变形,四点非平面面会优先重建成连续曲面,不再默认拆出面内对角线。 - 对同一个正方体,选择 `移动端面` 时会把改边长转成端面拉伸/切除,相关平行边会一起变化,结果更像从正方体变成长方体。 - - 如果确认窗口里的策略是 `local-edge-only-deform`,说明走的就是“只变这条边”的局部形变路径。 + - 如果确认窗口里的策略是 `local-edge-only-deform`,说明走的就是“只改这条边”的局部形变路径。 - 如果确认窗口里的策略是 `local-edge-endpoint-deform`,说明走的是“移动起点/终点坐标”的局部形变路径。 - 如果确认窗口里的策略是 `local-edge-center-deform`,说明走的是“移动整条Edge中心”的局部形变路径。 - 如果模型不是简单全平面多面体,直线Edge会继续尝试端面拉伸/切除后备路径,并计算需要移动的端面Face和拉伸/切除距离。 @@ -961,7 +964,10 @@ pythonocc-step-editor/ verify_cone_semi_angle_isolation.py # 验证圆锥半角/参考半径隔离执行和嵌入式锥孔局部重切 verify_boss_resize.py # 临时生成圆柱凸台 STEP 并验证凸台直径/高度/轴心修改 verify_edge_edit_suite.py # 一键运行 Edge 专项修改验证 + verify_edge_first_level_topology.py # 验证 Edge 一级端点/相邻边/相邻 Face 事实包和编辑计划携带情况 verify_edge_coordinate_edit.py # 验证直线 Edge 起点/中心/终点坐标局部修改 + verify_edge_target_guards.py # 验证 Edge 目标值不变、0/负数、退化或过大参数会在计划阶段阻止 + verify_edge_intent_specs.py # 验证 Edge 长度建模意图下拉项和底层策略映射一致 verify_ellipse_edge_resize.py # 临时生成椭圆 STEP 并验证椭圆主半径/小半径单轴缩放 verify_edge_length_resize.py # 验证立方体 Edge 长度局部形变基线 verify_edge_round_chamfer.py # 临时生成盒子 STEP 并验证 Edge 圆角/倒角/已有圆角半径修改 @@ -969,13 +975,13 @@ pythonocc-step-editor/ 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 面积、U向尺寸/V向尺寸、偏移曲面和壳体厚度不能输入过大或过小的极端目标值 + 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 的局部/整体中心、面积、U向尺寸/V向尺寸、偏移曲面和拉伸/切除 + 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 # 验证平面、孔、槽候选会输出统一识别摘要、置信度、风险和证据 @@ -1087,30 +1093,33 @@ git diff --check - `scripts/verify_analytic_surface_resize.py`:临时生成圆锥、球面和环面 STEP,验证简单圆锥参考半径/半角解析重建,以及球面半径、环面主半径和环面小半径整体缩放。 - `scripts/verify_cone_semi_angle_isolation.py`:临时生成简单圆锥、旋转圆锥、嵌入式沉头锥孔和带圆柱通孔的沉头锥孔,验证圆锥半角/参考半径的大参数修改会走解析重建或锥孔局部重切;同时覆盖非端面参考半径的局部重切推导、圆锥特征的小端/大端/高度/半角信息、真实模型复杂浅锥大幅半角修改的提前阻断、`1499.5 -> 1700` 这类参考半径修改的快速阻止,以及隔离子进程保护主界面。 - `scripts/verify_boss_resize.py`:临时生成底板加圆柱凸台 STEP,验证凸台直径扩大、直径缩小、高度修改和轴心移动的局部重建链路,并断言不会把单 Solid 拆成多个 Solid。 -- `scripts/verify_edge_edit_suite.py`:集中运行当前 Edge 阶段验证,覆盖直线 Edge 长度的自动策略、只改当前 Edge、移动端面/保持垂直、缩放所属对象,以及固定起点、固定终点、固定中心三类锚点;同时串起直线 Edge 起点/中心/终点坐标修改、Edge 圆角/倒角/已有圆角修改和椭圆 Edge 主/小半径修改。 +- `scripts/verify_edge_edit_suite.py`:集中运行当前 Edge 阶段验证,覆盖 Edge 一级拓扑事实包、直线 Edge 长度的自动策略、只改当前 Edge、移动端面/保持垂直、缩放所属对象,以及固定起点、固定终点、固定中心三类锚点;同时串起 Edge 建模意图 UI 映射、直线 Edge 起点/中心/终点坐标修改、Edge 圆角/倒角/已有圆角修改和椭圆 Edge 主/小半径修改。 +- `scripts/verify_edge_first_level_topology.py`:加载 `cube_10mm.step`,验证一条正方体 Edge 的一级关系包含当前 Edge、2 个端点 Vertex、4 条共享端点相邻 Edge 和 2 个直接相邻 Face,并确认边长、端点/中心坐标、圆角和倒角计划都会携带这份一级事实包。 - `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_target_guards.py`:验证 Edge 长度、端点、中心、圆角、倒角、椭圆半径和圆 Edge 轴心这几类参数在目标值等于当前值、0/负数、退化或明显过大时会直接返回 blocked,并给出阻止原因,不进入真实几何计算。 +- `scripts/verify_edge_intent_specs.py`:验证当前选中对象表里 Edge `长度` 行的 `自动`、`只改当前Edge`、`移动端面`、`相邻圆柱`、`缩放所属` 五种建模意图都能正确同步到底层 `strategy_mode`,并确认计划摘要会解释“邻面变斜”“端面跟随/类似长方体”“其它尺寸跟随”等执行影响。 +- `scripts/verify_edge_length_resize.py`:加载 `cube_10mm.step`,把一条 10mm 直线 Edge 改到 15mm,并断言策略、目标边长和形态分布都正确:局部变形只应有 1 条目标长度边;移动端面或缩放所属应让同向 4 条边一起变成目标长度。 - `scripts/verify_edge_round_chamfer.py`:临时生成盒子和已有圆角盒子 STEP,验证直线 Edge 新增圆角、对称倒角、不等距倒角、距离+角度倒角,以及已有圆角半径修改。 - `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_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 会报告边界环数量,并禁用 `局部重建` 的局部面积、U向尺寸/V向尺寸、中心和偏移曲面计划;同时确认 `拉伸/切除` 的向外补料和向内切削都仍可用并保持单 Solid,且同一 Solid 的局部变形可用性判断会复用缓存。 -- `scripts/verify_face_mixed_surface_guard.py`:临时生成简单圆柱,验证圆柱端盖这个平面 Face 会因为所属 Solid 含有曲面而禁用 `局部重建` 的局部面积、中心和偏移曲面计划,属性提示会说明具体原因;同时确认 `拉伸/切除` 仍可用并让圆柱高度增加。 +- `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_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_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_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 的U向尺寸、面积、中心和偏移曲面编辑完成后,当前选中对象表里的当前值和目标值会重新读到新几何;特别覆盖 `偏移曲面` 的 `局部重建`、`移动特征` 和 `拉伸/切除` 三种语义。 +- `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向尺寸` 两个方向上可以分别做 `局部重建` 与 `缩放特征`,另一个方向不会被误改,属性表也会读回新值。 @@ -1118,8 +1127,8 @@ git diff --check - `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_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_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,验证槽宽、槽深、弧长、弧角、半圆槽局部轴心、长圆槽整槽轴心、槽孔总长度和两端中心距的局部重建链路。 @@ -1149,14 +1158,14 @@ git diff --check - STEP 后台加载:首次先显示粗略网格,再异步切到精细显示,并可隐藏同域内部边。 - 选择:支持零件、Solid、Face、Edge、特征。鼠标悬停红色预高亮,选中黄色高亮;悬停和普通点选只做轻量拾取,不触发完整特征识别。 - 同域面:同域合并仍用于编辑计划、扫描候选、历史定位和必要的模型修复;普通交互不再实时扫描整片同域区域。 -- 编辑:偏移曲面、面积、U向尺寸/V向尺寸、中心、孔径/半径、孔/槽轴心、槽宽、槽深、弧长、弧角、开口角、总长度和中心距、盲孔/盲槽深度、凸台直径/半径/高度/轴心、普通完整圆柱高度、已有圆角半径/圆弧长度、圆锥参考半径/直径/半角、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge圆角、Edge倒角、圆Edge半径/直径、椭圆Edge主半径/小半径、壳体厚度、修改Edge长度、直线Edge起点/中心/终点坐标、零件/Solid 平移和旋转。 -- 编辑语义:Face 的偏移曲面、面积、U向尺寸/V向尺寸 和中心通过 `影响范围` 选择“拉伸/切除”“局部重建”“移动特征”或“缩放特征”;圆锥 Face 会优先区分简单圆锥解析重建、嵌入式锥孔局部重切和复杂兜底整体缩放;孔/槽、凸台、盲孔/盲槽、壳体、圆角和 Edge 长度也会通过 `影响范围` 或计划策略说明本次修改会局部重建、拉伸/切除、移动还是整体缩放。 +- 编辑:偏移变换、面积、U向尺寸/V向尺寸、中心、孔径/半径、孔/槽轴心、槽宽、槽深、弧长、弧角、开口角、总长度和中心距、盲孔/盲槽深度、凸台直径/半径/高度/轴心、普通完整圆柱高度、已有圆角半径/圆弧长度、圆锥参考半径/直径/半角、球面半径/直径(缩放特征)、环面主/小半径或直径(缩放特征)、Edge圆角、Edge倒角、圆Edge半径/直径、椭圆Edge主半径/小半径、壳体厚度、修改Edge长度、直线Edge起点/中心/终点坐标、零件/Solid 平移和旋转。 +- 编辑语义:Face 的偏移变换、面积、U向尺寸/V向尺寸 和中心通过 `建模意图` 选择“拉伸/切除”“局部重建”“移动特征”或“缩放特征”;圆锥 Face 会优先区分简单圆锥解析重建、嵌入式锥孔局部重切和复杂兜底整体缩放;孔/槽、凸台、盲孔/盲槽、壳体、圆角和 Edge 长度也会通过 `建模意图` 或计划策略说明本次修改会局部重建、拉伸/切除、移动还是整体缩放。 - 恢复:编辑前快照、后台执行、失败回滚、撤销/重做、差异预览、热力图和差异报告。 - 导出:完整模型、零件、Solid、Face、特征区域和 Edge,导出前做基础质量检查。 ### 编辑语义 -所有几何参数修改都必须先明确语义、影响范围和风险。修改 Face、孔、槽、圆角、凸台、Solid 或 Edge 时,都不能只看“目标值能不能算出来”;要区分 Face 拉伸/切除、切削补料、特征重建、局部形变、端面移动、对象平移、整体缩放等不同策略,并在确认窗口、属性提示或操作历史里尽量说明本次采用的策略。Edge 的 `长度` 行已经通过 `影响范围` 提供显式改法选择;Face 当前把 `偏移曲面`、`面积`、`U向尺寸`、`V向尺寸` 和 `中心` 显示为单行属性,并通过 `影响范围` 下拉框表达“拉伸/切除”“局部重建”“移动特征”或“缩放特征”;孔/槽直径和半径也通过 `影响范围` 区分“局部重切/重建孔壁”和“缩放所属对象”;凸台直径、半径、高度和轴心也通过 `影响范围` 区分“局部重建/端盖拉伸/切除/移动凸台”和“整体缩放/移动所属对象”;普通完整圆柱的直径、半径和高度也通过 `影响范围` 区分“兜底重切/端盖拉伸/切除”和“整体缩放所属对象”;槽宽、槽深和弧长也通过 `影响范围` 区分“局部重建槽”和“缩放所属对象”;盲孔/盲槽深度也通过 `影响范围` 区分“局部切削/补料”和“轴向缩放所属对象”;壳体厚度也通过 `影响范围` 区分“局部拉伸/切除当前壳体平面”和“沿厚度方向缩放所属对象”;已有圆角半径/弧长也通过 `影响范围` 区分“移除并重建圆角”和“整体缩放所属对象”;孔、槽的轴心坐标已经开始区分“移动/重建局部特征”和“平移所属对象”;孔、槽、凸台、盲孔/盲槽深度、壳体厚度现在会显示并记录当前采用的编辑方式,后续还要继续把更多多语义场景做成用户可选策略。 +所有几何参数修改都必须先明确语义、建模意图、执行影响和风险。修改 Face、孔、槽、圆角、凸台、Solid 或 Edge 时,都不能只看“目标值能不能算出来”;要区分 Face 拉伸/切除、切削补料、特征重建、局部形变、端面移动、对象平移、整体缩放等不同策略,并在确认窗口、属性提示或操作历史里尽量说明本次采用的策略。Edge 的 `长度` 行已经通过 `建模意图` 提供显式改法选择;Face 当前把 `偏移变换`、`面积`、`U向尺寸`、`V向尺寸` 和 `中心` 显示为单行属性,并通过 `建模意图` 下拉框表达“拉伸/切除”“局部重建”“移动特征”或“缩放特征”;孔/槽直径和半径也通过 `建模意图` 区分“局部重切/重建孔壁”和“缩放所属对象”;凸台直径、半径、高度和轴心也通过 `建模意图` 区分“局部重建/端盖拉伸/切除/移动凸台”和“整体缩放/移动所属对象”;普通完整圆柱的直径、半径和高度也通过 `建模意图` 区分“兜底重切/端盖拉伸/切除”和“整体缩放所属对象”;槽宽、槽深和弧长也通过 `建模意图` 区分“局部重建槽”和“缩放所属对象”;盲孔/盲槽深度也通过 `建模意图` 区分“局部切削/补料”和“轴向缩放所属对象”;壳体厚度也通过 `建模意图` 区分“局部拉伸/切除当前壳体平面”和“沿厚度方向缩放所属对象”;已有圆角半径/弧长也通过 `建模意图` 区分“移除并重建圆角”和“整体缩放所属对象”;孔、槽的轴心坐标已经开始区分“移动/重建局部特征”和“平移所属对象”;孔、槽、凸台、盲孔/盲槽深度、壳体厚度现在会显示并记录当前采用的建模意图,后续还要继续把更多多语义场景做成用户可选策略。 必须记住:用户不是只要“数值改掉”,而是要选择建模意图。正方体改一条 Edge 只是例子,实际要求覆盖所有对象:用户应该能选择只改当前边、移动端面/保持垂直、固定某些边/点、让哪些相邻几何跟随变化,或整体缩放/重建所属对象。Face、孔、槽、凸台和圆角也要按同样思路设计。当多种结果都合理时,不要让程序暗自决定;要把“改法、约束、影响对象、跟随关系”做成当前选中对象里的策略行、下拉框或清晰按钮,并用普通用户能理解的话解释。 @@ -1167,7 +1176,7 @@ git diff --check 当前支持的长度修改策略: 1. `auto`:自动策略。按局部边形变、端面拉伸/切除、相邻圆柱直径、几何缩放的顺序寻找可用路径。 -2. `local-edge-only-deform`:只变当前Edge。直线Edge位于简单全平面多面体时,只移动被选Edge的端点并重建所属Solid/特征。对立方体测试模型,默认固定起点移动终点;相邻面会自然变成梯形,必要时非共面四边面拆三角面,而不是把整个正方体缩放成长方体。 +2. `local-edge-only-deform`:只改当前Edge。直线Edge位于简单全平面多面体时,只移动被选Edge的端点并重建所属Solid/特征。对立方体测试模型,默认固定起点移动终点;如果 `长度基准` 选择 `中心`,才会两端各移动一半以保持 Edge 中心不动。相邻面会自然变形,四点非平面面会优先重建成连续曲面,而不是把整个正方体缩放成长方体,也不会默认拆出面内对角线。 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,风险高,会影响其它尺寸。 @@ -1176,7 +1185,7 @@ git diff --check 确认窗口和操作历史里要尽量记录策略、目标范围、目标Edge长度、基准、移动端点、重建面数和风险,方便用户判断结果是否符合预期。 -最近一次基线验证:在 `assets/models/cube_10mm.step` 上选第一条直线Edge,将 10mm 改为 15mm,计划策略为 `local-edge-only-deform`,内存执行成功。执行后边长分布为 `10:9, 11.1803:2, 14.1421:1, 15:1`,Face数从 6 变 7,说明没有整体拉成长方体,而是局部重建并拆分了一个非共面面。 +最近一次基线验证:在 `assets/models/cube_10mm.step` 上选第一条直线Edge,将 10mm 改为 15mm,计划策略为 `local-edge-only-deform`,内存执行成功。执行后边长分布为 `10:9, 11.1803:2, 15:1`,Face 数保持 `6 -> 6`,Edge 数保持 `12 -> 12`,说明没有整体拉成长方体,也没有再把非平面四边面拆成两张三角半面。 移动端面基线验证:在同一个立方体模型上将第一条 10mm 直线 Edge 改为 15mm,并强制选择 `move-edge-end-plane-by-push-pull`、`keep-start`,内存执行成功。执行后 Face 数仍为 6、Edge 数仍为 12,边长分布为 `10:8, 15:4`,说明相关端面和同向边一起移动,结果更接近长方体语义。 @@ -1184,13 +1193,13 @@ 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 局部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 局部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 整体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 整体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。 diff --git a/scripts/verify_edge_edit_suite.py b/scripts/verify_edge_edit_suite.py index 0eb88e3..1fd4690 100644 --- a/scripts/verify_edge_edit_suite.py +++ b/scripts/verify_edge_edit_suite.py @@ -10,6 +10,10 @@ SCRIPT_DIR = PROJECT_ROOT / "scripts" CASES: tuple[tuple[str, tuple[str, ...]], ...] = ( + ( + "Edge first-level topology and edit-plan facts", + ("verify_edge_first_level_topology.py",), + ), ( "Edge length auto strategy resolves to local deformation", ( @@ -58,6 +62,14 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = ( "Edge start, center and end coordinate edits", ("verify_edge_coordinate_edit.py",), ), + ( + "Edge invalid, no-op and oversized target guards", + ("verify_edge_target_guards.py",), + ), + ( + "Edge modeling-intent scopes and plan mapping", + ("verify_edge_intent_specs.py",), + ), ( "Edge fillet, chamfer, asymmetric chamfer, distance-angle chamfer and existing fillet resize", ("verify_edge_round_chamfer.py",), diff --git a/scripts/verify_edge_first_level_topology.py b/scripts/verify_edge_first_level_topology.py new file mode 100644 index 0000000..c633fa3 --- /dev/null +++ b/scripts/verify_edge_first_level_topology.py @@ -0,0 +1,153 @@ +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.ui_helpers import INFO_LABELS +from step_editor.window_state import WindowStateMixin + + +DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" +TOLERANCE = 1e-5 + + +class _EdgeProbe(WindowStateMixin): + def __init__(self, model: StepModel) -> None: + self.model = model + + +def _assert(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def _line_edge_ids_near_length(model: StepModel, length: float, tolerance: float) -> list[int]: + edge_ids: list[int] = [] + for edge_id in range(len(model.edges)): + info = model.edge_info(edge_id) + if info.get("curve") != "line": + continue + if abs(float(info.get("length", 0.0)) - length) <= tolerance: + edge_ids.append(edge_id) + return edge_ids + + +def _edge_under_test(model: StepModel) -> int: + edge_ids = _line_edge_ids_near_length(model, 10.0, TOLERANCE) + if not edge_ids: + raise AssertionError("No 10mm line Edge found in cube model.") + return edge_ids[0] + + +def _point(value: object) -> tuple[float, float, float]: + if not isinstance(value, (tuple, list)) or len(value) != 3: + raise AssertionError(f"Expected 3D point, got {value!r}") + return float(value[0]), float(value[1]), float(value[2]) + + +def _assert_edge_topology(model: StepModel, edge_id: int) -> dict[str, object]: + topology = model.edge_first_level_topology(edge_id) + _assert(topology.get("topology_relation_depth") == 1, f"bad Edge topology depth: {topology}") + _assert( + topology.get("topology_relation_boundary") == "shared-vertex/shared-face", + f"bad Edge relation boundary: {topology}", + ) + _assert(int(topology.get("selected_edge_count", 0) or 0) == 1, f"bad selected Edge count: {topology}") + _assert(int(topology.get("first_level_vertex_count", 0) or 0) == 2, f"bad endpoint count: {topology}") + _assert( + int(topology.get("first_level_adjacent_face_count", 0) or 0) == 2, + f"cube Edge should have 2 incident Faces: {topology}", + ) + _assert( + int(topology.get("first_level_adjacent_edge_count", 0) or 0) == 4, + f"cube Edge should have 4 shared-endpoint neighbor Edges: {topology}", + ) + _assert( + int(topology.get("first_level_edge_count", 0) or 0) == 5, + f"cube Edge first-level Edge set should contain selected Edge + 4 neighbors: {topology}", + ) + ignored = tuple(topology.get("topology_ignored_relation_depths", ()) or ()) + _assert("second-level" in ignored and "third-level" in ignored, f"missing ignored depths: {topology}") + return topology + + +def _assert_edge_facts(facts: dict[str, object], label: str) -> None: + _assert(facts.get("first_level_fact_model") == "STEP/B-Rep first-level fact graph", f"{label}: bad model") + _assert(facts.get("first_level_fact_source_model") == "edge", f"{label}: bad source") + _assert(facts.get("first_level_fact_status") == "ready", f"{label}: bad status {facts}") + _assert(facts.get("first_level_fact_scope") == "edge", f"{label}: bad scope {facts}") + _assert(facts.get("first_level_fact_relation_depth") == 1, f"{label}: bad depth {facts}") + _assert( + facts.get("first_level_fact_relation_boundary") == "shared-vertex/shared-face", + f"{label}: bad boundary {facts}", + ) + _assert(int(facts.get("first_level_fact_subject_edge_count", 0) or 0) == 1, f"{label}: bad subject") + _assert(int(facts.get("first_level_fact_boundary_vertex_count", 0) or 0) == 2, f"{label}: bad vertices") + _assert(int(facts.get("first_level_fact_adjacent_edge_count", 0) or 0) == 4, f"{label}: bad adjacent Edges") + _assert(int(facts.get("first_level_fact_adjacent_face_count", 0) or 0) == 2, f"{label}: bad adjacent Faces") + _assert(int(facts.get("first_level_fact_included_edge_count", 0) or 0) == 5, f"{label}: bad included Edges") + _assert(str(facts.get("first_level_fact_summary") or ""), f"{label}: missing summary") + + +def _assert_plan_facts(plan: dict[str, object], label: str) -> None: + _assert_edge_facts(plan, label) + _assert("resize_strategy" in plan or "chamfer_mode" in plan, f"{label}: plan has no strategy marker") + + +def main() -> int: + for key in ( + "first_level_fact_subject_edge_ids", + "first_level_fact_subject_edge_count", + "first_level_fact_adjacent_edge_ids", + "first_level_fact_adjacent_edge_count", + "first_level_fact_included_edge_ids", + "first_level_fact_included_edge_count", + "first_level_vertex_count", + "first_level_adjacent_edge_count", + ): + _assert(key in INFO_LABELS, f"{key} should have a user-facing label") + + model = StepModel.load(DEFAULT_MODEL) + edge_id = _edge_under_test(model) + info = model.edge_info(edge_id) + start = _point(info.get("start_point")) + end = _point(info.get("end_point")) + center = _point(info.get("length_center")) + + topology = _assert_edge_topology(model, edge_id) + facts = model.edge_first_level_facts(edge_id) + _assert_edge_facts(facts, "Edge first-level facts") + + fields = _EdgeProbe(model)._edge_first_level_selection_fields(edge_id) + _assert_edge_facts(fields, "Edge selection fields") + + endpoint_target = (end[0] + 2.0, end[1], end[2]) + center_target = (center[0], center[1], center[2] + 1.0) + plans = ( + ("Edge length", model.general_edge_length_plan(edge_id, 15.0, anchor_mode="keep-start")), + ("Edge endpoint", model.edge_endpoint_move_plan(edge_id, "end", endpoint_target)), + ("Edge center", model.edge_center_move_plan(edge_id, center_target)), + ("Edge fillet", model.edge_fillet_plan(edge_id, 0.5)), + ("Edge chamfer", model.edge_chamfer_plan(edge_id, 0.4)), + ("Edge asymmetric chamfer", model.edge_asymmetric_chamfer_plan(edge_id, 0.3, 0.4)), + ("Edge distance-angle chamfer", model.edge_distance_angle_chamfer_plan(edge_id, 0.3, 45.0)), + ) + for label, plan in plans: + _assert_plan_facts(plan, label) + + print(f"model={DEFAULT_MODEL}") + print(f"edge_id={edge_id}") + print(f"start={start} end={end}") + print(f"topology={topology}") + print("edge first-level topology ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_edge_intent_specs.py b/scripts/verify_edge_intent_specs.py new file mode 100644 index 0000000..26e2424 --- /dev/null +++ b/scripts/verify_edge_intent_specs.py @@ -0,0 +1,229 @@ +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 step_editor.window_state import WindowStateMixin +from verify_edge_length_resize import _line_edge_ids_near_length +from verify_hole_resize import _write_through_hole_model + + +DEFAULT_CUBE = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" + + +class _FakeCombo: + def __init__(self, value: str, text: str) -> None: + self._value = value + self._text = text + + def currentData(self) -> str: + return self._value + + def currentText(self) -> str: + return self._text + + +class _EdgeIntentProbe(WindowStateMixin): + def __init__(self, *, strategy: str = "auto", anchor: str = "auto") -> None: + self.model = object() + self.operation_in_progress = False + self.scan_in_progress = False + self.load_in_progress = False + self.selected_kind = "edge" + self.selected_face_id = None + self.selected_edge_id = 0 + self.selected_part_id = 0 + self.selected_solid_id = 0 + self.edge_length_strategy_combo = _FakeCombo(strategy, _strategy_label(strategy)) + self.edge_length_anchor_combo = _FakeCombo(anchor, _anchor_label(anchor)) + + +def _assert(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def _strategy_label(strategy: str) -> str: + return { + "auto": "自动选择", + "local-edge-only-deform": "只改当前Edge", + "move-edge-end-plane-by-push-pull": "移动端面/保持垂直", + "resize-adjacent-cylinder-from-circular-edge-length": "相邻圆柱直径", + "scale-owning-shape-from-edge": "缩放所属", + }.get(strategy, strategy) + + +def _anchor_label(anchor: str) -> str: + return { + "auto": "自动", + "center": "中心", + "keep-start": "固定起点", + "keep-end": "固定终点", + }.get(anchor, anchor) + + +def _spec(specs: list[dict[str, object]], key: str) -> dict[str, object]: + for spec in specs: + if spec.get("key") == key: + return spec + raise AssertionError(f"{key} spec was not found") + + +def _scope(spec: dict[str, object], key: str) -> dict[str, object]: + modes = spec.get("scope_modes") + if not isinstance(modes, dict): + raise AssertionError(f"{spec.get('key')} should expose modeling-intent scope modes") + mode = modes.get(key) + if not isinstance(mode, dict): + raise AssertionError(f"{spec.get('key')} has no scope mode {key}") + return mode + + +def _specs_for_edge(info: dict[str, object], *, strategy: str = "auto", anchor: str = "auto") -> list[dict[str, object]]: + probe = _EdgeIntentProbe(strategy=strategy, anchor=anchor) + specs, _used = probe._editable_property_specs(info) + return specs + + +def _preselect_value(mode: dict[str, object]) -> str: + preselects = mode.get("preselect_combos") + if not isinstance(preselects, (tuple, list)) or not preselects: + raise AssertionError(f"scope mode should synchronize the hidden Edge strategy combo: {mode}") + first = preselects[0] + if not isinstance(first, dict): + raise AssertionError(f"invalid preselect combo spec: {mode}") + return str(first.get("value") or "") + + +def _assert_edge_length_scope(spec: dict[str, object], mode_key: str, label: str, *, enabled: bool) -> None: + mode = _scope(spec, mode_key) + _assert(str(mode.get("label")) == label, f"{mode_key} label should be {label!r}, got {mode.get('label')!r}") + _assert(str(mode.get("action")) == "resize_any_edge_length", f"{mode_key} should call resize_any_edge_length") + _assert(str(mode.get("target_attr")) == "edge_target_length_input", f"{mode_key} should target Edge length input") + _assert(bool(mode.get("enabled")) is enabled, f"{mode_key} enabled should be {enabled}, got {mode.get('enabled')}") + _assert(_preselect_value(mode) == mode_key, f"{mode_key} should preselect itself before execution") + hint = f"{mode.get('enabled_tip', '')} {mode.get('range_hint', '')}" + _assert(label in hint or mode_key == "auto", f"{mode_key} should explain its modeling intent") + + +def _verify_line_edge_intent_specs() -> None: + model = StepModel.load(DEFAULT_CUBE) + edge_ids = _line_edge_ids_near_length(model, 10.0, 1e-5) + if not edge_ids: + raise AssertionError("cube has no 10mm line Edge") + info = model.edge_info(edge_ids[0]) + specs = _specs_for_edge(info, strategy="move-edge-end-plane-by-push-pull", anchor="keep-start") + _assert(str(_spec(specs, "edge_edit_semantics").get("label")) == "建模意图", "Edge semantic row should be labeled 建模意图") + length_spec = _spec(specs, "length") + _assert(str(length_spec.get("label")) == "长度", f"Edge length label should stay user-facing: {length_spec}") + _assert( + str(length_spec.get("scope_default")) == "move-edge-end-plane-by-push-pull", + f"Edge length should honor selected modeling intent: {length_spec.get('scope_default')}", + ) + _assert_edge_length_scope(length_spec, "auto", "自动", enabled=True) + _assert_edge_length_scope(length_spec, "local-edge-only-deform", "只改当前Edge", enabled=True) + _assert_edge_length_scope(length_spec, "move-edge-end-plane-by-push-pull", "移动端面", enabled=True) + _assert_edge_length_scope( + length_spec, + "resize-adjacent-cylinder-from-circular-edge-length", + "相邻圆柱", + enabled=False, + ) + _assert_edge_length_scope(length_spec, "scale-owning-shape-from-edge", "缩放所属", enabled=True) + anchor_spec = _spec(specs, "edge_length_anchor_mode") + _assert(str(anchor_spec.get("label")) == "长度基准", "line Edge should expose a length anchor row") + print("line Edge modeling-intent specs ok") + + +def _circle_edge(model: StepModel) -> int: + for edge_id in range(len(model.edges)): + info = model.edge_info(edge_id) + if info.get("curve") == "circle" and info.get("radius") and info.get("length"): + return edge_id + raise AssertionError("model has no circular Edge") + + +def _verify_circle_edge_intent_specs() -> None: + with tempfile.TemporaryDirectory(prefix="geom_param_edge_intent_circle_") as temp_dir: + model_path = Path(temp_dir) / "hole.step" + _write_through_hole_model(model_path) + model = StepModel.load(model_path) + info = model.edge_info(_circle_edge(model)) + specs = _specs_for_edge( + info, + strategy="resize-adjacent-cylinder-from-circular-edge-length", + anchor="auto", + ) + length_spec = _spec(specs, "length") + _assert( + str(length_spec.get("scope_default")) == "resize-adjacent-cylinder-from-circular-edge-length", + "circular Edge length should honor adjacent-cylinder intent", + ) + _assert_edge_length_scope(length_spec, "local-edge-only-deform", "只改当前Edge", enabled=False) + _assert_edge_length_scope(length_spec, "move-edge-end-plane-by-push-pull", "移动端面", enabled=False) + _assert_edge_length_scope( + length_spec, + "resize-adjacent-cylinder-from-circular-edge-length", + "相邻圆柱", + enabled=True, + ) + radius_spec = _spec(specs, "circle_edge_radius") + radius_modes = radius_spec.get("scope_modes") + _assert( + isinstance(radius_modes, dict) + and set(radius_modes) == {"auto", "resize-adjacent-cylinder-from-circular-edge-length", "scale-owning-shape-from-edge"}, + f"circle Edge radius should expose only circular modeling intents: {radius_modes}", + ) + for mode_key in radius_modes: + _assert(_preselect_value(radius_modes[mode_key]) == mode_key, f"{mode_key} radius scope should sync strategy") + print("circular Edge modeling-intent specs ok") + + +def _verify_edge_length_plan_intents() -> None: + model = StepModel.load(DEFAULT_CUBE) + edge_id = _line_edge_ids_near_length(model, 10.0, 1e-5)[0] + expectations = ( + ("local-edge-only-deform", "keep-start", "local-edge-only-deform", "相邻面会自然变斜"), + ("move-edge-end-plane-by-push-pull", "keep-start", "move-edge-end-plane-by-push-pull", "正方体这类模型会更像变成长方体"), + ("scale-owning-shape-from-edge", "center", "scale-owning-shape-from-edge", "其它尺寸会跟随变化"), + ) + for strategy, anchor, expected_strategy, impact_fragment in expectations: + plan = model.general_edge_length_plan(edge_id, 15.0, anchor_mode=anchor, strategy_mode=strategy) + _assert(str(plan.get("status")) != "blocked", f"{strategy}/{anchor} should be available: {plan}") + _assert( + str(plan.get("resize_strategy")) == expected_strategy, + f"{strategy}/{anchor} resolved to {plan.get('resize_strategy')}, expected {expected_strategy}", + ) + impact = str(plan.get("edge_length_impact_summary") or "") + _assert(impact_fragment in impact, f"{strategy} impact summary should explain result: {impact}") + blocked = model.general_edge_length_plan( + edge_id, + 15.0, + anchor_mode="center", + strategy_mode="move-edge-end-plane-by-push-pull", + ) + _assert(str(blocked.get("status")) == "blocked", "moving an end plane while fixing center should be blocked") + _assert("不能使用固定中心" in str(blocked.get("message") or ""), f"blocked message should explain center conflict: {blocked}") + print("Edge modeling-intent plans ok") + + +def main() -> int: + _verify_line_edge_intent_specs() + _verify_circle_edge_intent_specs() + _verify_edge_length_plan_intents() + print("Edge modeling-intent spec suite passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_edge_length_resize.py b/scripts/verify_edge_length_resize.py index 62a98ed..b041ba1 100644 --- a/scripts/verify_edge_length_resize.py +++ b/scripts/verify_edge_length_resize.py @@ -34,6 +34,61 @@ def _length_distribution(model: StepModel) -> dict[float, int]: return dict(sorted(counts.items())) +def _count_near(values: list[float], target: float, tolerance: float) -> int: + return sum(1 for value in values if abs(value - target) <= tolerance) + + +def _assert_cube_edge_intent_geometry( + *, + source_length: float, + target_length: float, + strategy: str, + anchor: str, + lengths: list[float], + tolerance: float, +) -> None: + target_count = _count_near(lengths, target_length, tolerance) + source_count = _count_near(lengths, source_length, tolerance) + delta = abs(target_length - source_length) + effective_anchor = "keep-start" if anchor == "auto" else anchor + if strategy == "local-edge-only-deform": + if target_count != 1: + raise SystemExit( + "local Edge deformation should only make the selected Edge reach the target length; " + f"target_count={target_count}, lengths={_length_distribution_from_values(lengths)}" + ) + if effective_anchor == "center": + expected_slanted = (source_length * source_length + (delta * 0.5) * (delta * 0.5)) ** 0.5 + slanted_count = _count_near(lengths, expected_slanted, tolerance) + if source_count != 7 or slanted_count != 4: + raise SystemExit( + "center-anchored local Edge deformation should move both endpoints equally; " + f"source_count={source_count}, slanted_count={slanted_count}, " + f"expected_slanted={expected_slanted:g}, lengths={_length_distribution_from_values(lengths)}" + ) + else: + expected_slanted = (source_length * source_length + delta * delta) ** 0.5 + slanted_count = _count_near(lengths, expected_slanted, tolerance) + if source_count != 9 or slanted_count != 2: + raise SystemExit( + "one-end anchored local Edge deformation should move only the selected Edge endpoint; " + f"source_count={source_count}, slanted_count={slanted_count}, " + f"expected_slanted={expected_slanted:g}, lengths={_length_distribution_from_values(lengths)}" + ) + elif strategy in {"move-edge-end-plane-by-push-pull", "scale-owning-shape-from-edge"}: + if target_count != 4 or source_count != 8: + raise SystemExit( + f"{strategy} should resize the whole cube span in the selected Edge direction; " + f"target_count={target_count}, source_count={source_count}, " + f"lengths={_length_distribution_from_values(lengths)}" + ) + + +def _length_distribution_from_values(values: list[float]) -> dict[float, int]: + counts = Counter(round(value, 6) for value in values) + return dict(sorted(counts.items())) + + def main() -> int: parser = argparse.ArgumentParser(description="Verify cube edge-length resize semantics.") parser.add_argument("model", nargs="?", default=str(DEFAULT_MODEL), help="STEP model path.") @@ -87,6 +142,24 @@ def main() -> int: error = abs(nearest - args.target_length) if error > args.tolerance: raise SystemExit(f"target length check failed: nearest={nearest:g}, error={error:g}") + if ( + Path(args.model).resolve() == DEFAULT_MODEL.resolve() + and strategy == "local-edge-only-deform" + and (after.faces != before.faces or after.edges != before.edges) + ): + raise SystemExit( + "cube local Edge deformation should not split faces/edges; " + f"before faces/edges={before.faces}/{before.edges}, after={after.faces}/{after.edges}" + ) + if Path(args.model).resolve() == DEFAULT_MODEL.resolve(): + _assert_cube_edge_intent_geometry( + source_length=args.source_length, + target_length=args.target_length, + strategy=strategy, + anchor=args.anchor, + lengths=lengths, + tolerance=max(args.tolerance, 1e-5), + ) print(f"model={Path(args.model)}") print(f"edge_id={edge_id}") @@ -97,6 +170,7 @@ def main() -> int: print(f"end_move={plan.get('local_edge_deform_end_move')}") print(f"before_faces={before.faces} before_edges={before.edges}") print(f"after_faces={after.faces} after_edges={after.edges}") + print(f"topology_stable={after.faces == before.faces and after.edges == before.edges}") print(f"nearest_length={nearest:.6f} target_error={error:.6g}") print(f"length_distribution={_length_distribution(model)}") print(result.encode("ascii", "backslashreplace").decode("ascii")) diff --git a/scripts/verify_edge_target_guards.py b/scripts/verify_edge_target_guards.py new file mode 100644 index 0000000..541452e --- /dev/null +++ b/scripts/verify_edge_target_guards.py @@ -0,0 +1,118 @@ +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 _tuple3 +from verify_edge_length_resize import _line_edge_ids_near_length +from verify_ellipse_edge_resize import _ellipse_edge_ids, _write_ellipse_face_model +from verify_hole_resize import _write_through_hole_model + + +DEFAULT_CUBE = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" + + +def _assert_blocked(plan: dict[str, object], label: str) -> None: + if str(plan.get("status") or "") != "blocked" or str(plan.get("risk") or "") != "blocked": + raise AssertionError(f"{label} should be blocked, got status={plan.get('status')}, plan={plan}") + message = str(plan.get("message") or plan.get("blockers") or "") + if not message.strip(): + raise AssertionError(f"{label} should explain why it is blocked: {plan}") + + +def _cube_edge(model: StepModel) -> int: + edge_ids = _line_edge_ids_near_length(model, 10.0, 1e-5) + if not edge_ids: + raise AssertionError("cube has no 10mm line Edge") + return edge_ids[0] + + +def _verify_straight_edge_guards() -> None: + model = StepModel.load(DEFAULT_CUBE) + edge_id = _cube_edge(model) + info = model.edge_info(edge_id) + current_length = float(info.get("length") or 0.0) + start = _tuple3(info.get("start_point"), "edge start") + end = _tuple3(info.get("end_point"), "edge end") + center = _tuple3(info.get("length_center"), "edge center") + + _assert_blocked(model.general_edge_length_plan(edge_id, current_length), "same Edge length") + _assert_blocked(model.general_edge_length_plan(edge_id, 0.0), "zero Edge length") + _assert_blocked(model.general_edge_length_plan(edge_id, -1.0), "negative Edge length") + _assert_blocked(model.edge_endpoint_move_plan(edge_id, "start", start), "same start point") + _assert_blocked(model.edge_endpoint_move_plan(edge_id, "end", end), "same end point") + _assert_blocked(model.edge_endpoint_move_plan(edge_id, "start", end), "collapsed endpoint") + _assert_blocked(model.edge_center_move_plan(edge_id, center), "same Edge center") + _assert_blocked(model.edge_fillet_plan(edge_id, 0.0), "zero Edge fillet radius") + _assert_blocked(model.edge_fillet_plan(edge_id, current_length * 0.45), "oversized Edge fillet radius") + _assert_blocked(model.edge_chamfer_plan(edge_id, 0.0), "zero Edge chamfer distance") + _assert_blocked(model.edge_chamfer_plan(edge_id, current_length * 0.45), "oversized Edge chamfer distance") + _assert_blocked(model.edge_asymmetric_chamfer_plan(edge_id, 0.0, 0.5), "zero asymmetric chamfer distance") + _assert_blocked( + model.edge_asymmetric_chamfer_plan(edge_id, current_length * 0.45, 0.5), + "oversized asymmetric chamfer distance", + ) + _assert_blocked(model.edge_distance_angle_chamfer_plan(edge_id, 0.0, 45.0), "zero distance-angle chamfer") + _assert_blocked(model.edge_distance_angle_chamfer_plan(edge_id, 0.5, 0.0), "zero distance-angle angle") + _assert_blocked(model.edge_distance_angle_chamfer_plan(edge_id, 0.5, 90.0), "too large distance-angle angle") + print("straight Edge target guards ok") + + +def _verify_ellipse_guards() -> None: + with tempfile.TemporaryDirectory(prefix="geom_param_edge_guard_ellipse_") as temp_dir: + model_path = Path(temp_dir) / "ellipse.step" + _write_ellipse_face_model(model_path) + model = StepModel.load(model_path) + edge_ids = _ellipse_edge_ids(model) + if not edge_ids: + raise AssertionError("ellipse model has no ellipse Edge") + edge_id = edge_ids[0] + info = model.edge_info(edge_id) + major = float(info.get("major_radius") or 0.0) + minor = float(info.get("minor_radius") or 0.0) + + _assert_blocked(model.ellipse_edge_axis_radius_plan(edge_id, major, axis_kind="major"), "same ellipse major radius") + _assert_blocked(model.ellipse_edge_axis_radius_plan(edge_id, minor, axis_kind="minor"), "same ellipse minor radius") + _assert_blocked(model.ellipse_edge_axis_radius_plan(edge_id, 0.0, axis_kind="major"), "zero ellipse major radius") + _assert_blocked(model.ellipse_edge_axis_radius_plan(edge_id, -1.0, axis_kind="minor"), "negative ellipse minor radius") + print("ellipse Edge target guards ok") + + +def _verify_circular_axis_guard() -> None: + with tempfile.TemporaryDirectory(prefix="geom_param_edge_guard_circle_") as temp_dir: + model_path = Path(temp_dir) / "hole.step" + _write_through_hole_model(model_path) + model = StepModel.load(model_path) + for edge_id in range(len(model.edges)): + info = model.edge_info(edge_id) + if info.get("curve") != "circle": + continue + center = info.get("center") + if center is None: + continue + _assert_blocked(model.circular_edge_axis_move_plan(edge_id, center), "same circular Edge center") + print("circular Edge axis target guard ok") + return + raise AssertionError("hole model has no circular Edge with center") + + +def main() -> int: + _verify_straight_edge_guards() + _verify_ellipse_guards() + _verify_circular_axis_guard() + print("Edge target guard suite passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_face_feature_parameter_consistency.py b/scripts/verify_face_feature_parameter_consistency.py index 6fc129d..d168f06 100644 --- a/scripts/verify_face_feature_parameter_consistency.py +++ b/scripts/verify_face_feature_parameter_consistency.py @@ -9,6 +9,7 @@ if str(PROJECT_ROOT) not in sys.path: from step_editor.model import StepModel from step_editor.window_state import WindowStateMixin, _feature_dimension_keys +from scripts.verify_large_stepped_cap_push_pull import _large_multi_boundary_cap_face, _large_stepped_cap_face MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step" @@ -26,7 +27,7 @@ class _Probe(WindowStateMixin): pass -def _feature_rows(model: StepModel, face_id: int) -> tuple[str, ...]: +def _feature_context(model: StepModel, face_id: int) -> dict[str, object]: info = model.quick_face_info(face_id) probe = object.__new__(_Probe) probe.model = model @@ -41,7 +42,24 @@ def _feature_rows(model: StepModel, face_id: int) -> tuple[str, ...]: 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) + return probe._feature_context_info(face_id) + + +def _feature_rows(model: StepModel, face_id: int) -> tuple[str, ...]: + context = _feature_context(model, 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(context["part_id"]) + probe.selected_solid_id = int(context["solid_id"]) + probe.feature_detection_level = "current-only" + probe.manual_bottom_face_id = None + probe.manual_slot_pair_face_id = None 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") @@ -53,6 +71,47 @@ def _assert_contains(keys: tuple[str, ...], expected: tuple[str, ...], label: st raise AssertionError(f"{label} missing {missing}, got {keys}") +def _assert_face_594_stays_stable_after_remote_cap_edit() -> None: + model = StepModel.load(MODEL_PATH) + source_face_id = _large_stepped_cap_face(model) + target_face_id = _large_multi_boundary_cap_face(model) + if target_face_id != FACE_ID: + raise AssertionError(f"expected large multi-boundary cap to be Face {FACE_ID}, got {target_face_id}") + + target_logical_id = model.face_region_logical_id(target_face_id) + before_rows = _feature_rows(model, target_face_id) + before_info = model.quick_face_info(target_face_id) + model.push_pull_face(source_face_id, 89.0) + + resolved_face_id = model.resolve_face_selection_id(target_logical_id) + if resolved_face_id is None: + raise AssertionError(f"remote cap edit lost logical Face {target_logical_id}") + after_info = model.quick_face_info(resolved_face_id) + if after_info.get("surface") != "plane": + raise AssertionError( + f"logical Face {target_logical_id} should still point to the multi-boundary plane, " + f"got Face {resolved_face_id}: {after_info}" + ) + if int(after_info.get("inner_boundary_wires", 0) or 0) < int(before_info.get("inner_boundary_wires", 0) or 0): + raise AssertionError( + f"logical Face {target_logical_id} lost inner boundary wires after remote edit: " + f"before={before_info}, after={after_info}" + ) + + after_rows = _feature_rows(model, resolved_face_id) + if after_rows != before_rows: + raise AssertionError( + "Face 594 current-only feature rows changed after editing a remote cylindrical cap: " + f"before={before_rows}, after={after_rows}, resolved={resolved_face_id}" + ) + + context = _feature_context(model, resolved_face_id) + if context.get("selection_display_id") != target_logical_id: + raise AssertionError(f"feature context should keep logical Face ID {target_logical_id}: {context}") + if context.get("selection_topological_face_id") != resolved_face_id: + raise AssertionError(f"feature context should expose current topological Face {resolved_face_id}: {context}") + + def main() -> int: model = StepModel.load(MODEL_PATH) if FACE_ID >= len(model.faces): @@ -80,6 +139,8 @@ def main() -> int: f"before={before_cached_rows}, after={after_cached_rows}" ) + _assert_face_594_stays_stable_after_remote_cap_edit() + print("Face feature parameter consistency ok") return 0 diff --git a/scripts/verify_isolated_face_edit.py b/scripts/verify_isolated_face_edit.py index d0eb984..5b37486 100644 --- a/scripts/verify_isolated_face_edit.py +++ b/scripts/verify_isolated_face_edit.py @@ -374,7 +374,7 @@ 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, diff --git a/scripts/verify_property_editor_specs.py b/scripts/verify_property_editor_specs.py index 53c42c9..5b46db1 100644 --- a/scripts/verify_property_editor_specs.py +++ b/scripts/verify_property_editor_specs.py @@ -37,6 +37,15 @@ def _specs(info: dict[str, object]) -> list[dict[str, object]]: return specs +def _edge_specs(info: dict[str, object]) -> list[dict[str, object]]: + probe = _PropertySpecProbe() + probe.selected_kind = "edge" + probe.selected_face_id = None + probe.selected_edge_id = 7 + specs, _used = probe._editable_property_specs(info) + return specs + + def _display_specs(info: dict[str, object]) -> list[dict[str, object]]: probe = _PropertySpecProbe() probe.selected_kind = "face" @@ -80,6 +89,19 @@ def _assert_label(specs: list[dict[str, object]], key: str, label: str) -> None: raise SystemExit(f"{key} label should be {label!r}, got {actual!r}") +def _assert_current_text_contains( + specs: list[dict[str, object]], + key: str, + fragments: tuple[str, ...], + label: str, +) -> None: + spec = _spec(specs, key) + text = str(spec.get("current_text") or "") + missing = [fragment for fragment in fragments if fragment not in text] + if missing: + raise SystemExit(f"{label} {key} text missing {missing}: {text!r}") + + def _assert_hard_range(specs: list[dict[str, object]], key: str, low: float, high: float) -> None: spec = _spec(specs, key) actual_low = spec.get("min_value") @@ -167,6 +189,8 @@ def _assert_actionable_rows_first(info: dict[str, object], expected_keys: tuple[ seen_non_actionable = False front_keys: list[str] = [] for spec in display_specs: + if bool(spec.get("pin_top")): + continue key = str(spec.get("key", "")) if _is_actionable_edit_spec(spec): if seen_non_actionable: @@ -368,6 +392,25 @@ def main() -> int: } plane_specs = _specs(plane_info) plane_keys = {str(spec.get("key", "")) for spec in plane_specs} + _assert_label(plane_specs, "cad_modeling_form", "建模形式") + _assert_current_text_contains( + plane_specs, + "cad_modeling_form", + ("柔性建模", "拉伸切除", "偏移变换"), + "plane Face", + ) + _assert_label(plane_specs, "cad_recommended_operation", "推荐操作") + _assert_current_text_contains( + plane_specs, + "cad_recommended_operation", + ("优先改偏移变换", "拉伸/切除"), + "plane Face", + ) + plane_display_specs = _display_specs(plane_info) + if str(plane_display_specs[0].get("key", "")) != "cad_modeling_form": + raise SystemExit(f"plane Face should show CAD modeling form first: {plane_display_specs[0]}") + if str(plane_display_specs[1].get("key", "")) != "cad_recommended_operation": + raise SystemExit(f"plane Face should show recommended operation second: {plane_display_specs[1]}") _assert_contains( plane_keys, { @@ -381,7 +424,7 @@ def main() -> int: ) _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_label(plane_specs, "face_target_normal_position", "偏移变换") _assert_actionable_rows_first( plane_info, ( @@ -404,6 +447,13 @@ 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}") + _assert_label(plane_feature_rows, "face_edit_semantics", "建模意图") + _assert_label(plane_feature_rows, "cad_modeling_form", "建模形式") + _assert_label(plane_feature_rows, "cad_recommended_operation", "推荐操作") + if str(plane_feature_rows[0].get("key", "")) != "cad_modeling_form": + raise SystemExit(f"feature mode should keep CAD modeling form first: {plane_feature_rows[0]}") + if str(plane_feature_rows[1].get("key", "")) != "cad_recommended_operation": + raise SystemExit(f"feature mode should keep recommended operation second: {plane_feature_rows[1]}") expected_plane_feature_keys = { "area", "local_face_width", @@ -512,24 +562,36 @@ def main() -> int: if shell_owning.get("label") != "缩放特征": raise SystemExit(f"shell thickness owning scope should expose an edit semantic, got {shell_owning}") - cylinder_keys = _spec_keys( - { - "surface": "cylinder", - "feature_guess": "hole/groove candidate", - "diameter": 6.0, - "radius": 3.0, - "angular_span": 6.283185307179586, - "area": 188.0, - "area_center": (0.0, 0.0, 0.0), - "bbox_center": (0.0, 0.0, 0.0), - "axis": (0.0, 0.0, 1.0), - "axis_point": (0.0, 0.0, 0.0), - "axis_center": (0.0, 0.0, 5.0), - "feature_bottom_face_ids": (), - } - ) + cylinder_info = { + "surface": "cylinder", + "feature_guess": "hole/groove candidate", + "diameter": 6.0, + "radius": 3.0, + "angular_span": 6.283185307179586, + "area": 188.0, + "area_center": (0.0, 0.0, 0.0), + "bbox_center": (0.0, 0.0, 0.0), + "axis": (0.0, 0.0, 1.0), + "axis_point": (0.0, 0.0, 0.0), + "axis_center": (0.0, 0.0, 5.0), + "feature_bottom_face_ids": (), + } + cylinder_specs = _specs(cylinder_info) + cylinder_keys = {str(spec.get("key", "")) for spec in cylinder_specs} _assert_no_generic_face_leak(cylinder_keys, "cylindrical hole feature") _assert_contains(cylinder_keys, {"diameter", "hole_cylinder_radius"}, "cylindrical hole feature") + _assert_current_text_contains( + cylinder_specs, + "cad_modeling_form", + ("工程特征", "孔", "重切"), + "cylindrical hole feature", + ) + _assert_current_text_contains( + cylinder_specs, + "cad_recommended_operation", + ("孔径", "盲孔", "轴心"), + "cylindrical hole feature", + ) cylinder_topology_info = { "surface": "cylinder", @@ -646,30 +708,42 @@ def main() -> int: 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", - "feature_guess": "hole/groove candidate", - "diameter": 6.0, - "radius": 3.0, - "angular_span": 3.141592653589793, - "slot_chord_width_estimate": 6.0, - "slot_sagitta_depth_estimate": 3.0, - "slot_arc_length_estimate": 9.42477796076938, - "area": 188.0, - "area_center": (0.0, 0.0, 0.0), - "bbox_center": (0.0, 0.0, 0.0), - "axis": (0.0, 0.0, 1.0), - "axis_point": (0.0, 0.0, 0.0), - "axis_center": (0.0, 0.0, 5.0), - } - ) + slot_info = { + "surface": "cylinder", + "feature_guess": "hole/groove candidate", + "diameter": 6.0, + "radius": 3.0, + "angular_span": 3.141592653589793, + "slot_chord_width_estimate": 6.0, + "slot_sagitta_depth_estimate": 3.0, + "slot_arc_length_estimate": 9.42477796076938, + "area": 188.0, + "area_center": (0.0, 0.0, 0.0), + "bbox_center": (0.0, 0.0, 0.0), + "axis": (0.0, 0.0, 1.0), + "axis_point": (0.0, 0.0, 0.0), + "axis_center": (0.0, 0.0, 5.0), + } + slot_specs = _specs(slot_info) + slot_keys = {str(spec.get("key", "")) for spec in slot_specs} _assert_no_generic_face_leak(slot_keys, "slot/half-hole feature") _assert_contains( slot_keys, {"slot_chord_width_estimate", "slot_sagitta_depth_estimate", "slot_arc_length_estimate"}, "slot/half-hole feature", ) + _assert_current_text_contains( + slot_specs, + "cad_modeling_form", + ("工程特征", "槽", "局部重建"), + "slot/half-hole feature", + ) + _assert_current_text_contains( + slot_specs, + "cad_recommended_operation", + ("槽宽", "槽深", "轴心"), + "slot/half-hole feature", + ) boss_info = { "surface": "cylinder", @@ -691,30 +765,42 @@ def main() -> int: boss_keys = {str(spec.get("key", "")) for spec in boss_specs} _assert_no_generic_face_leak(boss_keys, "boss feature") _assert_contains(boss_keys, {"boss_diameter", "boss_radius", "boss_height"}, "boss feature") + _assert_current_text_contains( + boss_specs, + "cad_modeling_form", + ("工程特征", "凸台", "局部重建"), + "boss feature", + ) boss_height = _spec(boss_specs, "boss_height") if boss_height.get("scope_default") != "owning": raise SystemExit(f"boss height should default to owning-axis resize: {boss_height}") if boss_height.get("action") != "resize_cylindrical_height_owning_scale": raise SystemExit(f"boss height should use owning-axis action by default: {boss_height}") - fillet_keys = _spec_keys( - { - "surface": "cylinder", - "feature_guess": "round/fillet candidate", - "diameter": 2.0, - "radius": 1.0, - "angular_span": 1.5707963267948966, - "existing_fillet_radius": 1.0, - "feature_existing_fillet_support_face_ids": (1, 2), - "area": 3.0, - "area_center": (0.0, 0.0, 0.0), - "bbox_center": (0.0, 0.0, 0.0), - "axis": (0.0, 0.0, 1.0), - "axis_point": (0.0, 0.0, 0.0), - } - ) + fillet_info = { + "surface": "cylinder", + "feature_guess": "round/fillet candidate", + "diameter": 2.0, + "radius": 1.0, + "angular_span": 1.5707963267948966, + "existing_fillet_radius": 1.0, + "feature_existing_fillet_support_face_ids": (1, 2), + "area": 3.0, + "area_center": (0.0, 0.0, 0.0), + "bbox_center": (0.0, 0.0, 0.0), + "axis": (0.0, 0.0, 1.0), + "axis_point": (0.0, 0.0, 0.0), + } + fillet_specs = _specs(fillet_info) + fillet_keys = {str(spec.get("key", "")) for spec in fillet_specs} _assert_no_generic_face_leak(fillet_keys, "existing fillet feature") _assert_contains(fillet_keys, {"existing_fillet_radius_estimate"}, "existing fillet feature") + _assert_current_text_contains( + fillet_specs, + "cad_modeling_form", + ("工程特征", "倒圆角", "重新倒圆"), + "existing fillet feature", + ) analytic_cases = ( ( @@ -766,6 +852,27 @@ def main() -> int: keys = {str(spec.get("key", "")) for spec in specs} _assert_no_generic_face_leak(keys, label) _assert_contains(keys, required, label) + if label == "cone feature": + _assert_current_text_contains( + specs, + "cad_modeling_form", + ("工程特征", "拔模", "锥孔"), + label, + ) + elif label == "sphere feature": + _assert_current_text_contains( + specs, + "cad_modeling_form", + ("解析曲面", "缩放特征", "球面"), + label, + ) + elif label == "torus feature": + _assert_current_text_contains( + specs, + "cad_modeling_form", + ("解析曲面", "缩放特征", "环面"), + label, + ) if label == "cone feature": _assert_label(specs, "cone_reference_radius", "参考半径") _assert_label(specs, "cone_reference_diameter", "参考直径") @@ -775,6 +882,29 @@ def main() -> int: if surface_spec.get("current_text") != "圆锥面 / 拔模面": raise SystemExit(f"cone surface should be displayed in user-facing Chinese, got {surface_spec}") + edge_specs = _edge_specs( + { + "curve": "line", + "length": 10.0, + "start_point": (0.0, 0.0, 0.0), + "end_point": (10.0, 0.0, 0.0), + "length_center": (5.0, 0.0, 0.0), + } + ) + _assert_current_text_contains( + edge_specs, + "cad_modeling_form", + ("柔性建模", "移动几何", "直线 Edge"), + "line Edge", + ) + _assert_current_text_contains( + edge_specs, + "cad_recommended_operation", + ("优先改长度", "移动端面", "只改当前Edge"), + "line Edge", + ) + _assert_contains({str(spec.get("key", "")) for spec in edge_specs}, {"length"}, "line Edge") + low_recognition_specs = _specs( { "surface": "sphere", diff --git a/step_editor/app.py b/step_editor/app.py index 3102670..e58879d 100644 --- a/step_editor/app.py +++ b/step_editor/app.py @@ -927,7 +927,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf object_edit_layout.setSpacing(4) self.property_table = QTableWidget(0, 5) self.property_table.setObjectName("propertyTable") - self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "修改方式", "目标值", "操作"]) + self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值", "操作"]) self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.property_table.setAlternatingRowColors(True) @@ -954,7 +954,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf self.property_table.setColumnWidth(4, 54) help_tip( self.property_table, - "特征模式显示当前特征及局部关联特征的可变尺寸;输入目标值后,可按行应用或使用下方按钮应用当前修改。", + "特征模式显示当前特征及局部关联特征的可变尺寸;建模意图决定这次修改是局部重建、拉伸/切除、端面移动还是整体缩放。", ) self.property_table.itemChanged.connect(self._on_property_table_item_changed) object_edit_layout.addWidget(self.property_table) @@ -1150,12 +1150,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf self.chamfer_edge_button.clicked.connect(self.chamfer_edge) edit_layout.addWidget(self.chamfer_edge_button, 17, 0, 1, 2) - edit_layout.addWidget(QLabel("Edge目标长度"), 18, 0) + edit_layout.addWidget(QLabel("Edge长度"), 18, 0) edge_length_inputs = QWidget() edge_length_layout = QHBoxLayout(edge_length_inputs) edge_length_layout.setContentsMargins(0, 0, 0, 0) self.edge_target_length_input = QLineEdit("") - help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度再执行。") + help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度后,还要确认右侧建模意图。") self.edge_start_x_input = QLineEdit("", edit_box) self.edge_start_y_input = QLineEdit("", edit_box) self.edge_start_z_input = QLineEdit("", edit_box) @@ -1192,7 +1192,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf self.edge_length_anchor_combo.setMinimumWidth(90) help_tip( self.edge_length_anchor_combo, - "选择修改Edge长度时尽量固定哪里:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。", + "选择Edge长度修改的固定基准:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。", ) self.edge_length_strategy_combo = NoWheelComboBox() self.edge_length_strategy_combo.addItem("自动选择", "auto") @@ -1204,7 +1204,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf self.edge_length_strategy_combo.setMinimumWidth(130) help_tip( self.edge_length_strategy_combo, - "选择Edge长度修改语义:自动会按可用性选择;只改当前Edge会让相邻面自然变斜;移动端面/保持垂直会让相关端面和同向边跟随;相邻圆柱直径会把圆Edge长度换算成孔/槽/凸台直径;缩放所属会让所属对象整体跟随变化。", + "选择Edge长度修改的建模意图:自动会按可用性选择;只改当前Edge会让相邻面自然变斜;移动端面/保持垂直会让相关端面和同向边跟随;相邻圆柱直径会把圆Edge长度换算成孔/槽/凸台直径;缩放所属会让所属对象整体跟随变化。", ) edge_length_layout.addWidget(self.edge_target_length_input, stretch=2) edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1) @@ -1213,7 +1213,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf self.resize_edge_length_button = QPushButton("修改Edge长度") help_tip( self.resize_edge_length_button, - "修改当前Edge长度。直线Edge优先局部形变;圆、椭圆和平面曲线会优先用局部/径向策略,必要时再用后备缩放。", + "按已选择的建模意图修改当前Edge长度。直线Edge可只改当前边或移动端面;圆Edge可转成相邻圆柱直径;缩放所属会影响其它尺寸。", ) self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length) edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2) diff --git a/step_editor/model.py b/step_editor/model.py index 000a8d7..6f5c79b 100644 --- a/step_editor/model.py +++ b/step_editor/model.py @@ -2,6 +2,7 @@ from __future__ import annotations import math from pathlib import Path +import time from typing import Callable, Iterable from OCC.Core.BRep import BRep_Tool @@ -114,6 +115,10 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd self._face_first_level_topology_cache: dict[int, dict[str, object]] = {} self._cylindrical_first_level_topology_cache: dict[int, dict[str, object]] = {} self._face_first_level_fact_cache: dict[tuple[int, str], dict[str, object]] = {} + self._edge_vertex_points_cache: dict[int, tuple[tuple[float, float, float], ...]] = {} + self._edge_vertex_key_edge_ids_cache: dict[tuple[int, int, int, int, int], set[int]] | None = None + self._edge_first_level_topology_cache: dict[int, dict[str, object]] = {} + self._edge_first_level_fact_cache: dict[int, dict[str, object]] = {} self._local_face_deform_readiness_cache: dict[int, dict[str, object]] = {} self._edge_duplicate_key_ids_cache: dict[tuple[object, ...], list[int]] | None = None self._same_domain_internal_edge_ids_cache: set[int] | None = None @@ -198,6 +203,10 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd self._face_first_level_topology_cache.clear() self._cylindrical_first_level_topology_cache.clear() self._face_first_level_fact_cache.clear() + self._edge_vertex_points_cache.clear() + self._edge_vertex_key_edge_ids_cache = None + self._edge_first_level_topology_cache.clear() + self._edge_first_level_fact_cache.clear() self._local_face_deform_readiness_cache.clear() self._edge_duplicate_key_ids_cache = None self._same_domain_internal_edge_ids_cache = None @@ -904,6 +913,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd max_depth: int = 3, max_scan_faces: int = 72, max_features: int = 10, + time_budget_seconds: float | None = None, + lightweight: bool = False, ) -> list[dict[str, object]]: """Detect editable feature candidates near the selected face. @@ -914,8 +925,73 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd """ if face_id < 0 or face_id >= len(self.faces): raise ValueError(f"Unknown face id {face_id}") + if max_features <= 0: + return [] - source_info = self.feature_info(face_id) + deadline = None + if time_budget_seconds is not None and time_budget_seconds > 0: + deadline = time.monotonic() + float(time_budget_seconds) + + def budget_expired() -> bool: + return deadline is not None and time.monotonic() >= deadline + + def selection_candidate_info(candidate_id: int) -> dict[str, object]: + cached = self.cached_feature_info(candidate_id) + if cached is not None: + return cached + info = self.quick_face_info(candidate_id) + surface = str(info.get("surface", "") or "") + result = dict(info) + result.setdefault("kind", "feature") + result.setdefault("feature_source_face_id", candidate_id) + result.setdefault("feature_face_ids", (candidate_id,)) + result.setdefault("feature_highlight_face_ids", (candidate_id,)) + if surface == "cylinder": + angular_span = _float_or_none(result.get("angular_span")) + is_partial = ( + not _is_effectively_full_cylinder(result) + and angular_span is not None + and angular_span < math.tau * 0.92 + ) + if is_partial: + result.setdefault("feature_type", "槽/半孔候选") + result.setdefault("feature_guess", "cylindrical face") + result.setdefault( + "feature_edit_actions", + "轻量探测阶段只读取半径、直径、轴线和大致高度;点击具体参数时会重新确认槽/半孔语义。", + ) + else: + result.setdefault("feature_type", "圆柱面候选") + result.setdefault("feature_guess", "cylindrical face") + result.setdefault( + "feature_edit_actions", + "轻量探测阶段只读取半径、直径、轴线和大致高度;点击具体参数时会重新确认孔、凸台或圆角语义。", + ) + elif surface in {"cone", "sphere", "torus"}: + result.setdefault("feature_type", f"{surface} 面候选") + result.setdefault("feature_edit_actions", "轻量探测阶段只读取解析曲面参数;点击具体参数时会重新生成完整编辑计划。") + elif surface == "plane": + result["feature_type"] = "相邻平面 Face 候选" + result.setdefault("feature_guess", "adjacent plane face") + result.setdefault( + "feature_edit_actions", + "轻量探测阶段显示共享边的相邻平面 Face;点击具体参数时会切换到该 Face 并重新生成完整编辑计划。", + ) + else: + result.setdefault("feature_type", "相邻几何候选") + result.setdefault("feature_edit_actions", "轻量探测阶段只显示相邻关系;暂不把它作为可修改特征。") + result.setdefault( + "feature_mode", + "相邻特征轻量探测:为保证旋转、悬停和点选流畅,选择时只读取直接邻域的基础几何;完整识别留到执行具体修改时再计算。", + ) + result["associated_feature_scan_mode"] = "lightweight" + try: + result.update(self._recognition_summary_fields(result)) + except Exception: + pass + return result + + source_info = selection_candidate_info(face_id) if lightweight else self.feature_info(face_id) source_area = max(float(source_info.get("area", 0.0) or 0.0), 1e-12) source_feature_faces = set(_int_values(source_info.get("feature_face_ids"))) or {face_id} visited = {face_id} @@ -923,12 +999,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd candidate_hops: dict[int, int] = {} while frontier and len(visited) < max_scan_faces: + if budget_expired(): + break current_id, depth = frontier.pop(0) if depth >= max_depth: continue edge_ids = self._face_boundary_edge_ids(current_id) neighbors = sorted(set(self._adjacent_face_ids_for_edges(edge_ids, current_id)) - {current_id}) for neighbor_id in neighbors: + if budget_expired(): + break candidate_hops[neighbor_id] = min(candidate_hops.get(neighbor_id, depth + 1), depth + 1) if neighbor_id in visited or len(visited) >= max_scan_faces: continue @@ -946,28 +1026,38 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd results: list[dict[str, object]] = [] seen_features: set[tuple[str, frozenset[int]]] = set() for candidate_id, hop_count in sorted(candidate_hops.items(), key=lambda item: (item[1], item[0])): + if budget_expired(): + break if candidate_id in source_feature_faces: continue try: - info = self.feature_info(candidate_id) + info = selection_candidate_info(candidate_id) if lightweight else self.feature_info(candidate_id) except Exception: continue surface = str(info.get("surface", "") or "") feature_guess = str(info.get("feature_guess", "") or "") feature_type = str(info.get("feature_type", "") or "") - is_semantic = bool( - info.get("prismatic_extrusion_status") == "candidate" - or surface in {"cone", "sphere", "torus"} - or ( - surface == "cylinder" - and feature_guess - in { - "hole/groove candidate", - "boss/outer-round candidate", - "round/fillet candidate", - } + if lightweight: + is_semantic = bool( + info.get("prismatic_extrusion_status") == "candidate" + or surface in {"plane", "cylinder", "cone", "sphere", "torus"} + ) + if surface == "plane" and hop_count > 1: + is_semantic = False + else: + is_semantic = bool( + info.get("prismatic_extrusion_status") == "candidate" + or surface in {"cone", "sphere", "torus"} + or ( + surface == "cylinder" + and feature_guess + in { + "hole/groove candidate", + "boss/outer-round candidate", + "round/fillet candidate", + } + ) ) - ) if not is_semantic: continue identity_face_ids = _int_values(info.get("feature_face_ids")) @@ -982,11 +1072,15 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd continue seen_features.add(identity) related = dict(info) + association_label = "" + if lightweight and surface == "plane": + association_label = f"相邻平面 Face {candidate_id}" related.update( { "association_source_face_id": candidate_id, "association_hop_count": hop_count, "association_relation": "shared-edge-topology", + "association_label": association_label, "association_priority": ( 0 if surface == "cylinder" else (1 if surface in {"cone", "sphere", "torus"} else 2) ), @@ -2869,6 +2963,183 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd matches.append(face_id) return matches + def _edge_vertex_points(self, edge_id: int) -> tuple[tuple[float, float, float], ...]: + if edge_id < 0 or edge_id >= len(self.edges): + return () + cached = self._edge_vertex_points_cache.get(edge_id) + if cached is not None: + return tuple(cached) + + diagonal = _shape_diagonal(self.shape) + tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3) + points_by_key: dict[tuple[int, int, int], tuple[float, float, float]] = {} + explorer = TopExp_Explorer(self.edges[edge_id], TopAbs_VERTEX) + while explorer.More(): + vertex = topods.Vertex(explorer.Current()) + point = _point_tuple(BRep_Tool.Pnt(vertex)) + points_by_key[self._local_point_key(point, tolerance)] = point + explorer.Next() + points = tuple(points_by_key[key] for key in sorted(points_by_key)) + self._edge_vertex_points_cache[edge_id] = points + return points + + def _edge_vertex_key_edge_ids(self) -> dict[tuple[int, int, int, int, int], set[int]]: + cached = self._edge_vertex_key_edge_ids_cache + if cached is not None: + return cached + + diagonal = _shape_diagonal(self.shape) + tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3) + mapping: dict[tuple[int, int, int, int, int], set[int]] = {} + for edge_id in range(len(self.edges)): + part_id = self.edge_part_ids[edge_id] if edge_id < len(self.edge_part_ids) else -1 + solid_id = self._edge_solid_id(edge_id) + for point in self._edge_vertex_points(edge_id): + key = (int(part_id), int(solid_id), *self._local_point_key(point, tolerance)) + mapping.setdefault(key, set()).add(edge_id) + self._edge_vertex_key_edge_ids_cache = mapping + return mapping + + def _edge_shared_vertex_adjacent_edge_ids(self, edge_id: int) -> tuple[int, ...]: + if edge_id < 0 or edge_id >= len(self.edges): + return () + diagonal = _shape_diagonal(self.shape) + tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3) + part_id = self.edge_part_ids[edge_id] if edge_id < len(self.edge_part_ids) else -1 + solid_id = self._edge_solid_id(edge_id) + mapping = self._edge_vertex_key_edge_ids() + adjacent: set[int] = set() + for point in self._edge_vertex_points(edge_id): + key = (int(part_id), int(solid_id), *self._local_point_key(point, tolerance)) + adjacent.update(mapping.get(key, set())) + adjacent.discard(edge_id) + return tuple(sorted(item for item in adjacent if 0 <= item < len(self.edges))) + + def edge_first_level_topology(self, edge_id: int) -> dict[str, object]: + """Return the explicit first-level B-Rep neighborhood for an Edge.""" + if edge_id < 0 or edge_id >= len(self.edges): + raise ValueError(f"Unknown edge id {edge_id}") + cached = self._edge_first_level_topology_cache.get(edge_id) + if cached is not None: + return dict(cached) + + source_part_id = self.edge_part_ids[edge_id] if edge_id < len(self.edge_part_ids) else -1 + source_solid_id = self._edge_solid_id(edge_id) + endpoint_points = self._edge_vertex_points(edge_id) + adjacent_edge_ids = self._edge_shared_vertex_adjacent_edge_ids(edge_id) + adjacent_face_ids = tuple(self._edge_adjacent_face_ids(edge_id)) + adjacent_surface_types = tuple((face_id, self.face_surface_kind(face_id)) for face_id in adjacent_face_ids) + + adjacent_face_boundary_edges: set[int] = set() + shared_edges_by_face: list[dict[str, object]] = [] + for face_id in adjacent_face_ids: + boundary_edge_ids = tuple(self._face_boundary_edge_ids(face_id)) + adjacent_face_boundary_edges.update(boundary_edge_ids) + shared_edges_by_face.append( + { + "face_id": face_id, + "edge_ids": (edge_id,), + "edge_count": 1, + "surface": self.face_surface_kind(face_id), + "face_boundary_edge_ids": boundary_edge_ids, + } + ) + + first_level_edge_ids = tuple(sorted({edge_id, *adjacent_edge_ids})) + topology = { + "topology_relation_model": "STEP/B-Rep edge first-level", + "topology_relation_depth": 1, + "topology_relation_scope": "selected Edge + endpoint Vertices + shared-endpoint Edges + direct incident Faces", + "topology_relation_boundary": "shared-vertex/shared-face", + "topology_ignored_relation_depths": ("second-level", "third-level", "deeper"), + "topology_ignored_relation_note": ( + "Edge first-level topology stops at endpoint Vertices, shared-endpoint Edges, " + "and Faces that directly use the selected Edge. Edges/Faces reached through those Faces " + "are second-level or deeper and are not propagated automatically in this stage." + ), + "source_edge_id": edge_id, + "source_part_id": source_part_id, + "source_solid_id": source_solid_id, + "selected_edge_id": edge_id, + "selected_edge_ids": (edge_id,), + "selected_edge_count": 1, + "first_level_vertex_points": endpoint_points, + "first_level_vertex_count": len(endpoint_points), + "first_level_adjacent_edge_ids": adjacent_edge_ids, + "first_level_adjacent_edge_count": len(adjacent_edge_ids), + "first_level_edge_ids": first_level_edge_ids, + "first_level_edge_count": len(first_level_edge_ids), + "first_level_adjacent_face_ids": adjacent_face_ids, + "first_level_adjacent_face_count": len(adjacent_face_ids), + "first_level_adjacent_surface_types": adjacent_surface_types, + "first_level_shared_edges_by_face": tuple(shared_edges_by_face), + "first_level_adjacent_face_boundary_edge_ids": tuple(sorted(adjacent_face_boundary_edges)), + "first_level_adjacent_face_boundary_edge_count": len(adjacent_face_boundary_edges), + "first_level_topology_note": ( + f"Selected Edge 1, endpoint Vertex {len(endpoint_points)}, " + f"shared-endpoint Edge {len(adjacent_edge_ids)}, direct adjacent Face {len(adjacent_face_ids)}. " + "Second-level and deeper propagation is not automatic in this stage." + ), + } + self._edge_first_level_topology_cache[edge_id] = dict(topology) + return dict(topology) + + def edge_first_level_facts(self, edge_id: int) -> dict[str, object]: + """Return a unified first-level fact graph for Edge recognition and edit plans.""" + if edge_id < 0 or edge_id >= len(self.edges): + raise ValueError(f"Unknown edge id {edge_id}") + cached = self._edge_first_level_fact_cache.get(edge_id) + if cached is not None: + return dict(cached) + + topology = self.edge_first_level_topology(edge_id) + endpoint_points = tuple(topology.get("first_level_vertex_points") or ()) + adjacent_edge_ids = tuple(_int_values(topology.get("first_level_adjacent_edge_ids"))) + adjacent_face_ids = tuple(_int_values(topology.get("first_level_adjacent_face_ids"))) + included_edge_ids = tuple(_int_values(topology.get("first_level_edge_ids"))) or (edge_id,) + ignored_depths = tuple(topology.get("topology_ignored_relation_depths") or ("second-level", "third-level")) + role_groups = ( + {"role": "selected-edge", "edge_ids": (edge_id,), "count": 1}, + {"role": "endpoint-vertices", "vertex_points": endpoint_points, "count": len(endpoint_points)}, + {"role": "shared-endpoint-edges", "edge_ids": adjacent_edge_ids, "count": len(adjacent_edge_ids)}, + {"role": "direct-incident-faces", "face_ids": adjacent_face_ids, "count": len(adjacent_face_ids)}, + ) + facts = { + "first_level_fact_model": "STEP/B-Rep first-level fact graph", + "first_level_fact_source_model": "edge", + "first_level_fact_status": "ready", + "first_level_fact_relation_depth": 1, + "first_level_fact_relation_boundary": "shared-vertex/shared-face", + "first_level_fact_scope": "edge", + "first_level_fact_subject_role": "selected Edge", + "first_level_fact_subject_edge_ids": (edge_id,), + "first_level_fact_subject_edge_count": 1, + "first_level_fact_boundary_edge_ids": (edge_id,), + "first_level_fact_boundary_edge_count": 1, + "first_level_fact_boundary_vertex_points": endpoint_points, + "first_level_fact_boundary_vertex_count": len(endpoint_points), + "first_level_fact_adjacent_edge_ids": adjacent_edge_ids, + "first_level_fact_adjacent_edge_count": len(adjacent_edge_ids), + "first_level_fact_adjacent_face_ids": adjacent_face_ids, + "first_level_fact_adjacent_face_count": len(adjacent_face_ids), + "first_level_fact_adjacent_surface_types": tuple(topology.get("first_level_adjacent_surface_types") or ()), + "first_level_fact_shared_edges_by_face": tuple(topology.get("first_level_shared_edges_by_face") or ()), + "first_level_fact_included_edge_ids": included_edge_ids, + "first_level_fact_included_edge_count": len(included_edge_ids), + "first_level_fact_included_face_ids": adjacent_face_ids, + "first_level_fact_included_face_count": len(adjacent_face_ids), + "first_level_fact_role_groups": role_groups, + "first_level_fact_ignored_relation_depths": ignored_depths, + "first_level_fact_ignored_relation_note": topology.get("topology_ignored_relation_note", ""), + "first_level_fact_summary": ( + f"selected Edge 1, endpoint Vertex {len(endpoint_points)}, " + f"shared-endpoint Edge {len(adjacent_edge_ids)}, direct adjacent Face {len(adjacent_face_ids)}; " + "deeper relations are recorded as future propagation targets, not edited automatically." + ), + } + self._edge_first_level_fact_cache[edge_id] = dict(facts) + return dict(facts) + def edge_info(self, edge_id: int) -> dict[str, object]: if edge_id in self._edge_info_cache: return dict(self._edge_info_cache[edge_id]) diff --git a/step_editor/operations.py b/step_editor/operations.py index 5f35ebd..e4a5bab 100644 --- a/step_editor/operations.py +++ b/step_editor/operations.py @@ -49,8 +49,10 @@ from OCC.Core.GeomAbs import ( ) from OCC.Core.GProp import GProp_GProps from OCC.Core.GC import GC_MakeArcOfCircle +from OCC.Core.GeomAPI import GeomAPI_PointsToBSplineSurface from OCC.Core.ShapeFix import ShapeFix_Shape from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain +from OCC.Core.TColgp import TColgp_Array2OfPnt from OCC.Core.TopAbs import ( TopAbs_EDGE, TopAbs_EXTERNAL, @@ -342,6 +344,28 @@ class OperationMixin: f"edges {before_part_stats.edges}->{after_part_stats.edges}." ) + def _edge_first_level_plan_fields(self, edge_id: int) -> dict[str, object]: + try: + return self.edge_first_level_facts(edge_id) + except Exception as exc: + return { + "first_level_fact_model": "STEP/B-Rep first-level fact graph", + "first_level_fact_source_model": "edge", + "first_level_fact_status": "unavailable", + "first_level_fact_relation_depth": 1, + "first_level_fact_relation_boundary": "shared-vertex/shared-face", + "first_level_fact_scope": "edge", + "first_level_fact_subject_role": "selected Edge", + "first_level_fact_subject_edge_ids": (edge_id,), + "first_level_fact_subject_edge_count": 1, + "first_level_fact_boundary_edge_count": 1, + "first_level_fact_boundary_vertex_count": 0, + "first_level_fact_adjacent_edge_count": 0, + "first_level_fact_adjacent_face_count": 0, + "first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"), + "first_level_fact_summary": f"Edge first-level facts are temporarily unavailable: {exc}", + } + def edge_fillet_plan(self, edge_id: int, radius: float) -> dict[str, object]: if edge_id < 0 or edge_id >= len(self.edges): raise ValueError(f"Unknown edge id {edge_id}") @@ -366,7 +390,7 @@ class OperationMixin: length = float(info.get("length", 0.0)) radius_to_length_ratio = radius / max(length, 1e-9) - return { + plan = { "status": readiness["fillet_status"], "risk": readiness["fillet_risk"], "message": readiness["fillet_note"], @@ -391,6 +415,8 @@ class OperationMixin: "不是修改已有圆角面。" ), } + plan.update(self._edge_first_level_plan_fields(edge_id)) + return plan def edge_chamfer_plan(self, edge_id: int, distance: float) -> dict[str, object]: if edge_id < 0 or edge_id >= len(self.edges): @@ -416,7 +442,7 @@ class OperationMixin: length = float(info.get("length", 0.0)) distance_to_length_ratio = distance / max(length, 1e-9) - return { + plan = { "status": readiness["chamfer_status"], "risk": readiness["chamfer_risk"], "message": readiness["chamfer_note"], @@ -440,6 +466,8 @@ class OperationMixin: "在当前直线 Edge 及其相邻 Face 上调用 OCCT 对称倒角;会替换这条边附近的局部拓扑。" ), } + plan.update(self._edge_first_level_plan_fields(edge_id)) + return plan def edge_asymmetric_chamfer_plan( self, @@ -669,6 +697,7 @@ class OperationMixin: "end_point": info.get("end_point"), "length_center": info.get("length_center"), } + base.update(self._edge_first_level_plan_fields(edge_id)) warnings: list[str] = [ "Edge长度修改基于当前 STEP/B-Rep 结果几何,不是 CAD 建模历史里的参数编辑。" ] @@ -693,7 +722,7 @@ class OperationMixin: warnings.append("长度变化超过当前Edge长度的 25%,请确认预览范围。") if not blockers and force_local and curve != "line": - blockers.append("“只变当前Edge”策略当前只支持直线Edge。") + blockers.append("“只改当前Edge”策略当前只支持直线Edge。") if not blockers and force_end_face and curve != "line": blockers.append("“移动端面/整体尺寸”策略当前只支持直线Edge。") if not blockers and force_cylinder and curve != "circle": @@ -711,7 +740,7 @@ class OperationMixin: risk = _max_risk(risk, str(local_candidate.get("local_edge_deform_risk", "medium"))) status = "caution" if risk != "low" else status warnings.append( - "将优先只移动当前 Edge 的端点并重建相邻平面;非共面的四边面会拆成三角面。" + "将优先只移动当前 Edge 的端点并重建相邻平面;四点非共面面会优先重建为连续曲面。" ) base.update(local_candidate) elif local_skip_note: @@ -945,7 +974,7 @@ class OperationMixin: "local_edge_deform_moved_endpoint_count": 2 if anchor_mode == "center" else 1, "local_edge_deform_risk": local_risk, "local_edge_deform_note": ( - "只移动当前 Edge 的端点并重建所属平面多面体;非共面 Face 会被拆成三角面。" + "只移动当前 Edge 的端点并重建所属平面多面体;四点非共面 Face 会优先重建为连续曲面。" ), "part_solid_count": part_solid_count, }, @@ -999,6 +1028,7 @@ class OperationMixin: "length_center": info.get("length_center"), "resize_strategy": "local-edge-endpoint-deform", } + base.update(self._edge_first_level_plan_fields(edge_id)) warnings = [ "Edge endpoint coordinate edit rebuilds the current STEP/B-Rep result geometry; it is not recovered CAD history." ] @@ -1082,7 +1112,7 @@ class OperationMixin: ) risk = _max_risk(risk, str(local_candidate.get("local_edge_deform_risk", "medium"))) warnings.append( - "The selected Edge endpoint will be moved and the surrounding planar faces will be rebuilt; non-planar faces may be split into triangles." + "The selected Edge endpoint will be moved and the surrounding planar faces will be rebuilt; four-point non-planar faces are rebuilt as continuous surfaces when possible." ) if blockers: @@ -1136,6 +1166,7 @@ class OperationMixin: "target_edge_center": target, "resize_strategy": "local-edge-center-deform", } + base.update(self._edge_first_level_plan_fields(edge_id)) warnings = [ "Edge center coordinate edit rebuilds the current STEP/B-Rep result geometry; it is not recovered CAD history." ] @@ -1205,7 +1236,7 @@ class OperationMixin: ) risk = _max_risk(risk, str(local_candidate.get("local_edge_deform_risk", "medium"))) warnings.append( - "Both endpoints of the selected Edge will be moved and the surrounding planar faces will be rebuilt; non-planar faces may be split into triangles." + "Both endpoints of the selected Edge will be moved and the surrounding planar faces will be rebuilt; four-point non-planar faces are rebuilt as continuous surfaces when possible." ) if blockers: @@ -1324,7 +1355,7 @@ class OperationMixin: if strategy == "local-edge-only-deform": return ( "只改当前Edge:只移动被选Edge的端点并重建相邻平面;相邻面会自然变斜," - "必要时非共面四边面会拆成三角面,整体端面不会一起平移。" + "四点非共面面会优先重建为连续曲面,整体端面不会一起平移。" ) if strategy == "move-edge-end-plane-by-push-pull": end_face = plan.get("end_face_id") @@ -1557,6 +1588,7 @@ class OperationMixin: } if candidate: result.update(candidate) + result.update(self._edge_first_level_plan_fields(edge_id)) return result def _circular_edge_axis_move_cylinder_candidate( @@ -2019,7 +2051,7 @@ class OperationMixin: warnings.append(f"目标{axis_label}变化超过当前值的 50%,周边几何变形或修复失败的概率较高。") elif delta_ratio > 0.25: risk = _max_risk(risk, "medium") - warnings.append(f"目标{axis_label}变化超过当前值的 25%,请确认影响范围。") + warnings.append(f"目标{axis_label}变化超过当前值的 25%,请确认建模意图。") part_id = int(info.get("part_id", -1)) solid_id = int(info.get("solid_id", -1)) @@ -2042,7 +2074,7 @@ class OperationMixin: target_major = target_radius if axis_kind == "major" else info.get("major_radius") target_minor = target_radius if axis_kind == "minor" else info.get("minor_radius") - return { + plan = { "edge_id": edge_id, "part_id": part_id, "solid_id": solid_id, @@ -2081,6 +2113,8 @@ class OperationMixin: "affine_target_kind": target_kind, "part_solid_count": part_solid_count, } + plan.update(self._edge_first_level_plan_fields(edge_id)) + return plan def resize_ellipse_edge_axis_radius( self, @@ -2857,7 +2891,7 @@ class OperationMixin: except (TypeError, ValueError): distance = 0.0 frame = None - blockers = ["目标偏移曲面必须是数字。"] + blockers = ["目标偏移变换位置必须是数字。"] else: frame = self.face_plane_offset_frame(face_id) blockers = [] @@ -2865,7 +2899,7 @@ class OperationMixin: if face_id < 0 or face_id >= len(self.faces): blockers.append(f"Unknown face id {face_id}") if frame is None: - blockers.append("当前 Face 缺少稳定平面方向或基准点,不能执行偏移曲面(局部重建)。") + blockers.append("当前 Face 缺少稳定平面方向或基准点,不能执行偏移变换(局部重建)。") current_center = None target_center = None @@ -2905,8 +2939,8 @@ class OperationMixin: "target_face_center": target_center, "face_center_move_vector": move_vector, "resize_strategy": "local-face-plane-offset-deform", - "edit_strategy_label": "偏移曲面(局部重建)", - "edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的移动量,移动当前 Face 并重建相邻平面。", + "edit_strategy_label": "偏移变换(局部重建)", + "edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 并重建相邻平面。", } plan = self.face_center_local_move_plan(face_id, target_center) @@ -2918,9 +2952,9 @@ class OperationMixin: "plane_direction": plane_direction, "plane_offset_distance": distance, "resize_strategy": "local-face-plane-offset-deform", - "edit_strategy_label": "偏移曲面(局部重建)", + "edit_strategy_label": "偏移变换(局部重建)", "edit_semantics": ( - "把目标偏移曲面换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;" + "把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;" "不拉伸/切除加料/切削,也不平移所属对象。" ), } @@ -2933,7 +2967,7 @@ class OperationMixin: except (TypeError, ValueError): distance = 0.0 frame = None - blockers = ["目标偏移曲面必须是数字。"] + blockers = ["目标偏移变换位置必须是数字。"] else: frame = self.face_plane_offset_frame(face_id) blockers = [] @@ -2944,7 +2978,7 @@ class OperationMixin: else: info = self.face_info(face_id) if frame is None: - blockers.append("当前 Face 缺少稳定平面方向或基准点,不能按偏移曲面平移所属对象。") + blockers.append("当前 Face 缺少稳定平面方向或基准点,不能按偏移变换平移所属对象。") plane_origin = None plane_direction = None @@ -2987,8 +3021,8 @@ class OperationMixin: "translation_vector": vector, "resize_strategy": "translate-owning-shape-from-plane-offset", "translate_strategy": f"translate-{target_kind}", - "edit_strategy_label": "按偏移曲面平移所属对象", - "edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid。", + "edit_strategy_label": "按偏移变换平移所属对象", + "edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid。", } plan = self.translate_solid_plan(solid_id, vector) if target_kind == "solid" else self.translate_part_plan(part_id, vector) @@ -2997,7 +3031,7 @@ class OperationMixin: if diagonal > 1e-9: distance_ratio = abs(distance) / diagonal if distance_ratio > 5.0: - blocker = "目标偏移曲面需要移动的距离超过所属对象尺寸的 5 倍,容易把特征移动到远离模型的位置。" + blocker = "目标偏移变换位置需要移动的距离超过所属对象尺寸的 5 倍,容易把特征移动到远离模型的位置。" blockers = [part for part in str(plan.get("blockers") or "").split(";") if part] blockers.append(blocker) plan["status"] = "blocked" @@ -3017,8 +3051,8 @@ class OperationMixin: "face_offset_distance_ratio": distance_ratio, "resize_strategy": "translate-owning-shape-from-plane-offset", "translate_strategy": f"translate-{target_kind}", - "edit_strategy_label": "按偏移曲面平移所属对象", - "edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。", + "edit_strategy_label": "按偏移变换平移所属对象", + "edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。", } ) return plan @@ -3466,7 +3500,7 @@ class OperationMixin: for face in solid_faces: surf = BRepAdaptor_Surface(face) if surf.GetType() != GeomAbs_Plane: - blockers.append("局部 Face 移动暂只支持全平面实体;含曲面的模型请使用其它编辑方式。") + blockers.append("局部 Face 移动暂只支持全平面实体;含曲面的模型请使用其它建模意图。") break if len(_explore(face, TopAbs_WIRE)) != 1: blockers.append("局部 Face 移动暂不处理带内孔的 Face;请使用孔/槽专门入口。") @@ -6762,67 +6796,94 @@ class OperationMixin: ) if plan["status"] == "blocked": raise ValueError(str(plan["message"])) + part_id = int(plan.get("part_id", -1)) + part = self.part_by_id(part_id) if part_id >= 0 else None + old_part_shape = part.shape if part is not None else None if plan.get("resize_strategy") == "local-edge-only-deform": - self._apply_local_edge_deform(plan) - result_check = self._edge_length_result_summary(plan) - return ( - "Edge length resize completed by local edge-only deformation: " - f"edge {edge_id}, current_length={float(plan['current_length']):g}, " - f"target_length={float(plan['target_length']):g}, " - f"delta={float(plan['delta_length']):g}, " - f"anchor={plan.get('local_edge_deform_anchor')}, " - f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, " - f"target={plan.get('local_edge_deform_target_kind')}, " - f"risk={plan['risk']}. {result_check}" - ) + try: + self._apply_local_edge_deform(plan) + result_check = self._edge_length_result_summary_or_raise(plan) + return ( + "Edge length resize completed by local edge-only deformation: " + f"edge {edge_id}, current_length={float(plan['current_length']):g}, " + f"target_length={float(plan['target_length']):g}, " + f"delta={float(plan['delta_length']):g}, " + f"anchor={plan.get('local_edge_deform_anchor')}, " + f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, " + f"target={plan.get('local_edge_deform_target_kind')}, " + f"risk={plan['risk']}. {result_check}" + ) + except Exception: + if part is not None and old_part_shape is not None: + part.shape = old_part_shape + self.refresh_topology() + raise if plan.get("resize_strategy") == "move-edge-end-plane-by-push-pull": - push_result = self.push_pull_face(int(plan["end_face_id"]), float(plan["push_pull_distance"])) - result_check = self._edge_length_result_summary(plan) - return ( - "Edge length resize completed by end-face push/pull: " - f"edge {edge_id}, current_length={float(plan['current_length']):g}, " - f"target_length={float(plan['target_length']):g}, " - f"delta={float(plan['delta_length']):g}, " - f"end_face={int(plan['end_face_id'])}, " - f"push_pull_distance={float(plan['push_pull_distance']):g}, " - f"anchor={plan.get('edge_length_anchor_label')}, " - f"risk={plan['risk']}. {result_check} {push_result}" - ) + try: + push_result = self.push_pull_face(int(plan["end_face_id"]), float(plan["push_pull_distance"])) + result_check = self._edge_length_result_summary_or_raise(plan) + return ( + "Edge length resize completed by end-face push/pull: " + f"edge {edge_id}, current_length={float(plan['current_length']):g}, " + f"target_length={float(plan['target_length']):g}, " + f"delta={float(plan['delta_length']):g}, " + f"end_face={int(plan['end_face_id'])}, " + f"push_pull_distance={float(plan['push_pull_distance']):g}, " + f"anchor={plan.get('edge_length_anchor_label')}, " + f"risk={plan['risk']}. {result_check} {push_result}" + ) + except Exception: + if part is not None and old_part_shape is not None: + part.shape = old_part_shape + self.refresh_topology() + raise if plan.get("resize_strategy") == "resize-adjacent-cylinder-from-circular-edge-length": - face_id = int(plan["cylinder_resize_face_id"]) - target_diameter = float(plan["cylinder_resize_target_diameter"]) - if plan.get("circular_edge_cylinder_mode") == "boss": - resize_result = self.resize_cylindrical_boss(face_id, target_diameter) - else: - resize_result = self.resize_cylindrical_hole(face_id, target_diameter) - result_check = self._edge_length_result_summary(plan) + try: + face_id = int(plan["cylinder_resize_face_id"]) + target_diameter = float(plan["cylinder_resize_target_diameter"]) + if plan.get("circular_edge_cylinder_mode") == "boss": + resize_result = self.resize_cylindrical_boss(face_id, target_diameter) + else: + resize_result = self.resize_cylindrical_hole(face_id, target_diameter) + result_check = self._edge_length_result_summary_or_raise(plan) + return ( + "Edge length resize completed by adjacent cylinder diameter edit: " + f"edge {edge_id}, current_length={float(plan['current_length']):g}, " + f"target_length={float(plan['target_length']):g}, " + f"delta={float(plan['delta_length']):g}, " + f"cylinder_face={face_id}, " + f"target_diameter={target_diameter:g}, " + f"mode={plan.get('circular_edge_cylinder_mode_label')}, " + f"risk={plan['risk']}. {result_check} {resize_result}" + ) + except Exception: + if part is not None and old_part_shape is not None: + part.shape = old_part_shape + self.refresh_topology() + raise + + try: + self._apply_edge_length_affine_transform(plan) + result_check = self._edge_length_result_summary_or_raise(plan) return ( - "Edge length resize completed by adjacent cylinder diameter edit: " + "Edge length resize completed by geometric scale fallback: " f"edge {edge_id}, current_length={float(plan['current_length']):g}, " f"target_length={float(plan['target_length']):g}, " f"delta={float(plan['delta_length']):g}, " - f"cylinder_face={face_id}, " - f"target_diameter={target_diameter:g}, " - f"mode={plan.get('circular_edge_cylinder_mode_label')}, " - f"risk={plan['risk']}. {result_check} {resize_result}" + f"scale={float(plan['affine_scale']):g}, " + f"transform={plan.get('affine_transform_label') or plan.get('affine_transform_kind')}, " + f"predicted_edge_length={float(plan.get('affine_predicted_edge_length') or 0.0):g}, " + f"axis_source={plan.get('affine_axis_source')}, " + f"anchor={plan.get('edge_length_anchor_label')}, " + f"target={plan.get('affine_target_kind')}, " + f"risk={plan['risk']}. {result_check}" ) - - self._apply_edge_length_affine_transform(plan) - result_check = self._edge_length_result_summary(plan) - return ( - "Edge length resize completed by geometric scale fallback: " - f"edge {edge_id}, current_length={float(plan['current_length']):g}, " - f"target_length={float(plan['target_length']):g}, " - f"delta={float(plan['delta_length']):g}, " - f"scale={float(plan['affine_scale']):g}, " - f"transform={plan.get('affine_transform_label') or plan.get('affine_transform_kind')}, " - f"predicted_edge_length={float(plan.get('affine_predicted_edge_length') or 0.0):g}, " - f"axis_source={plan.get('affine_axis_source')}, " - f"anchor={plan.get('edge_length_anchor_label')}, " - f"target={plan.get('affine_target_kind')}, " - f"risk={plan['risk']}. {result_check}" - ) + except Exception: + if part is not None and old_part_shape is not None: + part.shape = old_part_shape + self.refresh_topology() + raise def move_edge_endpoint( self, @@ -6836,22 +6897,31 @@ class OperationMixin: if plan.get("resize_strategy") != "local-edge-endpoint-deform": raise ValueError("Unsupported Edge endpoint move strategy.") - self._apply_local_edge_deform(plan) - result_check = self._edge_length_result_summary(plan) - return ( - "Edge endpoint move completed by local edge deformation: " - f"edge {edge_id}, " - f"endpoint={plan.get('edge_endpoint_role')}, " - f"current_endpoint={plan.get('current_endpoint_point')}, " - f"target_endpoint={plan.get('target_endpoint_point')}, " - f"move={plan.get('moved_endpoint_delta')}, " - f"current_length={float(plan['current_length']):g}, " - f"target_length={float(plan['target_length']):g}, " - f"delta={float(plan['delta_length']):g}, " - f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, " - f"target={plan.get('local_edge_deform_target_kind')}, " - f"risk={plan['risk']}. {result_check}" - ) + part_id = int(plan.get("part_id", -1)) + part = self.part_by_id(part_id) if part_id >= 0 else None + old_part_shape = part.shape if part is not None else None + try: + self._apply_local_edge_deform(plan) + result_check = self._edge_length_result_summary_or_raise(plan) + return ( + "Edge endpoint move completed by local edge deformation: " + f"edge {edge_id}, " + f"endpoint={plan.get('edge_endpoint_role')}, " + f"current_endpoint={plan.get('current_endpoint_point')}, " + f"target_endpoint={plan.get('target_endpoint_point')}, " + f"move={plan.get('moved_endpoint_delta')}, " + f"current_length={float(plan['current_length']):g}, " + f"target_length={float(plan['target_length']):g}, " + f"delta={float(plan['delta_length']):g}, " + f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, " + f"target={plan.get('local_edge_deform_target_kind')}, " + f"risk={plan['risk']}. {result_check}" + ) + except Exception: + if part is not None and old_part_shape is not None: + part.shape = old_part_shape + self.refresh_topology() + raise def move_edge_center( self, @@ -6864,19 +6934,28 @@ class OperationMixin: if plan.get("resize_strategy") != "local-edge-center-deform": raise ValueError("Unsupported Edge center move strategy.") - self._apply_local_edge_deform(plan) - result_check = self._edge_length_result_summary(plan) - return ( - "Edge center move completed by local edge deformation: " - f"edge {edge_id}, " - f"current_center={plan.get('current_edge_center')}, " - f"target_center={plan.get('target_edge_center')}, " - f"move={plan.get('moved_edge_center_delta')}, " - f"edge_length={float(plan['current_length']):g}, " - f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, " - f"target={plan.get('local_edge_deform_target_kind')}, " - f"risk={plan['risk']}. {result_check}" - ) + part_id = int(plan.get("part_id", -1)) + part = self.part_by_id(part_id) if part_id >= 0 else None + old_part_shape = part.shape if part is not None else None + try: + self._apply_local_edge_deform(plan) + result_check = self._edge_length_result_summary_or_raise(plan) + return ( + "Edge center move completed by local edge deformation: " + f"edge {edge_id}, " + f"current_center={plan.get('current_edge_center')}, " + f"target_center={plan.get('target_edge_center')}, " + f"move={plan.get('moved_edge_center_delta')}, " + f"edge_length={float(plan['current_length']):g}, " + f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, " + f"target={plan.get('local_edge_deform_target_kind')}, " + f"risk={plan['risk']}. {result_check}" + ) + except Exception: + if part is not None and old_part_shape is not None: + part.shape = old_part_shape + self.refresh_topology() + raise def face_center_local_move_preview_polydata( self, @@ -7255,12 +7334,29 @@ class OperationMixin: action: Callable[[], object], ) -> tuple[object, str]: snapshot = self._face_edit_snapshot(plan) + previous_logical_ids = tuple(getattr(self, "face_logical_ids", ())) + previous_faces = tuple(self.faces) + target_logical_id = _int_or_none(plan.get("target_logical_id")) + if target_logical_id is None: + plan_face_id = _int_or_none(plan.get("face_id")) + if plan_face_id is not None and 0 <= plan_face_id < len(self.faces): + try: + target_logical_id = self.face_region_logical_id(plan_face_id) + except Exception: + target_logical_id = None + excluded_logical_ids = (target_logical_id,) try: action_result = action() + self._apply_preserved_face_logical_ids_by_shape_identity( + previous_faces, + previous_logical_ids, + excluded_logical_ids=excluded_logical_ids, + ) result_check = self._checked_face_edit_result_summary(plan) return action_result, result_check except Exception as exc: self._restore_face_edit_snapshot(snapshot) + self._restore_face_logical_ids_if_count_matches(previous_logical_ids) raise RuntimeError(f"Face edit failed and the model was restored: {exc}") from exc def _face_edit_result_summary( @@ -7579,6 +7675,41 @@ class OperationMixin: f"scope={check['scope']}." ) + def _edge_length_result_summary_or_raise(self, plan: dict[str, object]) -> str: + check = self._edge_length_result_check(plan) + if check is None: + raise RuntimeError("Edge长度编辑结果无法校验,模型已恢复到修改前状态。") + target_length = float(plan.get("target_length", 0.0) or 0.0) + length_tolerance = max(_shape_diagonal(self.shape) * 1e-5, target_length * 1e-4, 1e-5) + if float(check.get("target_error", 0.0) or 0.0) > length_tolerance: + raise RuntimeError( + "Edge长度编辑结果没有达到目标值,模型已恢复到修改前状态。" + f"最近Edge {check.get('edge_id')} 的长度约 {float(check.get('nearest_length', 0.0) or 0.0):g}," + f"目标长度 {target_length:g},误差 {float(check.get('target_error', 0.0) or 0.0):g}。" + ) + + expected = self._edge_length_expected_endpoints(plan) + endpoint_error = float(check.get("endpoint_error", -1.0) or -1.0) + if expected is not None and endpoint_error >= 0.0: + endpoint_tolerance = max(_shape_diagonal(self.shape) * 1e-4, target_length * 1e-3, 1e-4) + if endpoint_error > endpoint_tolerance: + raise RuntimeError( + "Edge长度编辑后的目标端点位置不符合所选建模意图,模型已恢复到修改前状态。" + f"最近Edge {check.get('edge_id')} 的端点误差约 {endpoint_error:g}," + f"允许误差 {endpoint_tolerance:g}。" + ) + + return ( + "Result check: " + f"match={check['match_method']}, " + f"nearest_edge={check['edge_id']}, " + f"nearest_length={float(check['nearest_length']):g}, " + f"target_error={float(check['target_error']):g}, " + f"relative_error={float(check['relative_error']):g}, " + f"endpoint_error={float(check['endpoint_error']):g}, " + f"scope={check['scope']}." + ) + def _edge_length_result_check(self, plan: dict[str, object]) -> dict[str, object] | None: try: target_length = float(plan.get("target_length", 0.0)) @@ -7788,6 +7919,8 @@ class OperationMixin: points = self._orient_local_polygon_outward(points, center) if self._local_points_are_planar(points, tolerance): moved_faces.append(self._make_local_polygon_face(points)) + elif len(points) == 4: + moved_faces.append(self._make_local_bilinear_quad_face(points, tolerance)) else: for index in range(1, len(points) - 1): triangle = [points[0], points[index], points[index + 1]] @@ -8042,6 +8175,89 @@ class OperationMixin: if region_ids: self.assign_logical_face_region_exclusive(int(logical_id), region_ids) + def _apply_preserved_face_logical_ids_by_shape_identity( + self, + previous_faces: Iterable[TopoDS_Shape], + previous_logical_ids: Iterable[int], + *, + excluded_logical_ids: Iterable[int | None] = (), + ) -> int: + excluded = {int(item) for item in excluded_logical_ids if item is not None} + old_faces = list(previous_faces) + old_logical_ids = [int(item) for item in previous_logical_ids] + if not old_faces or len(old_faces) != len(old_logical_ids): + return 0 + + unmatched_new_ids: set[int] = set(range(len(self.faces))) + pending: list[tuple[TopoDS_Shape, int]] = [] + matched_by_logical_id: dict[int, set[int]] = {} + + for old_face_id, old_face in enumerate(old_faces): + logical_id = old_logical_ids[old_face_id] + if logical_id in excluded: + continue + if ( + old_face_id < len(self.faces) + and old_face_id in unmatched_new_ids + and _same_shape(old_face, self.faces[old_face_id]) + ): + matched_by_logical_id.setdefault(logical_id, set()).add(old_face_id) + unmatched_new_ids.remove(old_face_id) + else: + pending.append((old_face, logical_id)) + + for old_face, logical_id in pending: + if logical_id in excluded: + continue + match_id: int | None = None + for new_face_id in tuple(unmatched_new_ids): + if _same_shape(old_face, self.faces[new_face_id]): + match_id = new_face_id + break + if match_id is None: + continue + matched_by_logical_id.setdefault(logical_id, set()).add(match_id) + unmatched_new_ids.remove(match_id) + + reassigned = 0 + assigned_face_ids: set[int] = set() + logical_targets: dict[int, set[int]] = {} + for logical_id, face_ids in sorted(matched_by_logical_id.items()): + valid_face_ids = set(face_ids) - assigned_face_ids + if not valid_face_ids: + continue + logical_targets[int(logical_id)] = valid_face_ids + assigned_face_ids.update(valid_face_ids) + reassigned += len(valid_face_ids) + if not logical_targets: + return 0 + + new_logical_ids = list(self.face_logical_ids) + replacement_id = max( + [len(self.faces), *[int(item) for item in new_logical_ids], *logical_targets.keys()], + default=len(self.faces), + ) + 1 + for face_id, logical_id in enumerate(list(new_logical_ids)): + target_face_ids = logical_targets.get(int(logical_id)) + if target_face_ids is None or face_id in target_face_ids: + continue + new_logical_ids[face_id] = replacement_id + replacement_id += 1 + for logical_id, face_ids in logical_targets.items(): + for face_id in face_ids: + if 0 <= face_id < len(new_logical_ids): + new_logical_ids[face_id] = logical_id + self.face_logical_ids = new_logical_ids + self._quick_face_info_cache.clear() + self._face_info_cache.clear() + self._feature_info_cache.clear() + self._same_domain_face_ids_cache.clear() + self._face_first_level_topology_cache.clear() + self._cylindrical_first_level_topology_cache.clear() + self._face_first_level_fact_cache.clear() + self._local_face_deform_readiness_cache.clear() + return reassigned + def _local_face_deform_target( self, plan: dict[str, object], @@ -8177,6 +8393,27 @@ class OperationMixin: raise RuntimeError("Local edge deformation created an empty face.") return face + def _make_local_bilinear_quad_face( + self, + points: list[tuple[float, float, float]], + tolerance: float, + ) -> TopoDS_Shape: + if len(points) != 4: + raise RuntimeError("Local edge deformation bilinear face needs exactly four points.") + grid = TColgp_Array2OfPnt(1, 2, 1, 2) + grid.SetValue(1, 1, gp_Pnt(*points[0])) + grid.SetValue(2, 1, gp_Pnt(*points[1])) + grid.SetValue(1, 2, gp_Pnt(*points[3])) + grid.SetValue(2, 2, gp_Pnt(*points[2])) + surface = GeomAPI_PointsToBSplineSurface(grid).Surface() + maker = BRepBuilderAPI_MakeFace(surface, max(float(tolerance), 1e-7)) + if hasattr(maker, "IsDone") and not maker.IsDone(): + raise RuntimeError("Local edge deformation could not create a bilinear quad face.") + face = maker.Face() + if face.IsNull(): + raise RuntimeError("Local edge deformation created an empty bilinear quad face.") + return face + def _dedupe_local_points( self, points: list[tuple[float, float, float]], diff --git a/step_editor/ui_helpers.py b/step_editor/ui_helpers.py index c1cb92b..9506cf9 100644 --- a/step_editor/ui_helpers.py +++ b/step_editor/ui_helpers.py @@ -59,18 +59,36 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [ "first_level_fact_scope", "first_level_fact_source_model", "first_level_fact_subject_role", + "first_level_fact_subject_edge_ids", + "first_level_fact_subject_edge_count", "first_level_fact_subject_face_ids", "first_level_fact_subject_face_count", "first_level_fact_boundary_edge_ids", "first_level_fact_boundary_edge_count", + "first_level_fact_boundary_vertex_points", "first_level_fact_boundary_vertex_count", + "first_level_fact_adjacent_edge_ids", + "first_level_fact_adjacent_edge_count", "first_level_fact_adjacent_face_ids", "first_level_fact_adjacent_face_count", + "first_level_fact_included_edge_ids", + "first_level_fact_included_edge_count", "first_level_fact_included_face_ids", "first_level_fact_included_face_count", "first_level_fact_role_groups", "first_level_fact_ignored_relation_depths", + "first_level_fact_ignored_relation_note", "first_level_fact_summary", + "selected_edge_ids", + "selected_edge_count", + "first_level_vertex_points", + "first_level_vertex_count", + "first_level_adjacent_edge_ids", + "first_level_adjacent_edge_count", + "first_level_edge_ids", + "first_level_edge_count", + "first_level_adjacent_face_boundary_edge_ids", + "first_level_adjacent_face_boundary_edge_count", ], ), ( @@ -408,6 +426,8 @@ INFO_LABELS = { "parent_id": "父级 ID", "depth": "层级深度", "feature_mode": "特征模式说明", + "cad_modeling_form": "建模形式", + "cad_recommended_operation": "推荐操作", "feature_type": "特征类型", "feature_source_face_id": "特征来源 Face", "recognition_summary": "识别摘要", @@ -446,18 +466,36 @@ INFO_LABELS = { "first_level_fact_scope": "一级事实范围", "first_level_fact_source_model": "一级事实来源", "first_level_fact_subject_role": "一级事实主体", + "first_level_fact_subject_edge_ids": "一级主体 Edge", + "first_level_fact_subject_edge_count": "一级主体 Edge 数", "first_level_fact_subject_face_ids": "一级主体 Face", "first_level_fact_subject_face_count": "一级主体 Face 数", "first_level_fact_boundary_edge_ids": "一级边界 Edge", "first_level_fact_boundary_edge_count": "一级边界 Edge 数", + "first_level_fact_boundary_vertex_points": "一级边界 Vertex", "first_level_fact_boundary_vertex_count": "一级边界 Vertex 数", + "first_level_fact_adjacent_edge_ids": "一级相邻 Edge", + "first_level_fact_adjacent_edge_count": "一级相邻 Edge 数", "first_level_fact_adjacent_face_ids": "一级相邻 Face", "first_level_fact_adjacent_face_count": "一级相邻 Face 数", + "first_level_fact_included_edge_ids": "一级范围 Edge", + "first_level_fact_included_edge_count": "一级范围 Edge 数", "first_level_fact_included_face_ids": "一级范围 Face", "first_level_fact_included_face_count": "一级范围 Face 数", "first_level_fact_role_groups": "一级角色分组", "first_level_fact_ignored_relation_depths": "暂不传播层级", + "first_level_fact_ignored_relation_note": "暂不传播说明", "first_level_fact_summary": "一级事实摘要", + "selected_edge_ids": "当前 Edge", + "selected_edge_count": "当前 Edge 数", + "first_level_vertex_points": "一级端点 Vertex", + "first_level_vertex_count": "一级端点 Vertex 数", + "first_level_adjacent_edge_ids": "一级相邻 Edge", + "first_level_adjacent_edge_count": "一级相邻 Edge 数", + "first_level_edge_ids": "一级范围 Edge", + "first_level_edge_count": "一级范围 Edge 数", + "first_level_adjacent_face_boundary_edge_ids": "相邻 Face 的边界 Edge", + "first_level_adjacent_face_boundary_edge_count": "相邻 Face 的边界 Edge 数", "volume": "体积", "surface_area": "表面积", "area": "面积", @@ -710,9 +748,9 @@ INFO_LABELS = { "freeform_face_risk": "自由曲面风险", "freeform_face_blockers": "自由曲面限制", "resize_status": "切削状态", - "resize_strategy": "编辑策略", - "edit_strategy_label": "编辑策略说明", - "edit_semantics": "编辑方式", + "resize_strategy": "执行策略", + "edit_strategy_label": "执行策略说明", + "edit_semantics": "建模意图", "resize_mode": "调整模式", "resize_risk": "切削风险", "resize_warnings": "切削警告", diff --git a/step_editor/window_actions.py b/step_editor/window_actions.py index c28a151..412f1b0 100644 --- a/step_editor/window_actions.py +++ b/step_editor/window_actions.py @@ -375,8 +375,8 @@ class WindowActionMixin: f"本次移动距离: {_format_value(distance)}\n" f"当前法向位置: {_format_value(plan.get('current_plane_position'))}\n" f"目标法向位置: {_format_value(plan.get('target_plane_position'))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"影响范围: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"执行影响: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -1153,7 +1153,7 @@ class WindowActionMixin: face_id, blocker, resize_strategy="local-face-plane-offset-deform", - edit_strategy_label="偏移曲面(局部重建)", + edit_strategy_label="偏移变换(局部重建)", edit_semantics="按当前面垂直方向移动 Face 边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。", info=info, extra={ @@ -1394,9 +1394,9 @@ class WindowActionMixin: strategy_label = str(plan.get("edit_strategy_label") or "").strip() edit_semantics = str(plan.get("edit_semantics") or "").strip() if strategy_label: - semantic_lines.append(f"编辑策略: {strategy_label}") + semantic_lines.append(f"执行策略: {strategy_label}") if edit_semantics: - semantic_lines.append(f"编辑方式: {edit_semantics}") + semantic_lines.append(f"建模意图: {edit_semantics}") result = QMessageBox.question( self, @@ -1432,7 +1432,7 @@ class WindowActionMixin: "V向尺寸(局部重建)", "U向尺寸(缩放特征)", "V向尺寸(缩放特征)", - "偏移曲面(局部重建)", + "偏移变换(局部重建)", "中心(局部重建)", "圆柱高度调整", "高度(缩放特征)缩放所属对象", @@ -1511,7 +1511,7 @@ class WindowActionMixin: strategy = str(parameters.get("edit_strategy_label") or "").strip() if strategy: - lines.append(f"编辑策略: {strategy}") + lines.append(f"执行策略: {strategy}") surface = parameters.get("surface") distance = parameters.get("semantic_distance") @@ -2844,8 +2844,8 @@ class WindowActionMixin: f"置信度: {plan['confidence']}\n" f"材料投票: {plan['material_vote_summary']}\n" f"工具策略: {plan['boss_tool_strategy']}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"工具高度: {_format_value(plan['boss_tool_height'])}\n" f"轴向重叠余量: {_format_value(plan['boss_tool_axial_margin'])}\n" f"风险: {plan['risk']}\n\n" @@ -3034,8 +3034,8 @@ class WindowActionMixin: f"端盖拉伸/切除距离: {_format_value(plan.get('push_pull_distance'))}\n" f"候选判断: {plan.get('feature_guess')}\n" f"置信度: {plan.get('confidence')}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -3737,8 +3737,8 @@ class WindowActionMixin: f"支撑 Face: {_format_value(plan.get('feature_existing_fillet_support_face_ids', ''))}\n" f"边界 Edge: {_format_value(plan.get('feature_boundary_edge_ids', ''))}\n" f"策略: {plan['resize_strategy']}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -3828,8 +3828,8 @@ class WindowActionMixin: f"半径/Edge长度比例: {_format_percent(plan['radius_to_length_ratio'])}\n" f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n" f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -3910,8 +3910,8 @@ class WindowActionMixin: f"倒角距离/Edge长度比例: {_format_percent(plan['distance_to_length_ratio'])}\n" f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n" f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -4175,9 +4175,9 @@ class WindowActionMixin: f"目标Edge长度: {_format_value(plan['target_length'])}\n" f"变化量: {_format_value(plan['delta_length'])}\n" f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n" - f"Edge修改策略: {_format_value(plan.get('edge_length_strategy_label', ''))}\n" + f"建模意图: {_format_value(plan.get('edge_length_strategy_label', ''))}\n" f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n" - f"影响范围: {_format_value(plan.get('edge_length_impact_summary', ''))}\n" + f"执行影响: {_format_value(plan.get('edge_length_impact_summary', ''))}\n" f"策略: {_format_value(plan.get('resize_strategy', ''))}\n" f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n" f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n" @@ -4343,9 +4343,9 @@ class WindowActionMixin: f"目标Edge长度: {_format_value(plan['target_length'])}\n" f"变化量: {_format_value(plan['delta_length'])}\n" f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n" - f"Edge修改策略: {_format_value(plan.get('edge_length_strategy_label', ''))}\n" + f"建模意图: {_format_value(plan.get('edge_length_strategy_label', ''))}\n" f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n" - f"影响范围: {_format_value(plan.get('edge_length_impact_summary', ''))}\n" + f"执行影响: {_format_value(plan.get('edge_length_impact_summary', ''))}\n" f"策略: {_format_value(strategy)}\n" f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n" f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n" @@ -4516,7 +4516,7 @@ class WindowActionMixin: f"目标小半径: {_format_value(plan.get('ellipse_target_minor_radius'))}\n" f"缩放方向: {_format_value(plan.get('ellipse_axis_direction_label'))}\n" f"缩放比例: {_format_value(plan.get('affine_scale'))}\n" - f"影响范围: {_format_value(plan.get('edit_semantics'))}\n" + f"执行影响: {_format_value(plan.get('edit_semantics'))}\n" f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" @@ -4883,8 +4883,8 @@ class WindowActionMixin: f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n" f"缩放比例: {_format_value(plan.get('affine_scale'))}\n" f"策略: {_format_value(plan.get('resize_strategy'))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -4980,8 +4980,8 @@ class WindowActionMixin: f"目标半角: {_format_value(plan.get('target_semi_angle_degrees'))} 度\n" f"目标参考半径: {_format_value(plan.get('target_reference_radius'))}\n" f"变化比例: {_format_percent(plan.get('reference_radius_delta_ratio'))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"影响范围: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"执行影响: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -5079,8 +5079,8 @@ class WindowActionMixin: f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n" f"缩放比例: {_format_value(plan.get('affine_scale'))}\n" f"策略: {_format_value(plan.get('resize_strategy'))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -5184,8 +5184,8 @@ class WindowActionMixin: f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n" f"缩放比例: {_format_value(plan.get('affine_scale'))}\n" f"策略: {_format_value(plan.get('resize_strategy'))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -5277,8 +5277,8 @@ class WindowActionMixin: f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n" f"缩放比例: {_format_value(plan.get('affine_scale'))}\n" f"策略: {_format_value(plan.get('resize_strategy'))}\n" - f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n" - f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n" + f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n" + f"建模意图: {_format_value(plan.get('edit_semantics'))}\n" f"影响说明: {_format_value(plan.get('affine_transform_note'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" @@ -5625,7 +5625,7 @@ class WindowActionMixin: def move_selected_face_plane_position_by_translation(self) -> None: if self.model is None: return - if self._edit_busy("请等待当前编辑完成后再按偏移曲面平移所属对象。"): + if self._edit_busy("请等待当前编辑完成后再按偏移变换平移所属对象。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。") @@ -5633,14 +5633,14 @@ class WindowActionMixin: try: distance = float(self.offset_input.text()) except (AttributeError, ValueError): - QMessageBox.critical(self, "偏移曲面无效", "请输入数字形式的目标偏移曲面。") + QMessageBox.critical(self, "偏移变换无效", "请输入数字形式的目标偏移变换位置。") return face_id = self.selected_face_id plan = self.model.face_plane_offset_owning_translation_plan(face_id, distance) if plan["status"] == "blocked": - QMessageBox.information(self, "不能按偏移曲面平移", str(plan["message"])) - self.statusBar().showMessage("偏移曲面整体平移已阻止") + QMessageBox.information(self, "不能按偏移变换平移", str(plan["message"])) + self.statusBar().showMessage("偏移变换整体平移已阻止") return current_position = float(plan.get("current_plane_position", 0.0) or 0.0) target_position = float(plan.get("target_plane_position", current_position) or current_position) @@ -5652,7 +5652,7 @@ class WindowActionMixin: target_position, plane_direction, ): - self.statusBar().showMessage("已取消偏移曲面整体平移") + self.statusBar().showMessage("已取消偏移变换整体平移") return self.clear_edit_preview(render=False) @@ -5663,7 +5663,7 @@ class WindowActionMixin: moved_id = plan.get("solid_id") if moved_kind == "solid" else plan.get("part_id") self._run_edit_action( action, - operation_name="偏移曲面(移动特征)", + operation_name="偏移变换(移动特征)", target=f"Face {face_id}", parameters={ "face_id": face_id, @@ -5684,8 +5684,8 @@ class WindowActionMixin: "translate_warnings": plan.get("warnings"), "translate_blockers": plan.get("blockers"), "resize_strategy": "translate-owning-shape-from-plane-offset", - "edit_strategy_label": "按偏移曲面平移所属对象", - "edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。", + "edit_strategy_label": "按偏移变换平移所属对象", + "edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。", }, target_kind="face", target_id=face_id, @@ -5694,7 +5694,7 @@ class WindowActionMixin: def move_selected_face_plane_position_local(self) -> None: if self.model is None: return - if self._edit_busy("请等待当前编辑完成后再执行偏移曲面(局部重建)。"): + if self._edit_busy("请等待当前编辑完成后再执行偏移变换(局部重建)。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。") @@ -5702,7 +5702,7 @@ class WindowActionMixin: try: distance = float(self.offset_input.text()) except (AttributeError, ValueError): - QMessageBox.critical(self, "偏移曲面无效", "请输入数字形式的目标偏移曲面。") + QMessageBox.critical(self, "偏移变换无效", "请输入数字形式的目标偏移变换位置。") return face_id = self.selected_face_id @@ -5711,8 +5711,8 @@ class WindowActionMixin: plan = self.model.face_plane_offset_local_plan(face_id, distance) lines = [ f"Face: {face_id}", - f"当前偏移曲面: {_format_value(plan.get('current_plane_position'))}", - f"目标偏移曲面: {_format_value(plan.get('target_plane_position'))}", + f"当前偏移变换位置: {_format_value(plan.get('current_plane_position'))}", + f"目标偏移变换位置: {_format_value(plan.get('target_plane_position'))}", f"平面方向: {_format_value(plan.get('plane_direction'))}", f"移动向量: {_format_value(plan.get('face_center_move_vector'))}", f"移动距离: {_format_value(plan.get('face_center_move_distance'))}", @@ -5722,11 +5722,11 @@ class WindowActionMixin: f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}", ] if not self._confirm_quick_edit_plan( - "偏移曲面(局部重建)", + "偏移变换(局部重建)", plan, lines, - "偏移曲面(局部重建)已阻止", - "已取消偏移曲面(局部重建)", + "偏移变换(局部重建)已阻止", + "已取消偏移变换(局部重建)", ): return self.clear_edit_preview(render=False) @@ -5736,7 +5736,7 @@ class WindowActionMixin: self._run_edit_action( action, - operation_name="偏移曲面(局部重建)", + operation_name="偏移变换(局部重建)", target=f"Face {face_id}", parameters={ "face_id": face_id, @@ -5758,9 +5758,9 @@ class WindowActionMixin: "local_face_deform_face_count": plan.get("local_face_deform_face_count"), "local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"), "resize_strategy": "local-face-plane-offset-deform", - "edit_strategy_label": "偏移曲面(局部重建)", + "edit_strategy_label": "偏移变换(局部重建)", "edit_semantics": ( - "把目标偏移曲面换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;" + "把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;" "不拉伸/切除加料/切削,也不平移所属对象。" ), "resize_status": plan.get("status"), @@ -6067,8 +6067,8 @@ class WindowActionMixin: f"目标轴心: {_format_value(target_center)}\n" f"平移向量: {_format_value(plan['translation_vector'])}\n" f"平移距离: {_format_value(plan['translation_distance'])}\n" - f"编辑策略: 按轴心平移所属对象\n" - f"编辑方式: 平移所属特征或 Solid;不填旧孔、不切新孔、不重建局部凸台。\n" + f"执行策略: 按轴心平移所属对象\n" + f"建模意图: 平移所属特征或 Solid;不填旧孔、不切新孔、不重建局部凸台。\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -6157,8 +6157,8 @@ class WindowActionMixin: f"目标Face中心: {_format_value(target_center)}\n" f"平移向量: {_format_value(plan['translation_vector'])}\n" f"平移距离: {_format_value(plan['translation_distance'])}\n" - f"编辑策略: 平移Face所属对象\n" - f"编辑方式: 把目标 Face 的中心坐标换算成平移量,并平移所属特征或 Solid;不做单面局部扭曲。\n" + f"执行策略: 平移Face所属对象\n" + f"建模意图: 把目标 Face 的中心坐标换算成平移量,并平移所属特征或 Solid;不做单面局部扭曲。\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" @@ -6184,23 +6184,23 @@ class WindowActionMixin: warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, - "确认偏移曲面整体平移", + "确认偏移变换整体平移", ( f"Face: {face_id}\n" f"移动对象: {_format_value(plan.get('target_kind', ''))}\n" f"零件: {_format_value(plan.get('part_id', ''))}\n" f"Solid: {_format_value(plan.get('solid_id', ''))}\n" - f"当前偏移曲面: {_format_value(current_position)}\n" - f"目标偏移曲面: {_format_value(target_position)}\n" + f"当前偏移变换位置: {_format_value(current_position)}\n" + f"目标偏移变换位置: {_format_value(target_position)}\n" f"平面方向: {_format_value(plane_direction)}\n" f"平移向量: {_format_value(plan['translation_vector'])}\n" f"平移距离: {_format_value(plan['translation_distance'])}\n" - f"编辑策略: 按偏移曲面平移所属对象\n" - f"编辑方式: 沿当前面垂直方向平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。\n" + f"执行策略: 按偏移变换平移所属对象\n" + f"建模意图: 沿当前面垂直方向平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" - "如果你想改变厚度或把这个面推出/切入,请在“偏移曲面”这一行把影响范围选为“拉伸/切除”。确定继续吗?" + "如果你想改变厚度或把这个面推出/切入,请在“偏移变换”这一行把建模意图选为“拉伸/切除”。确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, @@ -7577,7 +7577,7 @@ class WindowActionMixin: if target_position is not None and plane_direction is not None: checks.append( ( - "偏移曲面", + "偏移变换", self._face_target_plane_position_matches( model, face_ids, @@ -8007,7 +8007,7 @@ class WindowActionMixin: "中心", "U向尺寸", "V向尺寸", - "偏移曲面", + "偏移变换", "壳体", "拉伸/切除", "圆锥", @@ -8062,9 +8062,9 @@ class WindowActionMixin: progress_text = f"{operation_name} 正在后台计算;当前会沿选中 Face 的平面内方向缩放所属特征或 Solid,其它几何会跟随变化。" elif "U向尺寸(局部重建)" in operation_name or "V向尺寸(局部重建)" in operation_name: progress_text = f"{operation_name} 正在后台计算;当前会只沿选中 Face 的一个平面内方向缩放顶点并重建相邻平面。" - elif "偏移曲面(局部重建)" in operation_name: + elif "偏移变换(局部重建)" in operation_name: progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向移动所选 Face 顶点并重建相邻平面。" - elif "偏移曲面(移动特征)" in operation_name: + elif "偏移变换(移动特征)" in operation_name: progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向平移所属特征或 Solid,不拉伸/切除。" elif "Face中心" in operation_name: progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不做单面局部扭曲。" diff --git a/step_editor/window_core.py b/step_editor/window_core.py index 2ae07d1..d8dd949 100644 --- a/step_editor/window_core.py +++ b/step_editor/window_core.py @@ -1930,6 +1930,7 @@ class WindowCoreMixin: self.selected_edge_id = edge_id self.selected_pick_position = pick_position info = self.model.edge_info(edge_id) + info.update(self._edge_first_level_selection_fields(edge_id)) self._sync_edge_edit_inputs(info) self.selected_part_id = int(info["part_id"]) self.selected_solid_id = int(info["solid_id"]) if int(info.get("solid_id", -1)) >= 0 else None diff --git a/step_editor/window_state.py b/step_editor/window_state.py index ff88e5f..39c26dd 100644 --- a/step_editor/window_state.py +++ b/step_editor/window_state.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime import math from pathlib import Path +import time from PySide6.QtCore import Qt, QThread, QTimer, Slot from PySide6.QtWidgets import ( QApplication, @@ -29,6 +30,8 @@ PROPERTY_TARGET_COLUMN = 3 PROPERTY_ACTION_COLUMN = 4 FEATURE_EDIT_SEMANTICS_KEYS = { + "cad_modeling_form", + "cad_recommended_operation", "face_first_level_topology", "cylindrical_feature_first_level_topology", "slot_edit_semantics", @@ -338,6 +341,59 @@ class WindowStateMixin: **self._first_level_fact_selection_fields(face_id, scope="cylindrical-feature"), } + def _edge_first_level_selection_fields(self, edge_id: int) -> dict[str, object]: + if self.model is None: + return {} + try: + topology = self.model.edge_first_level_topology(edge_id) + facts = self.model.edge_first_level_facts(edge_id) + except Exception as exc: + return { + "topology_relation_depth": 1, + "topology_relation_model": "STEP/B-Rep edge first-level", + "topology_relation_status": "unavailable", + "topology_relation_message": str(exc), + "topology_ignored_relation_depths": ("second-level", "third-level", "deeper"), + "topology_ignored_relation_note": "Current Edge first-level topology is unavailable.", + "first_level_vertex_count": 0, + "first_level_adjacent_edge_count": 0, + "first_level_adjacent_face_count": 0, + "first_level_fact_model": "STEP/B-Rep first-level fact graph", + "first_level_fact_status": "unavailable", + "first_level_fact_source_model": "edge", + "first_level_fact_relation_depth": 1, + "first_level_fact_scope": "edge", + "first_level_fact_subject_edge_ids": (edge_id,), + "first_level_fact_subject_edge_count": 1, + "first_level_fact_boundary_edge_count": 1, + "first_level_fact_boundary_vertex_count": 0, + "first_level_fact_adjacent_edge_count": 0, + "first_level_fact_adjacent_face_count": 0, + "first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"), + "first_level_fact_summary": f"Current Edge first-level fact graph is unavailable: {exc}", + } + return { + "topology_relation_depth": topology.get("topology_relation_depth", 1), + "topology_relation_model": topology.get("topology_relation_model"), + "topology_relation_scope": topology.get("topology_relation_scope"), + "topology_relation_boundary": topology.get("topology_relation_boundary"), + "topology_relation_status": "ready", + "topology_ignored_relation_depths": topology.get("topology_ignored_relation_depths", ()), + "topology_ignored_relation_note": topology.get("topology_ignored_relation_note", ""), + "selected_edge_ids": topology.get("selected_edge_ids", (edge_id,)), + "selected_edge_count": topology.get("selected_edge_count", 1), + "first_level_vertex_points": topology.get("first_level_vertex_points", ()), + "first_level_vertex_count": topology.get("first_level_vertex_count", 0), + "first_level_adjacent_edge_ids": topology.get("first_level_adjacent_edge_ids", ()), + "first_level_adjacent_edge_count": topology.get("first_level_adjacent_edge_count", 0), + "first_level_edge_ids": topology.get("first_level_edge_ids", (edge_id,)), + "first_level_edge_count": topology.get("first_level_edge_count", 1), + "first_level_adjacent_face_ids": topology.get("first_level_adjacent_face_ids", ()), + "first_level_adjacent_face_count": topology.get("first_level_adjacent_face_count", 0), + "first_level_topology_note": topology.get("first_level_topology_note", ""), + **facts, + } + def _feature_info_for_selected_face(self, face_id: int, fallback_info: dict[str, object]) -> dict[str, object]: if self.model is None: return dict(fallback_info) @@ -415,22 +471,28 @@ class WindowStateMixin: if self.model is None: return {} detection_level = self._current_feature_detection_level() - if detection_level == "current-only": - root_info = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id)) - else: - root_info = self.model.feature_info(face_id) - if str(root_info.get("surface", "") or "") == "plane": + root_info = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id)) + if str(root_info.get("surface", "") or "") == "plane" and "topology_relation_depth" not in root_info: root_info.update(self._face_first_level_selection_fields(face_id)) - elif str(root_info.get("surface", "") or "") == "cylinder" and detection_level != "current-only": - root_info.update(self._cylindrical_first_level_selection_fields(face_id)) associated: list[dict[str, object]] = [] if detection_level in {"associated-only", "secondary"}: try: - associated = self.model.associated_feature_infos(face_id) + associated = self.model.associated_feature_infos( + face_id, + max_depth=3, + max_scan_faces=72, + max_features=10, + time_budget_seconds=0.55 if detection_level == "associated-only" else 0.75, + lightweight=True, + ) except Exception: associated = [] if detection_level == "secondary": - associated = self._secondary_associated_feature_infos(face_id, associated) + associated = self._secondary_associated_feature_infos( + face_id, + associated, + time_budget_seconds=0.45, + ) highlight_ids = set(_int_values(root_info.get("feature_highlight_face_ids")) or [face_id]) for item in associated: @@ -509,11 +571,19 @@ class WindowStateMixin: self, source_face_id: int, direct_infos: list[dict[str, object]], + *, + time_budget_seconds: float | None = None, ) -> list[dict[str, object]]: if self.model is None: return list(direct_infos) results: list[dict[str, object]] = [] seen: set[tuple[str, tuple[int, ...]]] = set() + deadline = None + if time_budget_seconds is not None and time_budget_seconds > 0: + deadline = time.monotonic() + float(time_budget_seconds) + + def budget_expired() -> bool: + return deadline is not None and time.monotonic() >= deadline def add(info: dict[str, object]) -> None: source_id = _int_or_none(info.get("association_source_face_id")) @@ -529,17 +599,26 @@ class WindowStateMixin: for info in direct_infos: add(info) for info in list(results): + if budget_expired(): + break parent_id = _int_or_none(info.get("association_source_face_id")) if parent_id is None: continue try: + nested_budget = None + if deadline is not None: + nested_budget = max(0.02, min(0.12, deadline - time.monotonic())) for nested in self.model.associated_feature_infos( parent_id, - max_depth=2, - max_scan_faces=48, - max_features=6, + max_depth=1, + max_scan_faces=16, + max_features=4, + time_budget_seconds=nested_budget, + lightweight=True, ): add(nested) + if budget_expired(): + break except Exception: continue results.sort( @@ -1293,7 +1372,7 @@ class WindowStateMixin: action_item = self._property_table_item(action_text, editable=False) target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable)) row_items = (label_item, current_item, scope_item, target_item, action_item) - self._style_property_row_items(row_items, editable=editable) + self._style_property_row_items(row_items, editable=editable, spec=effective_spec) for column, item in enumerate(row_items): item.setToolTip(item.toolTip() or item.text()) self.property_table.setItem(row, column, item) @@ -1319,6 +1398,8 @@ class WindowStateMixin: def _sort_property_specs_for_display(self, specs: list[dict[str, object]]) -> list[dict[str, object]]: def rank(spec: dict[str, object]) -> int: + if bool(spec.get("pin_top")): + return -1 editable = bool(spec.get("editable")) enabled = bool(spec.get("enabled")) has_action = bool(spec.get("action")) @@ -1340,8 +1421,24 @@ class WindowStateMixin: items: tuple[QTableWidgetItem, QTableWidgetItem, QTableWidgetItem, QTableWidgetItem, QTableWidgetItem], *, editable: bool, + spec: dict[str, object] | None = None, ) -> None: label_item, current_item, scope_item, target_item, status_item = items + if spec is not None and bool(spec.get("pin_top")): + for item in items: + item.setBackground(QColor("#eef6ff")) + label_item.setForeground(QColor("#075985")) + current_item.setForeground(QColor("#0f172a")) + scope_item.setForeground(QColor("#0369a1")) + target_item.setForeground(QColor("#64748b")) + status_item.setForeground(QColor("#0369a1")) + label_font = label_item.font() + label_font.setBold(True) + label_item.setFont(label_font) + current_font = current_item.font() + current_font.setBold(True) + current_item.setFont(current_font) + return if editable: row_backgrounds = ("#fff7ed", "#fffbeb", "#fff7ed", "#fff7ed", "#fff7ed") for item, color in zip(items, row_backgrounds): @@ -1421,8 +1518,8 @@ class WindowStateMixin: tip_key = "enabled_tip" if bool(mode.get("enabled", True)) else "disabled_tip" tip = str(mode.get(tip_key) or mode.get("enabled_tip") or mode.get("disabled_tip") or "").strip() if tip: - return f"影响范围:{label}\n\n{tip}" - return f"影响范围:{label}" + return f"建模意图:{label}\n\n{tip}" + return f"建模意图:{label}" def _set_property_target_editor(self, row: int, spec: dict[str, object]) -> None: editor = QLineEdit(str(spec.get("target_text", ""))) @@ -1632,10 +1729,15 @@ class WindowStateMixin: dimension["label"] = label_overrides[key] dimensions.append(dimension) + pinned_explanations = [ + dict(spec) + for spec in specs + if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS and bool(spec.get("pin_top")) + ] explanations = [ dict(spec) for spec in specs - if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS + if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS and not bool(spec.get("pin_top")) ] if not dimensions: dimensions.append( @@ -1654,7 +1756,7 @@ class WindowStateMixin: ), } ) - return dimensions + explanations + return pinned_explanations + dimensions + explanations def _feature_context_property_specs( self, @@ -1675,6 +1777,7 @@ class WindowStateMixin: related_specs, _used = self._editable_property_specs(related_info) feature_label = str(related_info.get("feature_type") or related_info.get("feature_guess") or "关联特征") source_face_id = _int_or_none(related_info.get("association_source_face_id")) + feature_label = str(related_info.get("association_label") or feature_label) for spec in self._feature_property_specs(related_specs, related_info): if spec.get("parameter_role") != "dimension": continue @@ -2149,6 +2252,7 @@ class WindowStateMixin: label: str, text: str, tip: str, + pin_top: bool = False, ) -> None: specs.append( { @@ -2161,9 +2265,196 @@ class WindowStateMixin: "enabled": False, "status_text": "说明", "disabled_tip": tip or text, + "pin_top": pin_top, } ) + def cad_modeling_form() -> tuple[str, str]: + if has_face: + if is_hole_or_groove: + if is_slot_or_half_hole: + return ( + "工程特征 / 槽:按槽宽、槽深、弧长、总长或轴心做局部重建。", + "这类对象优先按 Creo 式槽特征理解;当前只传播直接相邻的一级关系,复杂二级/三级联动后续再开放。", + ) + return ( + "工程特征 / 孔:按孔径、轴心、封堵或盲孔/盲槽深度做重切/补料。", + "这类对象优先按 Creo 式孔特征理解;通孔、盲孔、锥孔和槽孔会根据识别结果开放不同参数。", + ) + if is_boss: + return ( + "工程特征 / 凸台:按直径、高度或轴心做局部重建。", + "凸台修改会尽量按局部补料、切除端盖或重建包络处理;也可以选择缩放所属对象这类整体语义。", + ) + if is_existing_fillet: + return ( + "工程特征 / 倒圆角:移除已有圆角后按目标半径重新倒圆。", + "STEP 没有原始倒圆角历史时,只能基于当前圆角面和支撑面推断;复杂 blend 会阻止或回滚。", + ) + if is_shell_candidate: + return ( + "工程特征 / 抽壳:按壳体厚度或相对面距离调整薄壁区域。", + "壳体厚度修改本质上是移动当前平面或沿厚度方向缩放所属对象,需要相对面和厚度方向识别稳定。", + ) + if is_plane: + return ( + "柔性建模 / 拉伸切除 / 偏移变换:平面 Face 可按不同意图改变位置、面积或尺寸。", + "平面 Face 不是直接暴露 B-Rep 参数;界面会把目标值映射为拉伸/切除、局部重建、移动特征或缩放特征。", + ) + if is_cone: + return ( + "工程特征 / 拔模或锥孔:只在简单圆锥或可识别锥孔上开放半角和参考半径。", + "复杂圆锥面可能只是拔模、过渡或导入后的自由裁剪面,当前不会把不可靠参数伪装成可修改特征。", + ) + if is_sphere: + return ( + "解析曲面 / 缩放特征:球面半径通过缩放所属对象调整。", + "当前不是恢复 CAD 历史里的球面特征,而是围绕识别到的中心做受限几何缩放。", + ) + if is_torus: + return ( + "解析曲面 / 缩放特征:环面主半径和小半径通过缩放所属对象调整。", + "当前不是恢复 CAD 历史里的环面特征,而是围绕识别到的环面中心和轴线做受限几何缩放。", + ) + if is_generic_surface: + surface_name = _format_info_value("surface", surface) + return ( + f"自由/复杂曲面:{surface_name} 当前只读,不开放参数化修改。", + "自由曲面需要单独的曲面替换、重拟合或控制点编辑语义;当前一级阶段不把底层 UV 参数当成用户尺寸。", + ) + if is_cylinder: + return ( + "解析圆柱 / 拉伸切除:可按直径、高度或轴向缩放处理普通圆柱面。", + "如果识别不到孔、槽、凸台或圆角语义,当前只按普通圆柱几何入口开放有限参数。", + ) + return ( + "导入几何 Face:当前只显示可识别参数。", + "STEP 通常没有完整建模历史;只有能映射到稳定 CAD 建模形式的参数才会开放修改。", + ) + if has_edge: + if curve == "line": + return ( + "柔性建模 / 移动几何:直线 Edge 可改长度、端点,并作为倒角/倒圆角入口。", + "Edge 长度有多种 CAD 语义:只改当前边、移动端面、相邻圆柱联动或缩放所属对象,界面会让用户选择。", + ) + if curve == "circle": + return ( + "工程特征入口 / 圆 Edge:可作为孔槽、圆角或相邻圆柱尺寸的参考。", + "圆 Edge 本身通常不是独立历史特征;修改时会优先寻找相邻圆柱、孔槽或圆角语义。", + ) + return ( + "曲线 Edge:当前只开放能稳定映射的尺寸或倒角/圆角入口。", + "复杂曲线 Edge 暂不直接做自由形变,避免把拓扑边参数误当成 CAD 设计尺寸。", + ) + if self.selected_solid_id is not None or self.selected_part_id is not None: + return ( + "整体特征 / 移动几何 / 缩放特征:当前对象按整体变换处理。", + "零件或 Solid 层级不代表单个 CAD 历史特征;当前只开放清晰的整体移动、旋转或缩放语义。", + ) + return "", "" + + def cad_recommended_operation() -> tuple[str, str]: + if has_face: + if is_hole_or_groove: + if is_slot_or_half_hole: + return ( + "优先改槽宽、槽深或槽孔总长度;要换位置时改轴心。", + "槽类修改先走局部重建;如果用户选择缩放特征,会连同所属对象上的其它尺寸一起变化。", + ) + return ( + "优先改孔径/半径;盲孔改深度;要换位置时改轴心。", + "孔类修改会尽量先补旧孔再按目标重新切孔;螺纹孔、复杂孔组和底面识别不稳的盲孔仍可能受限。", + ) + if is_boss: + return ( + "优先改凸台直径或高度;要换位置时改轴心。", + "凸台高度更接近拉伸/切除端盖;凸台直径和轴心更接近移除旧包络后重建局部凸台。", + ) + if is_existing_fillet: + return ( + "优先改圆角半径;复杂圆角链先不要用整体缩放替代。", + "已有圆角需要先恢复支撑锐边再重新倒圆,支撑面不明确时失败回滚是正常保护。", + ) + if is_shell_candidate: + return ( + "优先改壳体厚度;普通位置变化再用偏移变换。", + "壳体厚度需要相对面识别稳定;如果只想移动整个对象,请选择移动特征或缩放特征语义。", + ) + if is_plane: + return ( + "优先改偏移变换,并选择拉伸/切除;只想让当前面变形时再改面积、U/V尺寸或中心。", + "拉伸/切除更接近常见 CAD 加料/切料;局部重建会让相邻面自然跟随变形,不一定保持垂直。", + ) + if is_cone: + return ( + "简单圆锥优先改半角或参考半径;复杂拔模面先保持只读。", + "锥面可能来自拔模、沉孔或导入裁剪曲面,识别不稳时不直接开放大参数修改。", + ) + if is_sphere: + return ( + "优先改球面半径,并确认这是缩放所属对象。", + "当前没有恢复球面历史特征,只能按识别中心做受限几何缩放。", + ) + if is_torus: + return ( + "优先改环面主半径或小半径,并确认这是缩放所属对象。", + "当前没有恢复环面历史特征,只能按识别中心和轴线做受限几何缩放。", + ) + if is_generic_surface: + return ( + "当前建议只查看,不直接改自由曲面参数。", + "自由曲面需要曲面替换、重拟合或控制点编辑,不能用面积或包围盒伪装成稳定 CAD 尺寸。", + ) + if is_cylinder: + return ( + "优先改直径或高度;若它其实是孔、槽、凸台或圆角,请先提高特征探测级别。", + "普通圆柱入口是兜底几何编辑,不等同于已经识别到完整 CAD 特征。", + ) + return ( + "先查看识别摘要,再只修改标记为可修改的参数。", + "未映射到明确建模形式的 STEP Face 不应强行参数化。", + ) + if has_edge: + if curve == "line": + return ( + "优先改长度;想保持端面垂直选移动端面,想只动当前边选只改当前Edge。", + "同一条 Edge 改长可能对应多种 CAD 结果,当前必须显式选择建模意图。", + ) + if curve == "circle": + return ( + "优先让圆 Edge 关联到孔、槽、凸台或圆角直径,不直接自由变形这条边。", + "圆 Edge 往往是相邻特征的边界,直接改边可能破坏孔槽或圆角语义。", + ) + return ( + "只修改已明确开放的 Edge 参数;复杂曲线先保持只读。", + "复杂曲线需要约束或曲线重拟合,不适合直接用边长驱动。", + ) + if self.selected_solid_id is not None or self.selected_part_id is not None: + return ( + "优先使用整体移动、旋转或缩放;不要把整体对象误当成单个历史特征。", + "STEP 里零件/Solid 层级通常缺少装配约束和特征历史,整体操作要明确影响范围。", + ) + return "", "" + + cad_form_text, cad_form_tip = cad_modeling_form() + if cad_form_text: + add_readonly_spec( + key="cad_modeling_form", + label="建模形式", + text=cad_form_text, + tip=cad_form_tip, + pin_top=True, + ) + recommended_text, recommended_tip = cad_recommended_operation() + if recommended_text: + add_readonly_spec( + key="cad_recommended_operation", + label="推荐操作", + text=recommended_text, + tip=recommended_tip, + pin_top=True, + ) + if has_face: topology_depth = _int_or_none(action_info.get("topology_relation_depth")) is_cylindrical_topology = bool( @@ -2225,7 +2516,7 @@ class WindowStateMixin: if is_slot_or_half_hole: add_readonly_spec( key="slot_edit_semantics", - label="编辑方式", + label="建模意图", text="槽/半孔:局部扇形槽重建;槽孔总长度会尝试配对另一端。", tip=( "槽/半孔宽度、深度和圆弧参数会换算成圆柱半径或圆弧角度," @@ -2236,7 +2527,7 @@ class WindowStateMixin: else: add_readonly_spec( key="hole_edit_semantics", - label="编辑方式", + label="建模意图", text="孔:同轴重切孔壁;移动轴心会先填旧孔再切新孔。", tip=( "孔径/半径不是缩放整个模型,而是在当前孔轴线上做受限布尔重切;" @@ -2247,7 +2538,7 @@ class WindowStateMixin: elif is_boss: add_readonly_spec( key="boss_edit_semantics", - label="编辑方式", + label="建模意图", text="凸台:直径重建包络;高度拉伸/切除端盖;轴心先移除再补新凸台。", tip=( "凸台直径会通过局部布尔补料或移除旧包络后重建目标圆柱;" @@ -2258,7 +2549,7 @@ class WindowStateMixin: elif is_existing_fillet: add_readonly_spec( key="existing_fillet_edit_semantics", - label="编辑方式", + label="建模意图", text="圆角:移除已有圆角面,再尝试按目标半径重新倒圆。", tip=( "已有圆角修改不是恢复 CAD 历史参数;当前会先 defeature 当前圆角面," @@ -2268,11 +2559,11 @@ class WindowStateMixin: elif is_cone or is_sphere or is_torus: add_readonly_spec( key="analytic_surface_edit_semantics", - label="编辑方式", + label="建模意图", text="解析曲面:围绕中心或轴线缩放所属对象,会影响同对象其它尺寸。", tip=( "圆锥、球面、环面这类曲面当前走几何缩放语义," - "不是只替换单个曲面的历史参数;执行前需要确认影响范围。" + "不是只替换单个曲面的历史参数;执行前需要确认建模意图。" ), ) elif is_generic_surface: @@ -2284,7 +2575,7 @@ class WindowStateMixin: ) add_readonly_spec( key="freeform_surface_edit_semantics", - label="编辑方式", + label="建模意图", text=f"{surface_name}:暂不支持参数化编辑。", tip=( f"{blocker} 当前不会把自由曲面的面积、中心、包围盒或底层 UV 参数伪装成可修改尺寸;" @@ -2311,11 +2602,11 @@ class WindowStateMixin: ), tip=topology_tip, ) - face_semantics_text = "Face:先改目标值,再用“影响范围”选择局部重建、拉伸/切除、移动特征或缩放特征。" + face_semantics_text = "Face:先改目标值,再用“建模意图”选择局部重建、拉伸/切除、移动特征或缩放特征。" face_semantics_tip = ( "面积是 Face 的几何面积;U向尺寸/V向尺寸是这个 Face 自身参数方向上的两个投影尺寸;" - "偏移曲面使用 Creo 曲面 Offset 语义,表示沿当前 Face 法向得到的目标位置。" - "影响范围决定这次修改是局部重建当前 Face、按拉伸/切除加减材料," + "偏移变换使用 Creo 柔性建模里的 Offset 语义,表示沿当前 Face 法向得到的目标位置。" + "建模意图决定这次修改是局部重建当前 Face、按拉伸/切除加减材料," "还是移动或缩放所属特征/Solid。" ) if is_plane and not local_face_deform_ready: @@ -2327,7 +2618,7 @@ class WindowStateMixin: ) add_readonly_spec( key="face_edit_semantics", - label="编辑方式", + label="建模意图", text=face_semantics_text, tip=face_semantics_tip, ) @@ -2335,10 +2626,10 @@ class WindowStateMixin: if has_edge: add_readonly_spec( key="edge_edit_semantics", - label="编辑方式", + label="建模意图", text="Edge:长度修改可选择局部形变、移动端面或缩放所属对象。", tip=( - "只变当前Edge会重建局部相邻面;移动端面更像整体尺寸变化;" + "只改当前Edge会重建局部相邻面;移动端面更像整体尺寸变化;" "缩放所属会影响同一特征或 Solid 上的其它尺寸。" ), ) @@ -2571,7 +2862,7 @@ class WindowStateMixin: ) add_scoped_spec( key="face_target_normal_position", - label="偏移曲面", + label="偏移变换", current_raw=current_plane_position if current_plane_position is not None else "", target_text=numeric_text(current_plane_position), scope_default="push_pull", @@ -2581,10 +2872,10 @@ class WindowStateMixin: "action": "push_pull_face", "target_attr": "offset_input", "enabled": current_plane_position is not None, - "enabled_tip": "输入偏移曲面的目标位置;程序会沿当前 Face 法向执行拉伸或切除。", + "enabled_tip": "输入偏移变换的目标位置;程序会沿当前 Face 法向执行拉伸或切除。", "disabled_tip": "当前平面缺少稳定移动方向或基准点,不能按目标位置拉伸/切除。", "range_hint": ( - "偏移曲面是沿当前 Face 法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。" + "偏移变换是沿当前 Face 法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。" "程序会把目标位置自动换算成本次拉伸/切除距离。" f"{face_offset_hard_limit}" ), @@ -2602,11 +2893,11 @@ class WindowStateMixin: and current_face_center is not None ), "enabled_tip": ( - "输入偏移曲面的目标位置;程序只把当前 Face 沿法向移动到该目标值," + "输入偏移变换的目标位置;程序只把当前 Face 沿法向移动到该目标值," "并让相邻平面按新顶点重建。" ), "disabled_tip": face_local_disabled_tip( - "当前平面缺少稳定方向、基准点或中心,不能按偏移曲面局部重建当前 Face。" + "当前平面缺少稳定方向、基准点或中心,不能按偏移变换局部重建当前 Face。" ), "range_hint": ( "局部重建不会自动加料/切削,相邻面可能自然变斜," @@ -2626,10 +2917,10 @@ class WindowStateMixin: and (self.selected_part_id is not None or self.selected_solid_id is not None) ), "enabled_tip": ( - "输入偏移曲面的目标位置;程序会沿当前 Face 法向平移所属特征或 Solid," + "输入偏移变换的目标位置;程序会沿当前 Face 法向平移所属特征或 Solid," "当前面形状和所属对象内部尺寸不变。" ), - "disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移曲面移动所属特征。", + "disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移变换移动所属特征。", "range_hint": ( "这是移动所属对象,不是拉伸/切除当前 Face;如果想改变形状或厚度,请选择“拉伸/切除”。" f"{translation_hint()} {face_offset_hard_limit}" @@ -2682,7 +2973,7 @@ class WindowStateMixin: "disabled_tip": "当前壳体候选缺少稳定厚度、厚度方向、基准平面或所属对象,不能按厚度缩放所属特征。", "range_hint": ( "这是缩放所属对象,不是单独移动当前平面;" - f"如果只想移动当前壳体平面区域,请把影响范围设为“拉伸/切除”。 {relative_range_hint(current, 0.2, 0.5, face_linear_hard_limit)}" + f"如果只想移动当前壳体平面区域,请把建模意图设为“拉伸/切除”。 {relative_range_hint(current, 0.2, 0.5, face_linear_hard_limit)}" ), }, }, @@ -4788,7 +5079,7 @@ class WindowStateMixin: self._update_action_states() self._refresh_property_editor() label = self.edge_length_strategy_combo.currentText() if hasattr(self, "edge_length_strategy_combo") else "" - self.statusBar().showMessage(f"已设置修改策略:{label or '自动策略'}。") + self.statusBar().showMessage(f"已设置建模意图:{label or '自动选择'}。") def set_chamfer_reference_face(self) -> None: self._update_action_states() @@ -5126,6 +5417,7 @@ class WindowStateMixin: return result if self.selected_kind == "edge" and self.selected_edge_id is not None: result = self.model.edge_info(self.selected_edge_id) + result.update(self._edge_first_level_selection_fields(self.selected_edge_id)) self._selected_action_info_cache_key = cache_key self._selected_action_info_cache_value = dict(result) return result @@ -5200,6 +5492,7 @@ class WindowStateMixin: resolved_edge_id = self._resolve_record_edge_id(record) if resolved_edge_id is not None: info = self.model.edge_info(resolved_edge_id) + info.update(self._edge_first_level_selection_fields(resolved_edge_id)) self.selected_kind = "edge" self.selected_edge_id = resolved_edge_id self.selected_part_id = int(info["part_id"])