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
+128
View File
@@ -0,0 +1,128 @@
from __future__ import annotations
import argparse
from pathlib import Path
import sys
import tempfile
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.gp import 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 import StepModel
from step_editor.step_io import _write_step
def _write_split_top_box(path: Path) -> None:
left = BRepPrimAPI_MakeBox(5.0, 10.0, 10.0).Shape()
right = BRepPrimAPI_MakeBox(gp_Pnt(5.0, 0.0, 0.0), 5.0, 10.0, 10.0).Shape()
fuse = BRepAlgoAPI_Fuse(left, right)
fuse.Build()
if not fuse.IsDone():
raise SystemExit("failed to build split-top box")
_write_step(fuse.Shape(), path)
def _top_faces(model: StepModel, z_value: float, tolerance: float) -> list[int]:
face_ids: list[int] = []
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
center = info.get("area_center")
if not isinstance(center, tuple) or len(center) != 3:
continue
if abs(float(center[2]) - z_value) <= tolerance:
face_ids.append(face_id)
return face_ids
def _has_top_face(model: StepModel, z_value: float, area: float, tolerance: float) -> bool:
for face_id in _top_faces(model, z_value, tolerance):
info = model.face_info(face_id)
if abs(float(info.get("area") or 0.0) - area) <= tolerance:
return True
return False
def _assert_logical_top_region(
model: StepModel,
logical_id: int,
z_value: float,
area: float,
tolerance: float,
) -> int:
matches = model.face_ids_for_logical_id(logical_id)
if not matches:
raise SystemExit(f"logical Face {logical_id} was not retained after coplanar push/pull")
resolved = model.resolve_face_selection_id(logical_id)
if resolved is None or resolved not in matches:
raise SystemExit(f"logical Face {logical_id} did not resolve into retained matches {matches}")
info = model.face_info(resolved)
center = info.get("area_center")
if not isinstance(center, tuple) or len(center) != 3:
raise SystemExit(f"retained logical Face {logical_id} lacks a stable center: {info}")
if abs(float(center[2]) - z_value) > tolerance:
raise SystemExit(f"retained logical Face {logical_id} should be at z={z_value:g}, got {center}")
if abs(float(info.get("area") or 0.0) - area) > tolerance:
raise SystemExit(f"retained logical Face {logical_id} should have area {area:g}, got {info.get('area')}")
return resolved
def main() -> int:
parser = argparse.ArgumentParser(description="Verify coplanar split Face push/pull as one plane region.")
parser.add_argument("--distance", type=float, default=1.0)
parser.add_argument("--tolerance", type=float, default=2e-4)
args = parser.parse_args()
with tempfile.TemporaryDirectory(prefix="geom_param_face_coplanar_") as temp_dir:
model_path = Path(temp_dir) / "split_top_box.step"
_write_split_top_box(model_path)
model = StepModel.load(model_path)
before = model.stats()
top_faces = _top_faces(model, 10.0, args.tolerance)
if len(top_faces) != 2:
raise SystemExit(f"expected two split top faces before push/pull, got {top_faces}")
face_id = top_faces[0]
logical_id = model.face_region_logical_id(face_id)
plan = model.push_pull_plan(face_id, args.distance)
scope_ids = tuple(plan.get("push_pull_scope_face_ids", ()))
if set(scope_ids) != set(top_faces):
raise SystemExit(f"expected push/pull scope {top_faces}, got {scope_ids}")
if plan["status"] == "blocked":
raise SystemExit(f"coplanar push/pull plan was blocked: {plan['message']}")
result = model.push_pull_face(face_id, args.distance)
after = model.stats()
if after.solids != before.solids:
raise SystemExit(f"solid count changed: before={before.solids}, after={after.solids}")
if not _has_top_face(model, 10.0 + args.distance, 100.0, args.tolerance):
raise SystemExit("pushed coplanar top region was not rebuilt as a 100 mm^2 top plane")
logical_face_id = _assert_logical_top_region(
model,
logical_id,
10.0 + args.distance,
100.0,
args.tolerance,
)
print(f"model={model_path}")
print(f"source_face={face_id}")
print(f"source_logical_face={logical_id}")
print(f"retained_logical_face={logical_face_id}")
print(f"scope_faces={scope_ids}")
print(f"before={before}")
print(f"after={after}")
print(f"distance={args.distance:.6f}")
print(f"strategy={plan.get('resize_strategy', 'push-pull-planar-face')}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
return 0
if __name__ == "__main__":
raise SystemExit(main())