from __future__ import annotations import argparse from pathlib import Path import sys import tempfile from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox from OCC.Core.gp import gp_Pnt PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from step_editor.model import StepModel from step_editor.step_io import _write_step def _write_split_top_box(path: Path) -> None: left = BRepPrimAPI_MakeBox(5.0, 10.0, 10.0).Shape() right = BRepPrimAPI_MakeBox(gp_Pnt(5.0, 0.0, 0.0), 5.0, 10.0, 10.0).Shape() fuse = BRepAlgoAPI_Fuse(left, right) fuse.Build() if not fuse.IsDone(): raise SystemExit("failed to build split-top box") _write_step(fuse.Shape(), path) def _top_faces(model: StepModel, z_value: float, tolerance: float) -> list[int]: face_ids: list[int] = [] for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") != "plane": continue center = info.get("area_center") if not isinstance(center, tuple) or len(center) != 3: continue if abs(float(center[2]) - z_value) <= tolerance: face_ids.append(face_id) return face_ids def _has_top_face(model: StepModel, z_value: float, area: float, tolerance: float) -> bool: for face_id in _top_faces(model, z_value, tolerance): info = model.face_info(face_id) if abs(float(info.get("area") or 0.0) - area) <= tolerance: return True return False def _assert_logical_top_region( model: StepModel, logical_id: int, z_value: float, area: float, tolerance: float, ) -> int: matches = model.face_ids_for_logical_id(logical_id) if not matches: raise SystemExit(f"logical Face {logical_id} was not retained after coplanar push/pull") resolved = model.resolve_face_selection_id(logical_id) if resolved is None or resolved not in matches: raise SystemExit(f"logical Face {logical_id} did not resolve into retained matches {matches}") info = model.face_info(resolved) center = info.get("area_center") if not isinstance(center, tuple) or len(center) != 3: raise SystemExit(f"retained logical Face {logical_id} lacks a stable center: {info}") if abs(float(center[2]) - z_value) > tolerance: raise SystemExit(f"retained logical Face {logical_id} should be at z={z_value:g}, got {center}") if abs(float(info.get("area") or 0.0) - area) > tolerance: raise SystemExit(f"retained logical Face {logical_id} should have area {area:g}, got {info.get('area')}") return resolved def main() -> int: parser = argparse.ArgumentParser(description="Verify coplanar split Face push/pull as one plane region.") parser.add_argument("--distance", type=float, default=1.0) parser.add_argument("--tolerance", type=float, default=2e-4) args = parser.parse_args() with tempfile.TemporaryDirectory(prefix="geom_param_face_coplanar_") as temp_dir: model_path = Path(temp_dir) / "split_top_box.step" _write_split_top_box(model_path) model = StepModel.load(model_path) before = model.stats() top_faces = _top_faces(model, 10.0, args.tolerance) if len(top_faces) != 2: raise SystemExit(f"expected two split top faces before push/pull, got {top_faces}") face_id = top_faces[0] logical_id = model.face_region_logical_id(face_id) plan = model.push_pull_plan(face_id, args.distance) scope_ids = tuple(plan.get("push_pull_scope_face_ids", ())) if set(scope_ids) != set(top_faces): raise SystemExit(f"expected push/pull scope {top_faces}, got {scope_ids}") if plan["status"] == "blocked": raise SystemExit(f"coplanar push/pull plan was blocked: {plan['message']}") result = model.push_pull_face(face_id, args.distance) after = model.stats() if after.solids != before.solids: raise SystemExit(f"solid count changed: before={before.solids}, after={after.solids}") if not _has_top_face(model, 10.0 + args.distance, 100.0, args.tolerance): raise SystemExit("pushed coplanar top region was not rebuilt as a 100 mm^2 top plane") logical_face_id = _assert_logical_top_region( model, logical_id, 10.0 + args.distance, 100.0, args.tolerance, ) print(f"model={model_path}") print(f"source_face={face_id}") print(f"source_logical_face={logical_id}") print(f"retained_logical_face={logical_face_id}") print(f"scope_faces={scope_ids}") print(f"before={before}") print(f"after={after}") print(f"distance={args.distance:.6f}") print(f"strategy={plan.get('resize_strategy', 'push-pull-planar-face')}") print(result.encode("ascii", "backslashreplace").decode("ascii")) return 0 if __name__ == "__main__": raise SystemExit(main())