from __future__ import annotations import sys from pathlib import Path 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 DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" TOLERANCE = 1e-5 def _plane_metrics(model: StepModel) -> list[tuple[int, float, float, float, tuple[float, float, float]]]: metrics: list[tuple[int, float, float, float, tuple[float, float, float]]] = [] 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") or info.get("bbox_center") if not isinstance(center, tuple) or len(center) != 3: continue metrics.append( ( face_id, float(info.get("area") or 0.0), float(info.get("local_face_width") or 0.0), float(info.get("local_face_height") or 0.0), (float(center[0]), float(center[1]), float(center[2])), ) ) return metrics def _first_plane_face_near_size(model: StepModel, width: float, height: float) -> int: for face_id, _area, face_width, face_height, _center in _plane_metrics(model): if abs(face_width - width) <= TOLERANCE and abs(face_height - height) <= TOLERANCE: return face_id raise SystemExit(f"no plane Face near {width:g} x {height:g}") def _distance(a: tuple[float, float, float], b: tuple[float, float, float]) -> float: return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2) ** 0.5 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"Face lacks a stable center: {info}") return float(center[0]), float(center[1]), float(center[2]) def _assert_logical_face( model: StepModel, logical_id: int, label: str, *, area: float | None = None, width: float | None = None, height: float | None = None, center: tuple[float, float, float] | None = None, ) -> int: matches = model.face_ids_for_logical_id(logical_id) if not matches: raise SystemExit(f"{label}: logical Face {logical_id} was not retained") resolved = model.resolve_face_selection_id(logical_id) if resolved is None or resolved not in matches: raise SystemExit(f"{label}: logical Face {logical_id} did not resolve into retained matches {matches}") info = model.face_info(resolved) if info.get("surface") != "plane": raise SystemExit(f"{label}: retained logical Face should be planar, got {info.get('surface')}") if area is not None and abs(float(info.get("area") or 0.0) - area) > TOLERANCE: raise SystemExit(f"{label}: retained Face area should be {area:g}, got {info.get('area')}") if width is not None and abs(float(info.get("local_face_width") or 0.0) - width) > TOLERANCE: raise SystemExit(f"{label}: retained Face width should be {width:g}, got {info.get('local_face_width')}") if height is not None and abs(float(info.get("local_face_height") or 0.0) - height) > TOLERANCE: raise SystemExit(f"{label}: retained Face height should be {height:g}, got {info.get('local_face_height')}") if center is not None and _distance(_center(info), center) > TOLERANCE: raise SystemExit(f"{label}: retained Face center should be {center}, got {_center(info)}") return resolved def _fresh_selected_face() -> tuple[StepModel, int, int, dict[str, object]]: model = StepModel.load(DEFAULT_MODEL) face_id = _first_plane_face_near_size(model, 10.0, 10.0) return model, face_id, model.face_region_logical_id(face_id), model.face_info(face_id) def _target_center(info: dict[str, object], offset: tuple[float, float, float]) -> tuple[float, float, float]: center = _center(info) return center[0] + offset[0], center[1] + offset[1], center[2] + offset[2] def _offset_target_center(model: StepModel, face_id: int, info: dict[str, object], distance: float) -> tuple[float, float, float]: frame = model.face_plane_offset_frame(face_id) if frame is None: raise SystemExit("selected Face lacks a stable plane offset frame") _origin, direction, _position = frame center = _center(info) return ( center[0] + direction[0] * distance, center[1] + direction[1] * distance, center[2] + direction[2] * distance, ) def main() -> int: model, face_id, logical_id, _info = _fresh_selected_face() model.resize_face_size_local(face_id, 15.0, "width") resolved = _assert_logical_face(model, logical_id, "local Face width", width=15.0, height=10.0) print(f"local width logical {logical_id} -> Face {resolved}") model, face_id, logical_id, _info = _fresh_selected_face() model.resize_face_size_owning_scale(face_id, 15.0, "width") resolved = _assert_logical_face(model, logical_id, "owning Face width", width=15.0, height=10.0) print(f"owning width logical {logical_id} -> Face {resolved}") model, face_id, logical_id, _info = _fresh_selected_face() model.resize_face_area_local(face_id, 144.0) resolved = _assert_logical_face(model, logical_id, "local Face area", area=144.0) print(f"local area logical {logical_id} -> Face {resolved}") model, face_id, logical_id, _info = _fresh_selected_face() model.resize_face_area(face_id, 144.0) resolved = _assert_logical_face(model, logical_id, "owning Face area", area=144.0) print(f"owning area logical {logical_id} -> Face {resolved}") model, face_id, logical_id, info = _fresh_selected_face() center = _target_center(info, (2.0, 0.0, 3.0)) model.move_face_center_local(face_id, center) resolved = _assert_logical_face(model, logical_id, "local Face center", center=center) print(f"local center logical {logical_id} -> Face {resolved}") model, face_id, logical_id, info = _fresh_selected_face() center = _target_center(info, (2.0, 0.0, 3.0)) model.move_face_center_owning(face_id, center) resolved = _assert_logical_face(model, logical_id, "owning Face center", center=center) print(f"owning center logical {logical_id} -> Face {resolved}") for label, operation, distance in ( ("local Face plane offset", "local", 1.0), ("owning Face plane offset", "owning", 1.0), ("push/pull Face offset", "push_pull", 1.0), ("push/pull inward Face offset", "push_pull", -1.0), ): model, face_id, logical_id, info = _fresh_selected_face() target_center = _offset_target_center(model, face_id, info, distance) if operation == "local": model.move_face_plane_offset_local(face_id, distance) elif operation == "owning": model.translate_face_plane_offset_owning(face_id, distance) else: model.push_pull_face(face_id, distance) resolved = _assert_logical_face(model, logical_id, label, center=target_center) print(f"{label} logical {logical_id} -> Face {resolved}") print("Face logical selection retention ok") return 0 if __name__ == "__main__": raise SystemExit(main())