129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
|
|
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())
|