Files
pythonocc-step-editor/scripts/verify_face_complex_boundary_guard.py

158 lines
7.3 KiB
Python

from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
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.geometry_utils import _finalize_boolean_result
from step_editor.model import StepModel
from step_editor.step_io import _write_step
def _write_holed_plate(path: Path) -> None:
plate = BRepPrimAPI_MakeBox(30.0, 20.0, 8.0).Shape()
axis = gp_Ax2(gp_Pnt(15.0, 10.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
cutter = BRepPrimAPI_MakeCylinder(axis, 3.0, 10.0).Shape()
cut = BRepAlgoAPI_Cut(plate, cutter)
shape = _finalize_boolean_result(cut, "verify holed planar face guard cut")
_write_step(shape, path)
def _first_holed_plane_face(model: StepModel) -> int:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
if bool(info.get("has_inner_boundaries")):
return face_id
raise SystemExit("no planar Face with an inner boundary was found")
def _assert_blocked(plan: dict[str, object], label: str) -> None:
if plan.get("status") != "blocked":
raise SystemExit(f"{label} should be blocked for a holed planar Face: {plan}")
def _assert_push_pull_keeps_single_solid(path: Path, distance: float) -> str:
model = StepModel.load(path)
face_id = _first_holed_plane_face(model)
plan = model.push_pull_plan(face_id, distance)
if plan.get("status") == "blocked":
raise SystemExit(f"holed planar Face push/pull should remain available: {plan}")
result = model.push_pull_face(face_id, distance)
stats = model.stats()
if stats.solids != 1:
raise SystemExit(f"holed planar Face push/pull should keep one solid: {stats}")
if "inner_wires=" not in result:
raise SystemExit(f"holed planar Face push/pull should report inner-wire verification: {result}")
target_position = plan.get("target_plane_position")
direction = plan.get("plane_direction") or plan.get("outward_direction")
if not isinstance(target_position, (int, float)) or not isinstance(direction, tuple) or len(direction) != 3:
raise SystemExit(f"holed planar Face push/pull plan should expose a target plane position: {plan}")
best: tuple[float, int, dict[str, object]] | None = None
for candidate_id in range(len(model.faces)):
info = model.face_info(candidate_id)
if info.get("surface") != "plane":
continue
origin = info.get("plane_origin")
if not isinstance(origin, tuple) or len(origin) != 3:
continue
actual_position = (
float(origin[0]) * float(direction[0])
+ float(origin[1]) * float(direction[1])
+ float(origin[2]) * float(direction[2])
)
error = abs(actual_position - float(target_position))
if best is None or error < best[0]:
best = (error, candidate_id, info)
if best is None:
raise SystemExit("holed planar Face push/pull should leave a measurable target plane")
_error, matched_face_id, matched_info = best
if int(matched_info.get("inner_boundary_wires") or 0) < 1:
raise SystemExit(
"holed planar Face push/pull should preserve the target Face inner boundary: "
f"matched_face={matched_face_id}, info={matched_info}, result={result}"
)
return result
def main() -> int:
with tempfile.TemporaryDirectory(prefix="geom_param_face_boundary_") as temp_dir:
path = Path(temp_dir) / "holed_plate.step"
_write_holed_plate(path)
model = StepModel.load(path)
face_id = _first_holed_plane_face(model)
info = model.face_info(face_id)
if int(info.get("boundary_wires") or 0) < 2:
raise SystemExit(f"Face {face_id} should report at least two boundary wires: {info}")
if bool(info.get("local_face_deform_ready", True)):
raise SystemExit(f"Face {face_id} should not allow local Face deformation: {info}")
solid_raw = info.get("solid_id")
solid_id = int(solid_raw) if solid_raw is not None else -1
if solid_id < 0 or solid_id not in model._local_face_deform_readiness_cache:
raise SystemExit("local Face deformation readiness should be cached after face_info()")
other_face_id = next(
(item for item, item_solid_id in enumerate(model.face_solid_ids) if item_solid_id == solid_id and item != face_id),
None,
)
if other_face_id is None:
raise SystemExit("holed plate should expose another Face on the same Solid")
original_vertex_reader = model._local_deform_face_vertex_points
def fail_on_cache_miss(*_args: object, **_kwargs: object) -> list[tuple[float, float, float]]:
raise AssertionError("local Face readiness cache was not reused")
model._local_deform_face_vertex_points = fail_on_cache_miss # type: ignore[method-assign]
try:
cached_readiness = model._local_face_deform_readiness(other_face_id)
finally:
model._local_deform_face_vertex_points = original_vertex_reader # type: ignore[method-assign]
if bool(cached_readiness.get("local_face_deform_ready", True)):
raise SystemExit(f"cached readiness should keep local deformation disabled: {cached_readiness}")
center = info.get("area_center") or info.get("bbox_center")
if not isinstance(center, tuple) or len(center) != 3:
raise SystemExit(f"Face {face_id} does not expose a stable center")
target_center = (float(center[0]), float(center[1]), float(center[2]) + 1.0)
area = float(info.get("area") or 0.0)
if area <= 0:
raise SystemExit(f"Face {face_id} does not expose a stable area")
width = float(info.get("local_face_width") or 0.0)
if width <= 0:
raise SystemExit(f"Face {face_id} does not expose a measured Face width")
height = float(info.get("local_face_height") or 0.0)
if height <= 0:
raise SystemExit(f"Face {face_id} does not expose a measured Face height")
_assert_blocked(model.face_center_local_move_plan(face_id, target_center), "Face center local move")
_assert_blocked(model.face_area_local_resize_plan(face_id, area * 1.1), "Face area local resize")
_assert_blocked(model.face_size_local_resize_plan(face_id, width * 1.1, axis="width"), "Face width local resize")
_assert_blocked(model.face_size_local_resize_plan(face_id, height * 1.1, axis="height"), "Face height local resize")
_assert_blocked(model.face_plane_offset_local_plan(face_id, 1.0), "Face plane offset local move")
outward_result = _assert_push_pull_keeps_single_solid(path, 1.0)
inward_result = _assert_push_pull_keeps_single_solid(path, -1.0)
print(
"holed planar Face local edit guard ok: "
f"face_id={face_id}, boundary_wires={info.get('boundary_wires')}, "
f"blocker={info.get('local_face_deform_blocker')}, "
f"push_pull_outward={outward_result}, push_pull_inward={inward_result}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())