2026-07-31 16:36:05 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 16:51:52 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 16:36:05 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 16:51:52 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 16:36:05 +08:00
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 16:51:52 +08:00
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 16:36:05 +08:00
|
|
|
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")
|
|
|
|
|
|
2026-08-10 16:51:52 +08:00
|
|
|
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")
|
|
|
|
|
|
2026-07-31 16:36:05 +08:00
|
|
|
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())
|