feat: 完善 Face 参数化编辑和隔离执行

This commit is contained in:
2026-07-31 16:36:05 +08:00
parent 27e4f7236c
commit bb44e3920d
31 changed files with 5428 additions and 252 deletions
@@ -0,0 +1,224 @@
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 _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 _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_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")
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())