2026-07-31 16:36:05 +08:00
|
|
|
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}")
|
2026-08-04 18:15:29 +08:00
|
|
|
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}"
|
|
|
|
|
)
|
2026-07-31 16:36:05 +08:00
|
|
|
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())
|