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 from step_editor.window_state import WindowStateMixin DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" class _PropertySpecProbe(WindowStateMixin): def __init__(self, face_id: int, info: dict[str, object]) -> None: self.model = object() self.operation_in_progress = False self.scan_in_progress = False self.load_in_progress = False self.selected_face_id = face_id self.selected_edge_id = None self.selected_kind = "face" self.selected_part_id = int(info.get("part_id", 1)) self.selected_solid_id = int(info.get("solid_id", 1)) self.manual_bottom_face_id = None self.manual_slot_pair_face_id = None def _specs(face_id: int, info: dict[str, object]) -> list[dict[str, object]]: probe = _PropertySpecProbe(face_id, info) specs, _used = probe._editable_property_specs(info) return specs def _spec(face_id: int, info: dict[str, object], key: str) -> dict[str, object]: for spec in _specs(face_id, info): if spec.get("key") == key: return spec raise SystemExit(f"{key} spec was not found for Face {face_id}") def _float(value: object, label: str) -> float: try: return float(value) except (TypeError, ValueError) as exc: raise SystemExit(f"{label} should be numeric, got {value!r}") from exc def _assert_close(label: str, value: object, expected: float, tolerance: float = 1e-5) -> None: number = _float(value, label) if abs(number - expected) > tolerance: raise SystemExit(f"{label} should be {expected:g}, got {number:g}") 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 _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 _first_plane_face_near_size(model: StepModel, width: float, height: float, tolerance: float = 1e-5) -> int: for face_id, _area, face_width, face_height, _center in _plane_metrics(model): direct = abs(face_width - width) <= tolerance and abs(face_height - height) <= tolerance swapped = abs(face_width - height) <= tolerance and abs(face_height - width) <= tolerance if direct or swapped: return face_id raise SystemExit(f"no plane Face near {width:g} x {height:g}") def _nearest_plane_center( model: StepModel, target: tuple[float, float, float], ) -> tuple[int, tuple[float, float, float], float]: best: tuple[int, tuple[float, float, float], float] | None = None for face_id, _area, _width, _height, center in _plane_metrics(model): error = _distance(center, target) if best is None or error < best[2]: best = (face_id, center, error) if best is None: raise SystemExit("no plane Face center could be measured") return best def _assert_spec_readback( model: StepModel, face_id: int, key: str, expected: float, tolerance: float = 1e-5, ) -> None: info = model.face_info(face_id) spec = _spec(face_id, info, key) _assert_close(f"{key} current_raw", spec.get("current_raw"), expected, tolerance) _assert_close(f"{key} target_text", spec.get("target_text"), expected, tolerance) if spec.get("status_text") == "不可修改": raise SystemExit(f"{key} should remain editable after readback: {spec}") def main() -> int: model = StepModel.load(DEFAULT_MODEL) face_id = _first_plane_face_near_size(model, 10.0, 10.0) model.resize_face_size_local(face_id, 15.0, "width") face_id = _first_plane_face_near_size(model, 15.0, 10.0) _assert_spec_readback(model, face_id, "local_face_width", 15.0) model = StepModel.load(DEFAULT_MODEL) face_id = _first_plane_face_near_size(model, 10.0, 10.0) model.resize_face_area_local(face_id, 144.0) for candidate_id, area, _width, _height, _center in _plane_metrics(model): if abs(area - 144.0) <= 1e-5: info = model.face_info(candidate_id) _assert_close("area diagnostic value", info.get("area"), 144.0) if any(spec.get("key") == "area" for spec in _specs(candidate_id, info)): raise SystemExit("area should stay a diagnostic result, not an editable property spec") break else: raise SystemExit("no plane Face near area 144 after local area resize") model = StepModel.load(DEFAULT_MODEL) face_id = _first_plane_face_near_size(model, 10.0, 10.0) info = model.face_info(face_id) center = info.get("area_center") or info.get("bbox_center") if not isinstance(center, tuple) or len(center) != 3: raise SystemExit("selected Face lacks a stable center") target_center = (float(center[0]) + 2.0, float(center[1]), float(center[2]) + 3.0) model.move_face_center_local(face_id, target_center) moved_face_id, _moved_center, center_error = _nearest_plane_center(model, target_center) if center_error > 1e-5: raise SystemExit(f"no plane Face near moved center {target_center}") moved_info = model.face_info(moved_face_id) center_spec = _spec(moved_face_id, moved_info, "face_center_position") if str(center_spec.get("target_text") or "").replace(" ", "") != "7,5,3": raise SystemExit(f"Face center target_text did not read back the moved center: {center_spec}") for strategy in ("local", "owning", "push_pull"): model = StepModel.load(DEFAULT_MODEL) face_id = _first_plane_face_near_size(model, 10.0, 10.0) info = model.face_info(face_id) frame = model.face_plane_offset_frame(face_id) if frame is None: raise SystemExit("selected Face lacks a stable plane offset frame") _origin, direction, old_position = frame center = info.get("area_center") or info.get("bbox_center") if not isinstance(center, tuple) or len(center) != 3: raise SystemExit("selected Face lacks a stable center") target_center = ( float(center[0]) + direction[0], float(center[1]) + direction[1], float(center[2]) + direction[2], ) if strategy == "local": model.move_face_plane_offset_local(face_id, 1.0) elif strategy == "owning": model.translate_face_plane_offset_owning(face_id, 1.0) else: model.push_pull_face(face_id, 1.0) moved_face_id, _moved_center, center_error = _nearest_plane_center(model, target_center) if center_error > 1e-5: raise SystemExit(f"no plane Face near offset target for strategy {strategy}") moved_info = model.face_info(moved_face_id) spec = _spec(moved_face_id, moved_info, "face_target_normal_position") current_raw = _float(spec.get("current_raw"), f"{strategy} face_target_normal_position current_raw") target_text = _float(spec.get("target_text"), f"{strategy} face_target_normal_position target_text") if abs(current_raw) <= 1e-6: raise SystemExit(f"{strategy} offset readback still looks unchanged: {spec}") if abs(current_raw - target_text) > 1e-5: raise SystemExit(f"{strategy} offset target_text should match refreshed current value: {spec}") if abs(abs(current_raw) - abs(old_position + 1.0)) > 1e-5: raise SystemExit(f"{strategy} offset readback has unexpected plane position: {spec}") print("Face property readback ok") return 0 if __name__ == "__main__": raise SystemExit(main())