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

329 lines
12 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
import tempfile
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCone, BRepPrimAPI_MakeSphere
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.model_types import TopologyStats
from step_editor.geometry_utils import _finalize_boolean_result
from step_editor.model import StepModel
from step_editor.step_io import _write_step
from step_editor.window_actions import WindowActionMixin
class _Probe(WindowActionMixin):
pass
def _face_context(strategy: str = "local-face-area-only-deform") -> dict[str, object]:
return {
"operation_name": "Face integrity guard probe",
"target_kind": "face",
"target_id": 0,
"parameters": {"resize_strategy": strategy},
}
def _edge_context() -> dict[str, object]:
return {
"operation_name": "Edge integrity guard probe",
"target_kind": "edge",
"target_id": 0,
"parameters": {"resize_strategy": "local-edge-length-deform"},
}
def _expect_blocked(label: str, callback) -> None:
try:
callback()
except RuntimeError as exc:
print(f"{label}: blocked as expected: {exc}")
return
raise SystemExit(f"{label}: expected a RuntimeError")
def _expect_allowed(label: str, callback) -> list[str]:
try:
warnings = callback()
except RuntimeError as exc:
raise SystemExit(f"{label}: should have been allowed, got {exc}") from exc
print(f"{label}: allowed, warnings={len(warnings)}")
return warnings
def _write_embedded_countersink(path: Path) -> None:
block = BRepPrimAPI_MakeBox(30.0, 30.0, 10.0).Shape()
cutter = BRepPrimAPI_MakeCone(
gp_Ax2(gp_Pnt(15.0, 15.0, 5.0), gp_Dir(0.0, 0.0, 1.0)),
2.0,
5.0,
5.0,
).Shape()
cut = BRepAlgoAPI_Cut(block, cutter)
_write_step(_finalize_boolean_result(cut, "verify Face cone integrity countersink cut"), path)
def _first_face_by_surface(model: StepModel, surface: str) -> int:
for face_id in range(len(model.faces)):
if model.face_info(face_id).get("surface") == surface:
return face_id
raise SystemExit(f"no {surface} Face was recognized")
def main() -> int:
probe = _Probe()
before = TopologyStats(parts=1, solids=1, faces=6, edges=12, vertices=8)
after_ok = TopologyStats(parts=1, solids=1, faces=6, edges=12, vertices=8)
after_no_solid = TopologyStats(parts=1, solids=0, faces=0, edges=0, vertices=0)
after_two_solids = TopologyStats(parts=1, solids=2, faces=8, edges=16, vertices=12)
before_quality_ok = {"quality_status": "ok", "brep_valid": True, "quality_warnings": ""}
after_quality_ok = {"quality_status": "ok", "brep_valid": True, "quality_warnings": ""}
after_quality_bad = {
"quality_status": "warning",
"brep_valid": False,
"quality_warnings": "B-Rep invalid after edit",
}
before_quality_bad = {
"quality_status": "warning",
"brep_valid": False,
"quality_warnings": "B-Rep invalid before edit",
}
_expect_blocked(
"Face edit losing solids",
lambda: probe._verified_edit_quality_warnings(
_face_context(),
before,
after_no_solid,
before,
after_no_solid,
before_quality_ok,
after_quality_ok,
),
)
_expect_blocked(
"Face edit changing target solid count",
lambda: probe._verified_edit_quality_warnings(
_face_context(),
before,
after_two_solids,
before,
after_two_solids,
before_quality_ok,
after_quality_ok,
),
)
_expect_blocked(
"New invalid B-Rep",
lambda: probe._verified_edit_quality_warnings(
_face_context(),
before,
after_ok,
before,
after_ok,
before_quality_ok,
after_quality_bad,
),
)
warnings = _expect_allowed(
"Already-warning B-Rep stays warning",
lambda: probe._verified_edit_quality_warnings(
_face_context(),
before,
after_ok,
before,
after_ok,
before_quality_bad,
after_quality_bad,
),
)
if not warnings:
raise SystemExit("existing quality warning should still be reported")
cube = StepModel.load(PROJECT_ROOT / "assets" / "models" / "cube_10mm.step")
cube_stats = cube.stats()
cube_part_id = int(cube.face_part_ids[0])
cube_solid_id = int(cube.face_solid_ids[0])
cube_context = {
"operation_name": "Face target area integrity probe",
"target_kind": "face",
"target_id": 0,
"target_logical_id": cube.face_region_logical_id(0),
"parameters": {
"resize_strategy": "local-face-area-only-deform",
"part_id": cube_part_id,
"solid_id": cube_solid_id,
"surface": "plane",
"target_area": 144.0,
},
}
_expect_blocked(
"Face target area mismatch",
lambda: probe._verified_edit_quality_warnings(
cube_context,
cube_stats,
cube_stats,
cube.part_topology_stats(cube_part_id),
cube.part_topology_stats(cube_part_id),
before_quality_ok,
after_quality_ok,
after_model=cube,
),
)
edited_cube = StepModel.load(PROJECT_ROOT / "assets" / "models" / "cube_10mm.step")
edited_before_stats = edited_cube.stats()
edited_before_part_stats = edited_cube.part_topology_stats(cube_part_id)
edited_cube.resize_face_area_local(0, 144.0)
_expect_allowed(
"Face target area reached",
lambda: probe._verified_edit_quality_warnings(
cube_context,
edited_before_stats,
edited_cube.stats(),
edited_before_part_stats,
edited_cube.part_topology_stats(cube_part_id),
before_quality_ok,
after_quality_ok,
after_model=edited_cube,
),
)
with tempfile.TemporaryDirectory(prefix="geom_param_face_guard_sphere_") as temp_dir:
sphere_path = Path(temp_dir) / "sphere.step"
_write_step(BRepPrimAPI_MakeSphere(5.0).Shape(), sphere_path)
sphere = StepModel.load(sphere_path)
sphere_face_id = next(
face_id for face_id in range(len(sphere.faces)) if sphere.face_info(face_id).get("surface") == "sphere"
)
sphere_part_id = int(sphere.face_part_ids[sphere_face_id])
sphere_solid_id = int(sphere.face_solid_ids[sphere_face_id])
sphere_context = {
"operation_name": "Face target sphere radius integrity probe",
"target_kind": "face",
"target_id": sphere_face_id,
"target_logical_id": sphere.face_region_logical_id(sphere_face_id),
"parameters": {
"resize_strategy": "sphere-radius-owning-scale",
"part_id": sphere_part_id,
"solid_id": sphere_solid_id,
"surface": "sphere",
"target_radius": 6.25,
},
}
_expect_blocked(
"Face target sphere radius mismatch",
lambda: probe._verified_edit_quality_warnings(
sphere_context,
sphere.stats(),
sphere.stats(),
sphere.part_topology_stats(sphere_part_id),
sphere.part_topology_stats(sphere_part_id),
before_quality_ok,
after_quality_ok,
after_model=sphere,
),
)
edited_sphere = StepModel.load(sphere_path)
edited_sphere.resize_spherical_radius(sphere_face_id, 6.25)
_expect_allowed(
"Face target sphere radius reached",
lambda: probe._verified_edit_quality_warnings(
sphere_context,
sphere.stats(),
edited_sphere.stats(),
sphere.part_topology_stats(sphere_part_id),
edited_sphere.part_topology_stats(sphere_part_id),
before_quality_ok,
after_quality_ok,
after_model=edited_sphere,
),
)
with tempfile.TemporaryDirectory(prefix="geom_param_face_guard_cone_") as temp_dir:
cone_path = Path(temp_dir) / "embedded_countersink.step"
_write_embedded_countersink(cone_path)
cone = StepModel.load(cone_path)
cone_face_id = _first_face_by_surface(cone, "cone")
cone_part_id = int(cone.face_part_ids[cone_face_id])
cone_solid_id = int(cone.face_solid_ids[cone_face_id])
stale_plane_id = 0
if cone.face_info(stale_plane_id).get("surface") == "cone":
raise SystemExit("cone integrity test expected Face 0 to be a stale non-cone candidate")
cone_context = {
"operation_name": "Face target cone boundary integrity probe",
"target_kind": "face",
"target_id": stale_plane_id,
"target_logical_id": stale_plane_id,
"parameters": {
"resize_strategy": "bounded-cone-recut-preserve-angle-reference-radius",
"part_id": cone_part_id,
"solid_id": cone_solid_id,
"surface": "cone",
"target_reference_radius": 3.0,
"embedded_cone_target_small_radius": 3.0,
"embedded_cone_target_large_radius": 6.0,
},
}
_expect_blocked(
"Face target cone boundary mismatch",
lambda: probe._verified_edit_quality_warnings(
cone_context,
cone.stats(),
cone.stats(),
cone.part_topology_stats(cone_part_id),
cone.part_topology_stats(cone_part_id),
before_quality_ok,
after_quality_ok,
after_model=cone,
),
)
edited_cone = StepModel.load(cone_path)
edited_before_stats = edited_cone.stats()
edited_before_part_stats = edited_cone.part_topology_stats(cone_part_id)
edited_cone.resize_conical_reference_radius(cone_face_id, 3.0)
_expect_allowed(
"Face target cone boundary reached despite stale id",
lambda: probe._verified_edit_quality_warnings(
cone_context,
edited_before_stats,
edited_cone.stats(),
edited_before_part_stats,
edited_cone.part_topology_stats(cone_part_id),
before_quality_ok,
after_quality_ok,
after_model=edited_cone,
),
)
_expect_allowed(
"Non-Face edit solid-count change only warns",
lambda: probe._verified_edit_quality_warnings(
_edge_context(),
before,
after_two_solids,
before,
after_two_solids,
before_quality_ok,
after_quality_ok,
),
)
if probe._isolation_for_plan({"risk": "medium"}, "resize_face_area_local", [0, 125.0]) is None:
raise SystemExit("medium-risk Face edits should use isolated execution")
if probe._isolation_for_plan({"risk": "medium"}, "resize_edge_length", [0, 12.0]) is not None:
raise SystemExit("unsupported medium-risk Edge edits should not use Face isolation")
if probe._isolation_for_plan({"risk": "low"}, "resize_face_area_local", [0, 125.0]) is None:
raise SystemExit("low-risk Face parameter edits should still use isolated execution")
if probe._isolation_for_plan({"risk": "high"}, "resize_edge_length", [0, 12.0]) is not None:
raise SystemExit("unsupported high-risk Edge edits should not enter the Face isolation worker")
print("Face edit integrity guards ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())