feat: 完善 Face 参数化编辑和隔离执行
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user