Files
pythonocc-step-editor/scripts/verify_icepak_face45_push_pull.py
T

166 lines
7.8 KiB
Python
Raw Normal View History

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())