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

277 lines
13 KiB
Python

from __future__ import annotations
import math
import sys
import tempfile
from pathlib import Path
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakePrism
from OCC.Core.gp import gp_Pnt, gp_Vec
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_triangular_prism(path: Path) -> None:
polygon = BRepBuilderAPI_MakePolygon()
polygon.Add(gp_Pnt(0.0, 0.0, 0.0))
polygon.Add(gp_Pnt(12.0, 0.0, 0.0))
polygon.Add(gp_Pnt(0.0, 8.0, 0.0))
polygon.Close()
if hasattr(polygon, "IsDone") and not polygon.IsDone():
raise RuntimeError("Could not create triangular prism profile.")
face_maker = BRepBuilderAPI_MakeFace(polygon.Wire())
if hasattr(face_maker, "IsDone") and not face_maker.IsDone():
raise RuntimeError("Could not create triangular prism face.")
prism = BRepPrimAPI_MakePrism(face_maker.Face(), gp_Vec(0.0, 0.0, 6.0)).Shape()
_write_step(prism, path)
def _write_trapezoid_prism(path: Path) -> None:
polygon = BRepBuilderAPI_MakePolygon()
polygon.Add(gp_Pnt(0.0, 0.0, 0.0))
polygon.Add(gp_Pnt(12.0, 0.0, 0.0))
polygon.Add(gp_Pnt(10.0, 0.0, 6.0))
polygon.Add(gp_Pnt(0.0, 0.0, 8.0))
polygon.Close()
if hasattr(polygon, "IsDone") and not polygon.IsDone():
raise RuntimeError("Could not create trapezoid prism profile.")
face_maker = BRepBuilderAPI_MakeFace(polygon.Wire())
if hasattr(face_maker, "IsDone") and not face_maker.IsDone():
raise RuntimeError("Could not create trapezoid prism face.")
prism = BRepPrimAPI_MakePrism(face_maker.Face(), gp_Vec(0.0, 6.0, 0.0)).Shape()
_write_step(prism, path)
def _triangle_face_id(model: StepModel) -> int:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
if int(info.get("local_face_size_source_point_count") or 0) == 3:
return face_id
raise SystemExit("no triangular planar Face was found")
def _sloped_quad_face_id(model: StepModel) -> int:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
normal = info.get("normal") or info.get("push_pull_outward_direction")
if not isinstance(normal, tuple) or len(normal) != 3:
continue
if abs(float(normal[0])) > 0.1 and abs(float(normal[2])) > 0.1:
return face_id
raise SystemExit("no sloped quadrilateral planar Face was found")
def _triangle_infos(model: StepModel) -> list[dict[str, object]]:
infos: list[dict[str, object]] = []
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
if int(info.get("local_face_size_source_point_count") or 0) == 3:
infos.append(info)
return infos
def _center(info: dict[str, object]) -> tuple[float, float, float]:
center = info.get("area_center") or info.get("bbox_center")
if not isinstance(center, tuple) or len(center) != 3:
raise SystemExit(f"triangular Face lacks a stable center: {info}")
return float(center[0]), float(center[1]), float(center[2])
def _distance(a: tuple[float, float, float], b: tuple[float, float, float]) -> float:
return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2)
def _nearest_center_error(model: StepModel, target: tuple[float, float, float]) -> float:
infos = _triangle_infos(model)
if not infos:
raise SystemExit("local edit removed every triangular Face")
return min(_distance(_center(info), target) for info in infos)
def _has_triangle_area(model: StepModel, target_area: float, tolerance: float = 1e-4) -> bool:
return any(abs(float(info.get("area") or 0.0) - target_area) <= tolerance for info in _triangle_infos(model))
def _has_triangle_size(
model: StepModel,
key: str,
target_size: float,
tolerance: float = 1e-4,
) -> bool:
return any(abs(float(info.get(key) or 0.0) - target_size) <= tolerance for info in _triangle_infos(model))
def _assert_not_blocked(plan: dict[str, object], label: str) -> None:
if plan.get("status") == "blocked":
raise SystemExit(f"{label} should be available for a simple triangular Face: {plan}")
def _assert_keep_relations_blocked_for_angled(plan: dict[str, object], label: str) -> None:
if plan.get("status") != "blocked":
raise SystemExit(f"{label} should be blocked before execution: {plan}")
if plan.get("face_push_pull_planar_relation_constraint_requested") is not True:
raise SystemExit(f"{label} should record the requested keep-relation constraint: {plan}")
if plan.get("face_push_pull_planar_constraint_status") != "blocked":
raise SystemExit(f"{label} should expose blocked keep-relation constraint status: {plan}")
if int(plan.get("face_push_pull_planar_constraint_angled_count", 0) or 0) <= 0:
raise SystemExit(f"{label} should count at least one angled relation: {plan}")
message = str(plan.get("message") or "") + " " + str(plan.get("blockers") or "")
if "斜交" not in message:
raise SystemExit(f"{label} blocker should explain angled first-level planar relations: {plan}")
def _assert_single_solid(model: StepModel, label: str) -> None:
stats = model.stats()
if stats.solids != 1:
raise SystemExit(f"{label} should keep one Solid, got {stats}")
def _fresh_model(path: Path) -> tuple[StepModel, int, dict[str, object]]:
model = StepModel.load(path)
face_id = _triangle_face_id(model)
return model, face_id, model.face_info(face_id)
def main() -> int:
with tempfile.TemporaryDirectory(prefix="geom_param_face_triangle_") as temp_dir:
path = Path(temp_dir) / "triangular_prism.step"
_write_triangular_prism(path)
model, face_id, info = _fresh_model(path)
current_center = _center(info)
target_center = (current_center[0] + 1.0, current_center[1] + 0.5, current_center[2] + 1.5)
plan = model.face_center_local_move_plan(face_id, target_center)
_assert_not_blocked(plan, "triangular Face center move")
model.move_face_center_local(face_id, target_center)
_assert_single_solid(model, "triangular Face center move")
if _nearest_center_error(model, target_center) > 1e-4:
raise SystemExit("triangular Face center move did not reach the target center")
model, face_id, info = _fresh_model(path)
current_area = float(info.get("area") or 0.0)
target_area = current_area * 1.44
plan = model.face_area_local_resize_plan(face_id, target_area)
_assert_not_blocked(plan, "triangular Face area resize")
model.resize_face_area_local(face_id, target_area)
_assert_single_solid(model, "triangular Face area resize")
if not _has_triangle_area(model, target_area):
raise SystemExit("triangular Face area resize did not create the target area")
for axis, key in (("width", "local_face_width"), ("height", "local_face_height")):
model, face_id, info = _fresh_model(path)
current_size = float(info.get(key) or 0.0)
target_size = current_size * 1.25
plan = model.face_size_local_resize_plan(face_id, target_size, axis)
_assert_not_blocked(plan, f"triangular Face {axis} resize")
model.resize_face_size_local(face_id, target_size, axis)
_assert_single_solid(model, f"triangular Face {axis} resize")
if not _has_triangle_size(model, key, target_size):
raise SystemExit(f"triangular Face {axis} resize did not create the target size")
model, face_id, info = _fresh_model(path)
frame = model.face_plane_offset_frame(face_id)
if frame is None:
raise SystemExit("triangular Face lacks a stable plane offset frame")
_origin, direction, _position = frame
current_center = _center(info)
offset_distance = 1.0
target_center = (
current_center[0] + direction[0] * offset_distance,
current_center[1] + direction[1] * offset_distance,
current_center[2] + direction[2] * offset_distance,
)
plan = model.face_plane_offset_local_plan(face_id, offset_distance)
_assert_not_blocked(plan, "triangular Face plane offset")
model.move_face_plane_offset_local(face_id, offset_distance)
_assert_single_solid(model, "triangular Face plane offset")
if _nearest_center_error(model, target_center) > 1e-4:
raise SystemExit("triangular Face plane offset did not reach the target plane")
model, face_id, _info = _fresh_model(path)
plan = model.push_pull_plan(face_id, 1.0)
_assert_not_blocked(plan, "triangular Face push/pull")
model.push_pull_face(face_id, 1.0)
_assert_single_solid(model, "triangular Face push/pull")
wedge_path = Path(temp_dir) / "trapezoid_prism.step"
_write_trapezoid_prism(wedge_path)
wedge = StepModel.load(wedge_path)
wedge_face_id = _sloped_quad_face_id(wedge)
plan = wedge.push_pull_plan(wedge_face_id, 0.5)
_assert_not_blocked(plan, "sloped planar Face push/pull")
keep_plan = wedge.push_pull_keep_relations_plan(wedge_face_id, 0.5)
_assert_keep_relations_blocked_for_angled(keep_plan, "sloped planar Face keep-relations push/pull")
model, face_id, info = _fresh_model(path)
current_area = float(info.get("area") or 0.0)
target_area = current_area * 1.44
plan = model.face_area_scale_plan(face_id, target_area)
_assert_not_blocked(plan, "triangular Face owning area resize")
model.resize_face_area(face_id, target_area)
_assert_single_solid(model, "triangular Face owning area resize")
if not _has_triangle_area(model, target_area):
raise SystemExit("triangular Face owning area resize did not create the target area")
for axis, key in (("width", "local_face_width"), ("height", "local_face_height")):
model, face_id, info = _fresh_model(path)
current_size = float(info.get(key) or 0.0)
target_size = current_size * 1.25
plan = model.face_size_owning_scale_plan(face_id, target_size, axis)
_assert_not_blocked(plan, f"triangular Face owning {axis} resize")
model.resize_face_size_owning_scale(face_id, target_size, axis)
_assert_single_solid(model, f"triangular Face owning {axis} resize")
if not _has_triangle_size(model, key, target_size):
raise SystemExit(f"triangular Face owning {axis} resize did not create the target size")
model, face_id, info = _fresh_model(path)
current_center = _center(info)
offset = (1.0, 0.5, 1.5)
target_center = (
current_center[0] + offset[0],
current_center[1] + offset[1],
current_center[2] + offset[2],
)
plan = model.face_center_owning_translation_plan(face_id, target_center)
_assert_not_blocked(plan, "triangular Face owning center move")
model.move_face_center_owning(face_id, target_center)
_assert_single_solid(model, "triangular Face owning center move")
if _nearest_center_error(model, target_center) > 1e-4:
raise SystemExit("triangular Face owning center move did not reach the target center")
model, face_id, info = _fresh_model(path)
frame = model.face_plane_offset_frame(face_id)
if frame is None:
raise SystemExit("triangular Face lacks a stable plane offset frame")
_origin, direction, _position = frame
current_center = _center(info)
target_center = (
current_center[0] + direction[0],
current_center[1] + direction[1],
current_center[2] + direction[2],
)
plan = model.face_plane_offset_owning_translation_plan(face_id, 1.0)
_assert_not_blocked(plan, "triangular Face owning plane offset")
model.translate_face_plane_offset_owning(face_id, 1.0)
_assert_single_solid(model, "triangular Face owning plane offset")
if _nearest_center_error(model, target_center) > 1e-4:
raise SystemExit("triangular Face owning plane offset did not reach the target plane")
print("non-rectangular planar Face edit semantics ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())