from __future__ import annotations from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from step_editor.model import StepModel from step_editor.window_state import WindowStateMixin, _feature_dimension_keys 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" FACE_ID = 594 BASE_FACE_KEYS = ( "local_face_width", "local_face_height", "face_center_position", "face_target_normal_position", ) RESULT_ONLY_KEYS = ("area",) class _Probe(WindowStateMixin): pass 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 probe.operation_in_progress = False probe.scan_in_progress = False probe.load_in_progress = False probe.selected_face_id = face_id probe.selected_edge_id = None probe.selected_kind = "feature" probe.selected_part_id = int(info["part_id"]) probe.selected_solid_id = int(info["solid_id"]) probe.feature_detection_level = "current-only" probe.manual_bottom_face_id = None probe.manual_slot_pair_face_id = None 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") def _assert_contains(keys: tuple[str, ...], expected: tuple[str, ...], label: str) -> None: missing = [key for key in expected if key not in keys] if missing: raise AssertionError(f"{label} missing {missing}, got {keys}") def _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: 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): raise AssertionError(f"test model has no Face {FACE_ID}") quick_info = model.quick_face_info(FACE_ID) if quick_info.get("surface") != "plane": raise AssertionError(f"Face {FACE_ID} should be planar in the baseline model: {quick_info}") before_cached_rows = _feature_rows(model, FACE_ID) _assert_contains(before_cached_rows, BASE_FACE_KEYS, "Face 594 before full feature cache") _assert_absent(before_cached_rows, RESULT_ONLY_KEYS, "Face 594 before full feature cache") full_info = model.feature_info(FACE_ID) if full_info.get("shell_region_status") == "candidate": dimension_keys = _feature_dimension_keys(full_info) _assert_contains(dimension_keys, BASE_FACE_KEYS, "shell candidate feature dimensions") _assert_absent(dimension_keys, RESULT_ONLY_KEYS, "shell candidate feature dimensions") if "shell_thickness_estimate" not in dimension_keys: raise AssertionError(f"shell candidate should keep shell thickness as an extra dimension: {dimension_keys}") after_cached_rows = _feature_rows(model, FACE_ID) _assert_contains(after_cached_rows, BASE_FACE_KEYS, "Face 594 after full feature cache") _assert_absent(after_cached_rows, RESULT_ONLY_KEYS, "Face 594 after full feature cache") if before_cached_rows != after_cached_rows: raise AssertionError( "Face 594 current-only feature rows changed after full recognition cache: " f"before={before_cached_rows}, after={after_cached_rows}" ) _assert_face_594_stays_stable_after_remote_cap_edit() print("Face feature parameter consistency ok") return 0 if __name__ == "__main__": raise SystemExit(main())