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())