166 lines
7.8 KiB
Python
166 lines
7.8 KiB
Python
|
|
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())
|