from __future__ import annotations import json from pathlib import Path import sys import tempfile from OCC.Core.BRepAdaptor import BRepAdaptor_Surface from OCC.Core.GeomAbs import GeomAbs_Plane 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.window_actions import WindowActionMixin from step_editor.isolated_edit_worker import run_request from step_editor.model import StepModel MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "ICEPAK-NATURAL.stp" def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _dot(left: tuple[float, float, float], right: tuple[float, float, float]) -> float: return sum(left[index] * right[index] for index in range(3)) def _plane_axis(model: StepModel, face_id: int) -> tuple[float, float, float]: surf = BRepAdaptor_Surface(model.faces[face_id]) _assert(surf.GetType() == GeomAbs_Plane, f"Face {face_id} should be planar") direction = surf.Plane().Axis().Direction() return (float(direction.X()), float(direction.Y()), float(direction.Z())) def _isolated_push_pull(model: StepModel, face_id: int, distance: float, temp_dir: Path, stem: str) -> tuple[StepModel, str]: input_path = temp_dir / f"{stem}_input.brep" output_path = temp_dir / f"{stem}_output.brep" request_path = temp_dir / f"{stem}_request.json" model.export_internal_brep(input_path) request_path.write_text( json.dumps( { "input_path": str(input_path), "output_path": str(output_path), "input_format": "brep", "output_format": "brep", "operation": "push_pull_face", "args": [face_id, distance], }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) code = run_request(request_path) response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8")) _assert(code == 0 and response.get("ok") is True, f"isolated Face {face_id} push-pull should pass: {response}") _assert(output_path.exists(), f"isolated Face {face_id} push-pull should write an output BREP") return StepModel.load_internal_brep(output_path), str(response.get("message") or "") def _ui_push_pull_context(model: StepModel, face_id: int, plan: dict[str, object]) -> dict[str, object]: return { "operation_name": "拉伸/切除平面", "target": f"Face {face_id}", "target_kind": "face", "target_id": face_id, "target_logical_id": model.face_logical_id(face_id), "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "semantic_distance": plan.get("distance"), "distance_rule": "positive=outward fuse, negative=inward cut", "surface": plan.get("surface"), "outward_direction": plan.get("outward_direction"), "plane_direction": plan.get("plane_direction"), "current_plane_position": plan.get("current_plane_position"), "target_plane_position": plan.get("target_plane_position"), "current_plane_center": plan.get("current_plane_center"), "target_plane_center": plan.get("target_plane_center"), "direction_confidence": plan.get("direction_confidence"), "direction_note": plan.get("direction_note"), "resize_strategy": plan.get("resize_strategy"), "edit_strategy_label": plan.get("edit_strategy_label"), "edit_semantics": plan.get("edit_semantics"), "push_pull_risk": plan.get("risk"), "push_pull_status": plan.get("status"), "push_pull_message": plan.get("message"), "push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"), "push_pull_scope_face_count": plan.get("push_pull_scope_face_count"), "push_pull_scope_note": plan.get("push_pull_scope_note"), "bbox_diagonal": plan.get("bbox_diagonal"), "selected_boundary_wires": plan.get("selected_boundary_wires"), "selected_inner_boundary_wires": plan.get("selected_inner_boundary_wires"), "selected_has_inner_boundaries": plan.get("selected_has_inner_boundaries"), }, } def main() -> int: if not MODEL_PATH.exists(): print("icepak face45 push-pull skipped: local ICEPAK-NATURAL.stp is not present") return 0 model = StepModel.load(MODEL_PATH) face_id = 45 plan = model.push_pull_plan(face_id, 10.0) _assert(plan.get("status") != "blocked", f"Face 45 positive push-pull should be plannable: {plan}") _assert(float(plan.get("current_plane_position") or 0.0) == 5.0, f"Face 45 should use outward offset +5: {plan}") _assert(float(plan.get("target_plane_position") or 0.0) == 15.0, f"Face 45 target should be +15: {plan}") _assert(plan.get("target_plane_center") is not None, "Face push-pull plan should carry target_plane_center") inward_plan = model.push_pull_plan(face_id, -10.0) _assert(inward_plan.get("status") == "blocked", f"Face 45 inward cut through material should be blocked: {inward_plan}") _assert("材料厚度" in str(inward_plan.get("blockers") or ""), f"blocked reason should mention material depth: {inward_plan}") direct_model = StepModel.load(MODEL_PATH) direct_model.push_pull_face(face_id, 10.0) direct_followup_plan = direct_model.push_pull_plan(47, 10.0) direct_direction = tuple(float(item) for item in (direct_followup_plan.get("plane_direction") or ())) _assert( len(direct_direction) == 3 and abs(_dot(direct_direction, _plane_axis(direct_model, 47))) >= 0.92, f"Face 47 follow-up direction should stay aligned with its selected plane: {direct_followup_plan}", ) direct_followup_message = direct_model.push_pull_face(47, 10.0, plan=dict(direct_followup_plan)) direct_followup_target = float(direct_followup_plan.get("target_plane_position") or 0.0) _assert( f"target={direct_followup_target:g}" in direct_followup_message, f"direct second push-pull should match Face 47 target: {direct_followup_message}", ) with tempfile.TemporaryDirectory(prefix="icepak_face45_isolated_") as temp_dir: root = Path(temp_dir) edited_model, message = _isolated_push_pull(model, face_id, 10.0, root, "face45") _assert("actual=15" in message and "target=15" in message, f"result check should match target face: {message}") followup_face_id = 47 followup_plan = edited_model.push_pull_plan(followup_face_id, 10.0) followup_context = _ui_push_pull_context(edited_model, followup_face_id, followup_plan) followup_current = float(followup_plan.get("current_plane_position") or 0.0) followup_target = float(followup_plan.get("target_plane_position") or 0.0) _assert(followup_plan.get("status") != "blocked", f"Face 47 follow-up should be plannable: {followup_plan}") _assert( abs(followup_target - followup_current - 10.0) <= 1e-8, f"Face 47 follow-up target should preserve the requested +10 offset: {followup_plan}", ) _second_model, followup_message = _isolated_push_pull(edited_model, followup_face_id, 10.0, root, "face47") _assert( f"actual={followup_target:g}" in followup_message and f"target={followup_target:g}" in followup_message, f"second isolated push-pull should match Face 47 target: {followup_message}", ) checker = WindowActionMixin() blocker = checker._face_target_integrity_blocker(_second_model, followup_context) _assert(blocker == "", f"UI target integrity check should accept the second push-pull: {blocker}") print("icepak face45/face47 push-pull ok") return 0 if __name__ == "__main__": raise SystemExit(main())