from __future__ import annotations import sys from pathlib import Path import tempfile from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCone, BRepPrimAPI_MakeSphere from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Pnt PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from step_editor.model_types import TopologyStats from step_editor.geometry_utils import _finalize_boolean_result from step_editor.model import StepModel from step_editor.step_io import _write_step from step_editor.window_actions import WindowActionMixin class _Probe(WindowActionMixin): pass def _face_context(strategy: str = "local-face-area-only-deform") -> dict[str, object]: return { "operation_name": "Face integrity guard probe", "target_kind": "face", "target_id": 0, "parameters": {"resize_strategy": strategy}, } def _edge_context() -> dict[str, object]: return { "operation_name": "Edge integrity guard probe", "target_kind": "edge", "target_id": 0, "parameters": {"resize_strategy": "local-edge-length-deform"}, } def _expect_blocked(label: str, callback) -> None: try: callback() except RuntimeError as exc: print(f"{label}: blocked as expected: {exc}") return raise SystemExit(f"{label}: expected a RuntimeError") def _expect_allowed(label: str, callback) -> list[str]: try: warnings = callback() except RuntimeError as exc: raise SystemExit(f"{label}: should have been allowed, got {exc}") from exc print(f"{label}: allowed, warnings={len(warnings)}") return warnings def _write_embedded_countersink(path: Path) -> None: block = BRepPrimAPI_MakeBox(30.0, 30.0, 10.0).Shape() cutter = BRepPrimAPI_MakeCone( gp_Ax2(gp_Pnt(15.0, 15.0, 5.0), gp_Dir(0.0, 0.0, 1.0)), 2.0, 5.0, 5.0, ).Shape() cut = BRepAlgoAPI_Cut(block, cutter) _write_step(_finalize_boolean_result(cut, "verify Face cone integrity countersink cut"), path) def _first_face_by_surface(model: StepModel, surface: str) -> int: for face_id in range(len(model.faces)): if model.face_info(face_id).get("surface") == surface: return face_id raise SystemExit(f"no {surface} Face was recognized") def main() -> int: probe = _Probe() before = TopologyStats(parts=1, solids=1, faces=6, edges=12, vertices=8) after_ok = TopologyStats(parts=1, solids=1, faces=6, edges=12, vertices=8) after_no_solid = TopologyStats(parts=1, solids=0, faces=0, edges=0, vertices=0) after_two_solids = TopologyStats(parts=1, solids=2, faces=8, edges=16, vertices=12) before_quality_ok = {"quality_status": "ok", "brep_valid": True, "quality_warnings": ""} after_quality_ok = {"quality_status": "ok", "brep_valid": True, "quality_warnings": ""} after_quality_bad = { "quality_status": "warning", "brep_valid": False, "quality_warnings": "B-Rep invalid after edit", } before_quality_bad = { "quality_status": "warning", "brep_valid": False, "quality_warnings": "B-Rep invalid before edit", } _expect_blocked( "Face edit losing solids", lambda: probe._verified_edit_quality_warnings( _face_context(), before, after_no_solid, before, after_no_solid, before_quality_ok, after_quality_ok, ), ) _expect_blocked( "Face edit changing target solid count", lambda: probe._verified_edit_quality_warnings( _face_context(), before, after_two_solids, before, after_two_solids, before_quality_ok, after_quality_ok, ), ) _expect_blocked( "New invalid B-Rep", lambda: probe._verified_edit_quality_warnings( _face_context(), before, after_ok, before, after_ok, before_quality_ok, after_quality_bad, ), ) warnings = _expect_allowed( "Already-warning B-Rep stays warning", lambda: probe._verified_edit_quality_warnings( _face_context(), before, after_ok, before, after_ok, before_quality_bad, after_quality_bad, ), ) if not warnings: raise SystemExit("existing quality warning should still be reported") cube = StepModel.load(PROJECT_ROOT / "assets" / "models" / "cube_10mm.step") cube_stats = cube.stats() cube_part_id = int(cube.face_part_ids[0]) cube_solid_id = int(cube.face_solid_ids[0]) cube_context = { "operation_name": "Face target area integrity probe", "target_kind": "face", "target_id": 0, "target_logical_id": cube.face_region_logical_id(0), "parameters": { "resize_strategy": "local-face-area-only-deform", "part_id": cube_part_id, "solid_id": cube_solid_id, "surface": "plane", "target_area": 144.0, }, } _expect_blocked( "Face target area mismatch", lambda: probe._verified_edit_quality_warnings( cube_context, cube_stats, cube_stats, cube.part_topology_stats(cube_part_id), cube.part_topology_stats(cube_part_id), before_quality_ok, after_quality_ok, after_model=cube, ), ) edited_cube = StepModel.load(PROJECT_ROOT / "assets" / "models" / "cube_10mm.step") edited_before_stats = edited_cube.stats() edited_before_part_stats = edited_cube.part_topology_stats(cube_part_id) edited_cube.resize_face_area_local(0, 144.0) _expect_allowed( "Face target area reached", lambda: probe._verified_edit_quality_warnings( cube_context, edited_before_stats, edited_cube.stats(), edited_before_part_stats, edited_cube.part_topology_stats(cube_part_id), before_quality_ok, after_quality_ok, after_model=edited_cube, ), ) with tempfile.TemporaryDirectory(prefix="geom_param_face_guard_sphere_") as temp_dir: sphere_path = Path(temp_dir) / "sphere.step" _write_step(BRepPrimAPI_MakeSphere(5.0).Shape(), sphere_path) sphere = StepModel.load(sphere_path) sphere_face_id = next( face_id for face_id in range(len(sphere.faces)) if sphere.face_info(face_id).get("surface") == "sphere" ) sphere_part_id = int(sphere.face_part_ids[sphere_face_id]) sphere_solid_id = int(sphere.face_solid_ids[sphere_face_id]) sphere_context = { "operation_name": "Face target sphere radius integrity probe", "target_kind": "face", "target_id": sphere_face_id, "target_logical_id": sphere.face_region_logical_id(sphere_face_id), "parameters": { "resize_strategy": "sphere-radius-owning-scale", "part_id": sphere_part_id, "solid_id": sphere_solid_id, "surface": "sphere", "target_radius": 6.25, }, } _expect_blocked( "Face target sphere radius mismatch", lambda: probe._verified_edit_quality_warnings( sphere_context, sphere.stats(), sphere.stats(), sphere.part_topology_stats(sphere_part_id), sphere.part_topology_stats(sphere_part_id), before_quality_ok, after_quality_ok, after_model=sphere, ), ) edited_sphere = StepModel.load(sphere_path) edited_sphere.resize_spherical_radius(sphere_face_id, 6.25) _expect_allowed( "Face target sphere radius reached", lambda: probe._verified_edit_quality_warnings( sphere_context, sphere.stats(), edited_sphere.stats(), sphere.part_topology_stats(sphere_part_id), edited_sphere.part_topology_stats(sphere_part_id), before_quality_ok, after_quality_ok, after_model=edited_sphere, ), ) with tempfile.TemporaryDirectory(prefix="geom_param_face_guard_cone_") as temp_dir: cone_path = Path(temp_dir) / "embedded_countersink.step" _write_embedded_countersink(cone_path) cone = StepModel.load(cone_path) cone_face_id = _first_face_by_surface(cone, "cone") cone_part_id = int(cone.face_part_ids[cone_face_id]) cone_solid_id = int(cone.face_solid_ids[cone_face_id]) stale_plane_id = 0 if cone.face_info(stale_plane_id).get("surface") == "cone": raise SystemExit("cone integrity test expected Face 0 to be a stale non-cone candidate") cone_context = { "operation_name": "Face target cone boundary integrity probe", "target_kind": "face", "target_id": stale_plane_id, "target_logical_id": stale_plane_id, "parameters": { "resize_strategy": "bounded-cone-recut-preserve-angle-reference-radius", "part_id": cone_part_id, "solid_id": cone_solid_id, "surface": "cone", "target_reference_radius": 3.0, "embedded_cone_target_small_radius": 3.0, "embedded_cone_target_large_radius": 6.0, }, } _expect_blocked( "Face target cone boundary mismatch", lambda: probe._verified_edit_quality_warnings( cone_context, cone.stats(), cone.stats(), cone.part_topology_stats(cone_part_id), cone.part_topology_stats(cone_part_id), before_quality_ok, after_quality_ok, after_model=cone, ), ) edited_cone = StepModel.load(cone_path) edited_before_stats = edited_cone.stats() edited_before_part_stats = edited_cone.part_topology_stats(cone_part_id) edited_cone.resize_conical_reference_radius(cone_face_id, 3.0) _expect_allowed( "Face target cone boundary reached despite stale id", lambda: probe._verified_edit_quality_warnings( cone_context, edited_before_stats, edited_cone.stats(), edited_before_part_stats, edited_cone.part_topology_stats(cone_part_id), before_quality_ok, after_quality_ok, after_model=edited_cone, ), ) _expect_allowed( "Non-Face edit solid-count change only warns", lambda: probe._verified_edit_quality_warnings( _edge_context(), before, after_two_solids, before, after_two_solids, before_quality_ok, after_quality_ok, ), ) if probe._isolation_for_plan({"risk": "medium"}, "resize_face_area_local", [0, 125.0]) is None: raise SystemExit("medium-risk Face edits should use isolated execution") if probe._isolation_for_plan({"risk": "medium"}, "resize_edge_length", [0, 12.0]) is not None: raise SystemExit("unsupported medium-risk Edge edits should not use Face isolation") if probe._isolation_for_plan({"risk": "low"}, "resize_face_area_local", [0, 125.0]) is None: raise SystemExit("low-risk Face parameter edits should still use isolated execution") if probe._isolation_for_plan({"risk": "high"}, "resize_edge_length", [0, 12.0]) is not None: raise SystemExit("unsupported high-risk Edge edits should not enter the Face isolation worker") print("Face edit integrity guards ok") return 0 if __name__ == "__main__": raise SystemExit(main())