feat: 完善Face保持关系守门与大模型预判

This commit is contained in:
2026-08-10 16:51:52 +08:00
parent 415e8a0f27
commit 61f0b76e59
11 changed files with 218 additions and 21 deletions
+6 -6
View File
@@ -192,8 +192,8 @@ STEP/B-Rep 参数化编辑主线
├── 1. 平面 Face,第一条主线 ├── 1. 平面 Face,第一条主线
│ ├── [x] [偏移 -> 推拉平面 / 拉伸切除] │ ├── [x] [偏移 -> 推拉平面 / 拉伸切除]
│ │ └── 当前 Face 移动或加料/切削,边、点和共享边一级相邻面跟随重建,复杂端盖走隔离和回滚。 │ │ └── 当前 Face 移动或加料/切削,边、点和共享边一级相邻面跟随重建,复杂端盖走隔离和回滚。
│ ├── [~] [偏移 + 保持关系 -> 受限拉伸/切除] │ ├── [x] [偏移 + 保持关系 -> 受限拉伸/切除]
│ │ └── 偏移行新增“保持关系”建模意图:执行前要求一级相邻 Face 都能验证为平面平行/垂直关系,执行后反查关系;非平面、斜交或复杂多边界慢计划会提前阻止。 │ │ └── 偏移行新增“保持关系”建模意图:执行前要求一级相邻 Face 都能验证为平面平行/垂直关系,执行后反查关系;非平面、斜交或复杂多边界慢计划会提前阻止并有回归守门
│ ├── [x] [面内长度 -> 改长度] │ ├── [x] [面内长度 -> 改长度]
│ │ └── 近矩形平面 Face 沿长度方向变化,一级相邻面跟随,面积不作为驱动参数。 │ │ └── 近矩形平面 Face 沿长度方向变化,一级相邻面跟随,面积不作为驱动参数。
│ ├── [x] [面内宽度 -> 改宽度] │ ├── [x] [面内宽度 -> 改宽度]
@@ -1192,8 +1192,8 @@ pythonocc-step-editor/
verify_face_invalid_target_guards.py # 验证 Face 面内尺寸和壳体厚度不能输入非数字、0 或负数 verify_face_invalid_target_guards.py # 验证 Face 面内尺寸和壳体厚度不能输入非数字、0 或负数
verify_large_stepped_cap_push_pull.py # 验证大 STEP 里的同轴阶梯环形端盖拉伸/切除走局部 shell 重建而不是 180s 布尔慢路径 verify_large_stepped_cap_push_pull.py # 验证大 STEP 里的同轴阶梯环形端盖拉伸/切除走局部 shell 重建而不是 180s 布尔慢路径
verify_face_logical_selection_retention.py # 验证 Face 修改后逻辑 ID 仍指向被编辑的新面 verify_face_logical_selection_retention.py # 验证 Face 修改后逻辑 ID 仍指向被编辑的新面
verify_face_mixed_surface_guard.py # 验证圆柱端盖这类平面 Face 会禁用局部变形,但保留拉伸/切除 verify_face_mixed_surface_guard.py # 验证圆柱端盖这类平面 Face 会禁用局部变形,但保留拉伸/切除;保持关系会因非平面一级邻域阻止
verify_face_nonrectangular_local_edit.py # 验证三角平面 Face 的局部/整体中心、面内尺寸、偏移和拉伸/切除 verify_face_nonrectangular_local_edit.py # 验证三角平面 Face 的局部/整体中心、面内尺寸、偏移和拉伸/切除;斜交一级邻域会阻止保持关系
verify_face_noop_guards.py # 验证 Face 目标值不变时会被阻止 verify_face_noop_guards.py # 验证 Face 目标值不变时会被阻止
verify_first_level_fact_graph.py # 验证平面、孔、槽会输出统一一级拓扑事实包,且编辑计划会携带这份事实 verify_first_level_fact_graph.py # 验证平面、孔、槽会输出统一一级拓扑事实包,且编辑计划会携带这份事实
verify_feature_recognition_summary.py # 验证平面、孔、槽、凸台候选会输出统一识别摘要、常用优先级、置信度、风险和证据 verify_feature_recognition_summary.py # 验证平面、孔、槽、凸台候选会输出统一识别摘要、常用优先级、置信度、风险和证据
@@ -1341,7 +1341,7 @@ git diff --check
- `scripts/verify_face_first_level_topology.py`:加载 `cube_10mm.step`,验证正方体上表面 Face 的一级关系只包含当前 Face、4 条边界 Edge、4 个边界 Vertex 和 4 个共享边相邻侧面;局部移动上表面后,目标 Face 的边界 Vertex 与共享边 Edge 会到达新高度,4 个一级侧面会跟随重建,二级底面保持不动。 - `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_coplanar_push_pull.py`:临时生成顶部被拆成两片共面 Face 的单 Solid,验证选中其中一片时会把整片共面区域作为一个 profile 拉伸/切除,并确认拉伸/切除后原逻辑 Face ID 会定位到新生成的整片顶面。
- `scripts/verify_face_complex_boundary_guard.py`:临时生成带通孔板件,验证带内孔的平面 Face 会报告边界环数量,并禁用 `局部重建` 的面内长度/面内宽度、中心和偏移计划;同时确认 `拉伸/切除` 的向外补料和向内切削都仍可用并保持单 Solid,且同一 Solid 的局部变形可用性判断会复用缓存。 - `scripts/verify_face_complex_boundary_guard.py`:临时生成带通孔板件,验证带内孔的平面 Face 会报告边界环数量,并禁用 `局部重建` 的面内长度/面内宽度、中心和偏移计划;同时确认 `拉伸/切除` 的向外补料和向内切削都仍可用并保持单 Solid,且同一 Solid 的局部变形可用性判断会复用缓存。
- `scripts/verify_face_mixed_surface_guard.py`:临时生成简单圆柱,验证圆柱端盖这个平面 Face 会因为所属 Solid 含有曲面而禁用 `局部重建` 的中心和偏移计划,属性提示会说明具体原因;同时确认 `拉伸/切除` 仍可用并让圆柱高度增加。 - `scripts/verify_face_mixed_surface_guard.py`:临时生成简单圆柱,验证圆柱端盖这个平面 Face 会因为所属 Solid 含有曲面而禁用 `局部重建` 的中心和偏移计划,属性提示会说明具体原因;同时确认 `拉伸/切除` 仍可用并让圆柱高度增加`保持关系` 会因一级相邻含非平面/曲面而在 UI 和计划阶段阻止
- `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 面内尺寸和壳体厚度的局部/整体计划在目标值为非数字、0 或负数时都会返回 blocked,并提示目标值必须是有效正数;历史面积路径只作为后端守门回归保留。 - `scripts/verify_face_invalid_target_guards.py`:验证 Face 面内尺寸和壳体厚度的局部/整体计划在目标值为非数字、0 或负数时都会返回 blocked,并提示目标值必须是有效正数;历史面积路径只作为后端守门回归保留。
@@ -1349,7 +1349,7 @@ git diff --check
- `scripts/verify_face_logical_selection_retention.py`:验证 Face 的局部/整体面内尺寸、局部/整体中心、局部/整体偏移和拉伸/切除正负方向完成后,原逻辑 Face ID 会排他地指向被编辑后的新 Face,避免 UI 改完参数后选中跑到别的面;历史面积路径只作为后端结果绑定回归保留。 - `scripts/verify_face_logical_selection_retention.py`:验证 Face 的局部/整体面内尺寸、局部/整体中心、局部/整体偏移和拉伸/切除正负方向完成后,原逻辑 Face ID 会排他地指向被编辑后的新 Face,避免 UI 改完参数后选中跑到别的面;历史面积路径只作为后端结果绑定回归保留。
- `scripts/verify_selection_identity_ui.py`:验证编辑后当前拓扑 Face ID 改变时,Face / 特征标题会显示 `逻辑 Face ID(拓扑 Face ID`,复制 ID 会优先复制可再次选择的逻辑 Face ID,避免把用户带到重建后的临时拓扑编号上。 - `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_feature_parameter_consistency.py`:加载默认模型 Face 594,验证轻量 `当前特征` 选择在 full feature 缓存前后都显示同一组基础 Face 参数,避免先改其它对象或触发深层识别后,同一个 Face 的属性列表突然缩水或膨胀。
- `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `局部重建``缩放特征``移动特征``拉伸/切除` 语义,在中心、面内尺寸和偏移场景下都能保持单 Solid。 - `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `局部重建``缩放特征``移动特征``拉伸/切除` 语义,在中心、面内尺寸和偏移场景下都能保持单 Solid;同时生成斜顶楔块,验证斜交一级相邻平面会提前阻止 `保持关系`
- `scripts/verify_face_resize_semantics.py`:加载 `cube_10mm.step`,验证 Face 的面内长度/面内宽度、中心坐标和偏移在 `当前面` 局部编辑、`整体` 所属对象编辑、`拉伸/切除``保持关系` 之间不会互相混淆;面积路径只作为后端缩放语义回归保留。 - `scripts/verify_face_resize_semantics.py`:加载 `cube_10mm.step`,验证 Face 的面内长度/面内宽度、中心坐标和偏移在 `当前面` 局部编辑、`整体` 所属对象编辑、`拉伸/切除``保持关系` 之间不会互相混淆;面积路径只作为后端缩放语义回归保留。
- `scripts/verify_face_noop_guards.py`:验证 Face 的拉伸/切除、偏移、中心、面内尺寸和壳体厚度在目标值等于当前值时都会返回 blocked,并给出“不需要修改”的说明。 - `scripts/verify_face_noop_guards.py`:验证 Face 的拉伸/切除、偏移、中心、面内尺寸和壳体厚度在目标值等于当前值时都会返回 blocked,并给出“不需要修改”的说明。
- `scripts/verify_first_level_fact_graph.py`:验证正方体平面 Face、通孔圆柱面和槽/半孔候选都会输出统一一级拓扑事实包,包含主体 Face、边界 Edge/Vertex、共享边一级相邻 Face、角色分组,以及明确不自动传播的二级/三级关系;同时验证 Face 拉伸/切除/面积/中心移动、孔径修改和槽宽修改的编辑计划都会携带这份事实。 - `scripts/verify_first_level_fact_graph.py`:验证正方体平面 Face、通孔圆柱面和槽/半孔候选都会输出统一一级拓扑事实包,包含主体 Face、边界 Edge/Vertex、共享边一级相邻 Face、角色分组,以及明确不自动传播的二级/三级关系;同时验证 Face 拉伸/切除/面积/中心移动、孔径修改和槽宽修改的编辑计划都会携带这份事实。
+4
View File
@@ -58,6 +58,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = (
"face offset, push pull", "face offset, push pull",
("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "push_pull", "--offset-distance", "1"), ("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "push_pull", "--offset-distance", "1"),
), ),
(
"face offset, keep first-level planar relations",
("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "keep_relations", "--offset-distance", "1"),
),
( (
"face offset, push pull inward cut", "face offset, push pull inward cut",
("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "push_pull", "--offset-distance", "-1"), ("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "push_pull", "--offset-distance", "-1"),
@@ -15,12 +15,12 @@ from scripts.verify_large_stepped_cap_push_pull import _large_multi_boundary_cap
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step" MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
FACE_ID = 594 FACE_ID = 594
BASE_FACE_KEYS = ( BASE_FACE_KEYS = (
"area",
"local_face_width", "local_face_width",
"local_face_height", "local_face_height",
"face_center_position", "face_center_position",
"face_target_normal_position", "face_target_normal_position",
) )
RESULT_ONLY_KEYS = ("area",)
class _Probe(WindowStateMixin): class _Probe(WindowStateMixin):
@@ -71,6 +71,12 @@ def _assert_contains(keys: tuple[str, ...], expected: tuple[str, ...], label: st
raise AssertionError(f"{label} missing {missing}, got {keys}") raise AssertionError(f"{label} missing {missing}, got {keys}")
def _assert_absent(keys: tuple[str, ...], forbidden: tuple[str, ...], label: str) -> None:
leaked = [key for key in forbidden if key in keys]
if leaked:
raise AssertionError(f"{label} should keep result-only values out of feature parameters: {leaked}")
def _assert_face_594_stays_stable_after_remote_cap_edit() -> None: def _assert_face_594_stays_stable_after_remote_cap_edit() -> None:
model = StepModel.load(MODEL_PATH) model = StepModel.load(MODEL_PATH)
source_face_id = _large_stepped_cap_face(model) source_face_id = _large_stepped_cap_face(model)
@@ -123,16 +129,19 @@ def main() -> int:
before_cached_rows = _feature_rows(model, FACE_ID) before_cached_rows = _feature_rows(model, FACE_ID)
_assert_contains(before_cached_rows, BASE_FACE_KEYS, "Face 594 before full feature cache") _assert_contains(before_cached_rows, BASE_FACE_KEYS, "Face 594 before full feature cache")
_assert_absent(before_cached_rows, RESULT_ONLY_KEYS, "Face 594 before full feature cache")
full_info = model.feature_info(FACE_ID) full_info = model.feature_info(FACE_ID)
if full_info.get("shell_region_status") == "candidate": if full_info.get("shell_region_status") == "candidate":
dimension_keys = _feature_dimension_keys(full_info) dimension_keys = _feature_dimension_keys(full_info)
_assert_contains(dimension_keys, BASE_FACE_KEYS, "shell candidate feature dimensions") _assert_contains(dimension_keys, BASE_FACE_KEYS, "shell candidate feature dimensions")
_assert_absent(dimension_keys, RESULT_ONLY_KEYS, "shell candidate feature dimensions")
if "shell_thickness_estimate" not in dimension_keys: if "shell_thickness_estimate" not in dimension_keys:
raise AssertionError(f"shell candidate should keep shell thickness as an extra dimension: {dimension_keys}") raise AssertionError(f"shell candidate should keep shell thickness as an extra dimension: {dimension_keys}")
after_cached_rows = _feature_rows(model, FACE_ID) after_cached_rows = _feature_rows(model, FACE_ID)
_assert_contains(after_cached_rows, BASE_FACE_KEYS, "Face 594 after full feature cache") _assert_contains(after_cached_rows, BASE_FACE_KEYS, "Face 594 after full feature cache")
_assert_absent(after_cached_rows, RESULT_ONLY_KEYS, "Face 594 after full feature cache")
if before_cached_rows != after_cached_rows: if before_cached_rows != after_cached_rows:
raise AssertionError( raise AssertionError(
"Face 594 current-only feature rows changed after full recognition cache: " "Face 594 current-only feature rows changed after full recognition cache: "
+8 -2
View File
@@ -144,12 +144,18 @@ def _assert_selection_exposes_first_level(model: StepModel, face_id: int) -> Non
raise SystemExit(f"Selected Face should expose 4 first-level adjacent Faces, got {feature_info}") raise SystemExit(f"Selected Face should expose 4 first-level adjacent Faces, got {feature_info}")
editable_specs, _used = probe._editable_property_specs(feature_info) editable_specs, _used = probe._editable_property_specs(feature_info)
feature_rows = probe._feature_property_specs(editable_specs, feature_info) topology_row = _spec(editable_specs, "face_first_level_topology")
topology_row = _spec(feature_rows, "face_first_level_topology")
topology_text = str(topology_row.get("current_text") or "") topology_text = str(topology_row.get("current_text") or "")
for fragment in ("Face 区域 1 个", "边界 Edge 4 条", "共享边相邻 Face 4 个"): for fragment in ("Face 区域 1 个", "边界 Edge 4 条", "共享边相邻 Face 4 个"):
if fragment not in topology_text: if fragment not in topology_text:
raise SystemExit(f"Selected Face topology row is not clear enough: {topology_row}") raise SystemExit(f"Selected Face topology row is not clear enough: {topology_row}")
feature_rows = probe._feature_property_specs(editable_specs, feature_info)
feature_row_keys = {str(spec.get("key", "")) for spec in feature_rows}
if "face_first_level_topology" in feature_row_keys:
raise SystemExit(
"Feature parameter table should keep first-level topology in diagnostics, "
f"not in editable feature rows: {feature_rows}"
)
def main() -> int: def main() -> int:
+5 -2
View File
@@ -120,8 +120,11 @@ def main() -> int:
feature_keys = {str(spec.get("key")) for spec in feature_rows} feature_keys = {str(spec.get("key")) for spec in feature_rows}
if "no_editable_feature_dimensions" not in feature_keys: if "no_editable_feature_dimensions" not in feature_keys:
raise SystemExit(f"feature mode should say there are no reliable dimensions: {feature_rows}") raise SystemExit(f"feature mode should say there are no reliable dimensions: {feature_rows}")
if "freeform_surface_edit_semantics" not in feature_keys: if "freeform_surface_edit_semantics" in feature_keys:
raise SystemExit(f"feature mode should keep the freeform limitation explanation: {feature_rows}") raise SystemExit(
"feature parameter table should keep freeform limitations in diagnostics, "
f"not mixed into editable parameters: {feature_rows}"
)
_assert_no_editable_actions(feature_rows, "feature mode rows") _assert_no_editable_actions(feature_rows, "feature mode rows")
print( print(
+29 -1
View File
@@ -166,6 +166,19 @@ def _assert_local_scope_explains_curved_owner(specs: list[dict[str, object]], ke
raise SystemExit(f"{key}/local disabled tip should mention curved owner: {disabled_tip}") raise SystemExit(f"{key}/local disabled tip should mention curved owner: {disabled_tip}")
def _assert_keep_relations_blocked(plan: dict[str, object], label: str, *expected_fragments: str) -> None:
if plan.get("status") != "blocked":
raise SystemExit(f"{label} should be blocked before execution: {plan}")
if plan.get("face_push_pull_planar_relation_constraint_requested") is not True:
raise SystemExit(f"{label} should record the requested keep-relation constraint: {plan}")
if plan.get("face_push_pull_planar_constraint_status") != "blocked":
raise SystemExit(f"{label} should expose blocked keep-relation constraint status: {plan}")
message = str(plan.get("message") or "") + " " + str(plan.get("blockers") or "")
for fragment in expected_fragments:
if fragment not in message:
raise SystemExit(f"{label} blocker should mention {fragment!r}: {plan}")
def _bbox_height(model: StepModel) -> float: def _bbox_height(model: StepModel) -> float:
info = model.part_info(1) info = model.part_info(1)
bbox_size = info.get("bbox_size") bbox_size = info.get("bbox_size")
@@ -199,6 +212,12 @@ def main() -> int:
_assert_blocked(model.face_center_local_move_plan(face_id, target_center), "Face center local move") _assert_blocked(model.face_center_local_move_plan(face_id, target_center), "Face center local move")
_assert_blocked(model.face_area_local_resize_plan(face_id, area * 1.1), "Face area local resize") _assert_blocked(model.face_area_local_resize_plan(face_id, area * 1.1), "Face area local resize")
_assert_blocked(model.face_plane_offset_local_plan(face_id, 1.0), "Face plane offset local move") _assert_blocked(model.face_plane_offset_local_plan(face_id, 1.0), "Face plane offset local move")
_assert_keep_relations_blocked(
model.push_pull_keep_relations_plan(face_id, 1.0),
"planar cylinder cap keep-relations push/pull",
"非平面",
"平面邻域",
)
specs = _specs(face_id, info) specs = _specs(face_id, info)
semantics = _spec(specs, "face_edit_semantics") semantics = _spec(specs, "face_edit_semantics")
@@ -206,11 +225,20 @@ def main() -> int:
raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}") raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}")
if "曲面" not in str(semantics.get("disabled_tip") or ""): if "曲面" not in str(semantics.get("disabled_tip") or ""):
raise SystemExit(f"Face edit semantics tip should include the curved-owner blocker: {semantics}") raise SystemExit(f"Face edit semantics tip should include the curved-owner blocker: {semantics}")
for key in ("area", "face_center_position", "face_target_normal_position"): for key in ("face_center_position", "face_target_normal_position"):
_assert_local_scope_explains_curved_owner(specs, key) _assert_local_scope_explains_curved_owner(specs, key)
push_pull_mode = _scope_mode(specs, "face_target_normal_position", "push_pull") push_pull_mode = _scope_mode(specs, "face_target_normal_position", "push_pull")
if not bool(push_pull_mode.get("enabled", False)): if not bool(push_pull_mode.get("enabled", False)):
raise SystemExit("planar cylinder cap push/pull scope should remain available") raise SystemExit("planar cylinder cap push/pull scope should remain available")
keep_relations_mode = _scope_mode(specs, "face_target_normal_position", "keep_relations")
if bool(keep_relations_mode.get("enabled", True)):
raise SystemExit(f"planar cylinder cap keep-relations scope should be disabled: {keep_relations_mode}")
keep_relations_tip = str(keep_relations_mode.get("disabled_tip") or "")
if "非平面" not in keep_relations_tip and "曲面" not in keep_relations_tip:
raise SystemExit(
"planar cylinder cap keep-relations disabled tip should mention non-planar adjacency: "
f"{keep_relations_tip}"
)
huge_push_plan = model.push_pull_plan(face_id, 50.0) huge_push_plan = model.push_pull_plan(face_id, 50.0)
if huge_push_plan.get("status") == "blocked": if huge_push_plan.get("status") == "blocked":
@@ -32,6 +32,22 @@ def _write_triangular_prism(path: Path) -> None:
_write_step(prism, path) _write_step(prism, path)
def _write_trapezoid_prism(path: Path) -> None:
polygon = BRepBuilderAPI_MakePolygon()
polygon.Add(gp_Pnt(0.0, 0.0, 0.0))
polygon.Add(gp_Pnt(12.0, 0.0, 0.0))
polygon.Add(gp_Pnt(10.0, 0.0, 6.0))
polygon.Add(gp_Pnt(0.0, 0.0, 8.0))
polygon.Close()
if hasattr(polygon, "IsDone") and not polygon.IsDone():
raise RuntimeError("Could not create trapezoid prism profile.")
face_maker = BRepBuilderAPI_MakeFace(polygon.Wire())
if hasattr(face_maker, "IsDone") and not face_maker.IsDone():
raise RuntimeError("Could not create trapezoid prism face.")
prism = BRepPrimAPI_MakePrism(face_maker.Face(), gp_Vec(0.0, 6.0, 0.0)).Shape()
_write_step(prism, path)
def _triangle_face_id(model: StepModel) -> int: def _triangle_face_id(model: StepModel) -> int:
for face_id in range(len(model.faces)): for face_id in range(len(model.faces)):
info = model.face_info(face_id) info = model.face_info(face_id)
@@ -42,6 +58,19 @@ def _triangle_face_id(model: StepModel) -> int:
raise SystemExit("no triangular planar Face was found") raise SystemExit("no triangular planar Face was found")
def _sloped_quad_face_id(model: StepModel) -> int:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
normal = info.get("normal") or info.get("push_pull_outward_direction")
if not isinstance(normal, tuple) or len(normal) != 3:
continue
if abs(float(normal[0])) > 0.1 and abs(float(normal[2])) > 0.1:
return face_id
raise SystemExit("no sloped quadrilateral planar Face was found")
def _triangle_infos(model: StepModel) -> list[dict[str, object]]: def _triangle_infos(model: StepModel) -> list[dict[str, object]]:
infos: list[dict[str, object]] = [] infos: list[dict[str, object]] = []
for face_id in range(len(model.faces)): for face_id in range(len(model.faces)):
@@ -89,6 +118,20 @@ def _assert_not_blocked(plan: dict[str, object], label: str) -> None:
raise SystemExit(f"{label} should be available for a simple triangular Face: {plan}") raise SystemExit(f"{label} should be available for a simple triangular Face: {plan}")
def _assert_keep_relations_blocked_for_angled(plan: dict[str, object], label: str) -> None:
if plan.get("status") != "blocked":
raise SystemExit(f"{label} should be blocked before execution: {plan}")
if plan.get("face_push_pull_planar_relation_constraint_requested") is not True:
raise SystemExit(f"{label} should record the requested keep-relation constraint: {plan}")
if plan.get("face_push_pull_planar_constraint_status") != "blocked":
raise SystemExit(f"{label} should expose blocked keep-relation constraint status: {plan}")
if int(plan.get("face_push_pull_planar_constraint_angled_count", 0) or 0) <= 0:
raise SystemExit(f"{label} should count at least one angled relation: {plan}")
message = str(plan.get("message") or "") + " " + str(plan.get("blockers") or "")
if "斜交" not in message:
raise SystemExit(f"{label} blocker should explain angled first-level planar relations: {plan}")
def _assert_single_solid(model: StepModel, label: str) -> None: def _assert_single_solid(model: StepModel, label: str) -> None:
stats = model.stats() stats = model.stats()
if stats.solids != 1: if stats.solids != 1:
@@ -162,6 +205,15 @@ def main() -> int:
model.push_pull_face(face_id, 1.0) model.push_pull_face(face_id, 1.0)
_assert_single_solid(model, "triangular Face push/pull") _assert_single_solid(model, "triangular Face push/pull")
wedge_path = Path(temp_dir) / "trapezoid_prism.step"
_write_trapezoid_prism(wedge_path)
wedge = StepModel.load(wedge_path)
wedge_face_id = _sloped_quad_face_id(wedge)
plan = wedge.push_pull_plan(wedge_face_id, 0.5)
_assert_not_blocked(plan, "sloped planar Face push/pull")
keep_plan = wedge.push_pull_keep_relations_plan(wedge_face_id, 0.5)
_assert_keep_relations_blocked_for_angled(keep_plan, "sloped planar Face keep-relations push/pull")
model, face_id, info = _fresh_model(path) model, face_id, info = _fresh_model(path)
current_area = float(info.get("area") or 0.0) current_area = float(info.get("area") or 0.0)
target_area = current_area * 1.44 target_area = current_area * 1.44
@@ -136,7 +136,11 @@ def main() -> int:
face_id = _large_stepped_cap_face(model) face_id = _large_stepped_cap_face(model)
logical_id = model.face_region_logical_id(face_id) logical_id = model.face_region_logical_id(face_id)
before_topology = _face_topology_counts(model, face_id) before_topology = _face_topology_counts(model, face_id)
started = time.perf_counter()
plan = model.push_pull_plan(face_id, 89.0) plan = model.push_pull_plan(face_id, 89.0)
plan_elapsed = time.perf_counter() - started
if plan_elapsed > 5.0:
raise SystemExit(f"large stepped cap push/pull plan took too long: {plan_elapsed:.3f}s; plan={plan}")
if plan.get("cylindrical_cap_extension_kind") != "coaxial-stepped-cap": if plan.get("cylindrical_cap_extension_kind") != "coaxial-stepped-cap":
raise SystemExit(f"large stepped cap should be recognized as stepped cap: {plan}") raise SystemExit(f"large stepped cap should be recognized as stepped cap: {plan}")
if plan.get("cylindrical_cap_extension_method") != "local-shell-rebuild": if plan.get("cylindrical_cap_extension_method") != "local-shell-rebuild":
@@ -432,7 +436,8 @@ def main() -> int:
print( print(
"large stepped cap push/pull ok: " "large stepped cap push/pull ok: "
f"face_id={face_id}, elapsed={elapsed:.3f}s, isolated_elapsed={isolated_elapsed:.3f}s, " f"face_id={face_id}, plan_elapsed={plan_elapsed:.3f}s, "
f"elapsed={elapsed:.3f}s, isolated_elapsed={isolated_elapsed:.3f}s, "
f"topology_before={before_topology}, topology_after={after_topology}, result={result}" f"topology_before={before_topology}, topology_after={after_topology}, result={result}"
) )
print( print(
+16 -1
View File
@@ -729,6 +729,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
center=_tuple_or_none(info.get("area_center")), center=_tuple_or_none(info.get("area_center")),
) )
) )
try:
info.update(self.face_first_level_topology(face_id))
info.update(self.face_first_level_facts(face_id, scope="face"))
except Exception as exc:
info.setdefault("topology_relation_status", "unavailable")
info.setdefault("topology_relation_message", str(exc))
info.setdefault("first_level_planar_relation_status", "unavailable")
info.setdefault("first_level_planar_relation_summary", str(exc))
elif surface_type == GeomAbs_Cylinder: elif surface_type == GeomAbs_Cylinder:
cyl = surf.Cylinder() cyl = surf.Cylinder()
axis = cyl.Axis() axis = cyl.Axis()
@@ -1737,6 +1745,12 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
) -> dict[str, object]: ) -> dict[str, object]:
if info.get("surface") != "plane": if info.get("surface") != "plane":
return {} return {}
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
inner_boundary_wires = int(info.get("inner_boundary_wires", 0) or 0)
if inner_boundary_wires > 1:
return {}
if inner_boundary_wires == 1 and len(boundary_edge_ids) < 6:
return {}
if str(prismatic_info.get("prismatic_feature_semantics") or "") in {"additive-boss", "subtractive-pocket"}: if str(prismatic_info.get("prismatic_feature_semantics") or "") in {"additive-boss", "subtractive-pocket"}:
return {} return {}
try: try:
@@ -1748,7 +1762,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
except Exception: except Exception:
return {} return {}
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
adjacent_face_ids = sorted(set(self._adjacent_face_ids_for_edges(boundary_edge_ids, face_id)) - {face_id}) adjacent_face_ids = sorted(set(self._adjacent_face_ids_for_edges(boundary_edge_ids, face_id)) - {face_id})
if len(adjacent_face_ids) < 6: if len(adjacent_face_ids) < 6:
return {} return {}
@@ -2340,6 +2353,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
shell_info: dict[str, object], shell_info: dict[str, object],
prismatic_info: dict[str, object], prismatic_info: dict[str, object],
) -> dict[str, object]: ) -> dict[str, object]:
if bool(info.get("has_inner_boundaries")) or int(info.get("inner_boundary_wires", 0) or 0) > 0:
return {}
try: try:
solid_id = int(info.get("solid_id", -1)) solid_id = int(info.get("solid_id", -1))
except (TypeError, ValueError): except (TypeError, ValueError):
+13
View File
@@ -369,6 +369,19 @@ class OperationMixin:
return plan return plan
def _multistep_prismatic_plan_blocker(self, face_id: int, operation_label: str) -> str: def _multistep_prismatic_plan_blocker(self, face_id: int, operation_label: str) -> str:
try:
quick_info = self.quick_face_info(face_id)
except Exception:
quick_info = {}
if quick_info:
if str(quick_info.get("surface") or "") != "plane":
return ""
inner_boundary_wires = int(quick_info.get("inner_boundary_wires", 0) or 0)
boundary_edges = int(quick_info.get("boundary_edges", 0) or 0)
if inner_boundary_wires > 1:
return ""
if inner_boundary_wires == 1 and boundary_edges < 6:
return ""
try: try:
feature = self.feature_info(face_id) feature = self.feature_info(face_id)
except Exception: except Exception:
+69 -7
View File
@@ -2466,13 +2466,37 @@ class WindowStateMixin:
root_specs: list[dict[str, object]], root_specs: list[dict[str, object]],
action_info: dict[str, object], action_info: dict[str, object],
) -> list[dict[str, object]]: ) -> list[dict[str, object]]:
root_rows = self._feature_property_specs(root_specs, action_info) def no_editable_feature_dimensions_spec() -> dict[str, object]:
associated = action_info.get("associated_feature_infos") reason = str(
if not isinstance(associated, (list, tuple)) or not associated: action_info.get("freeform_face_blockers")
return root_rows or action_info.get("recognition_blockers")
or action_info.get("local_face_deform_blocker")
or action_info.get("feature_edit_actions")
or "当前识别结果没有稳定可修改参数;详细原因请看诊断信息。"
).strip()
if not reason:
reason = "当前识别结果没有稳定可修改参数;详细原因请看诊断信息。"
return {
"key": "no_editable_feature_dimensions",
"label": "可修改参数",
"current_text": "当前无可修改参数",
"current_raw": "",
"target_text": "",
"editable": False,
"enabled": False,
"status_text": "不可修改",
"disabled_tip": reason,
"span_value_columns": True,
"parameter_role": "empty_state",
}
root_rows = self._feature_property_specs(root_specs, action_info)
root_dimensions = [dict(spec) for spec in root_rows if spec.get("parameter_role") == "dimension"] root_dimensions = [dict(spec) for spec in root_rows if spec.get("parameter_role") == "dimension"]
related_rows: list[dict[str, object]] = [] related_rows: list[dict[str, object]] = []
associated = action_info.get("associated_feature_infos")
if not isinstance(associated, (list, tuple)) or not associated:
return root_dimensions or [no_editable_feature_dimensions_spec()]
for index, related_info in enumerate(associated, start=1): for index, related_info in enumerate(associated, start=1):
if not isinstance(related_info, dict): if not isinstance(related_info, dict):
continue continue
@@ -2492,7 +2516,8 @@ class WindowStateMixin:
related["association_index"] = index related["association_index"] = index
related_rows.append(related) related_rows.append(related)
return root_dimensions + related_rows rows = root_dimensions + related_rows
return rows or [no_editable_feature_dimensions_spec()]
def _ordered_property_info_items(self, info: dict[str, object]) -> list[tuple[str, object]]: def _ordered_property_info_items(self, info: dict[str, object]) -> list[tuple[str, object]]:
items = self._ordered_info_items(info) items = self._ordered_info_items(info)
@@ -3572,6 +3597,43 @@ class WindowStateMixin:
+ plane_origin[1] * plane_direction[1] + plane_origin[1] * plane_direction[1]
+ plane_origin[2] * plane_direction[2] + plane_origin[2] * plane_direction[2]
) )
keep_relation_fields_present = any(
key in action_info
for key in (
"first_level_planar_relation_status",
"first_level_planar_relation_count",
"first_level_planar_relation_angled_count",
"first_level_planar_relation_non_planar_face_ids",
"first_level_adjacent_surface_types",
)
)
keep_relation_enabled = current_plane_position is not None
keep_relation_disabled_tip = "当前平面缺少稳定移动方向或基准点,不能保持一级平面关系。"
if keep_relation_enabled and keep_relation_fields_present:
relation_status = str(action_info.get("first_level_planar_relation_status") or "")
relation_count = _int_or_none(action_info.get("first_level_planar_relation_count")) or 0
angled_count = _int_or_none(action_info.get("first_level_planar_relation_angled_count")) or 0
non_planar_face_ids = _int_values(action_info.get("first_level_planar_relation_non_planar_face_ids"))
adjacent_surfaces = tuple(action_info.get("first_level_adjacent_surface_types") or ())
non_plane_adjacent_surfaces = tuple(
item
for item in adjacent_surfaces
if isinstance(item, (tuple, list))
and len(item) >= 2
and str(item[1]) != "plane"
)
if non_planar_face_ids or non_plane_adjacent_surfaces:
keep_relation_enabled = False
keep_relation_disabled_tip = "一级相邻 Face 中包含非平面/曲面,当前“保持关系”只支持平面邻域。"
elif angled_count > 0:
keep_relation_enabled = False
keep_relation_disabled_tip = "一级相邻平面中存在斜交关系,当前“保持关系”只支持平行/垂直。"
elif relation_status != "ready":
keep_relation_enabled = False
keep_relation_disabled_tip = "当前一级平面关系还不可验证,不能使用“保持关系”。"
elif relation_count <= 0:
keep_relation_enabled = False
keep_relation_disabled_tip = "当前一级邻域没有识别到可保持的平行/垂直平面关系。"
add_scoped_spec( add_scoped_spec(
key="face_target_normal_position", key="face_target_normal_position",
label="偏移", label="偏移",
@@ -3598,12 +3660,12 @@ class WindowStateMixin:
"label": "保持关系", "label": "保持关系",
"action": "push_pull_face_keep_relations", "action": "push_pull_face_keep_relations",
"target_attr": "offset_input", "target_attr": "offset_input",
"enabled": current_plane_position is not None, "enabled": keep_relation_enabled,
"enabled_tip": ( "enabled_tip": (
"输入偏移的目标位置;程序会沿当前 Face 法向拉伸/切除," "输入偏移的目标位置;程序会沿当前 Face 法向拉伸/切除,"
"并要求一级相邻平面的平行/垂直关系可验证。" "并要求一级相邻平面的平行/垂直关系可验证。"
), ),
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能保持一级平面关系。", "disabled_tip": keep_relation_disabled_tip,
"range_hint": ( "range_hint": (
"这是带约束守门的拉伸/切除:只支持直接相邻平面关系清楚、且关系为平行/垂直的一级邻域;" "这是带约束守门的拉伸/切除:只支持直接相邻平面关系清楚、且关系为平行/垂直的一级邻域;"
"遇到非平面相邻面、斜交关系或复杂多边界慢计划会直接阻止。" "遇到非平面相邻面、斜交关系或复杂多边界慢计划会直接阻止。"