from __future__ import annotations import sys import tempfile from pathlib import Path from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox 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 from step_editor.window_state import WindowStateMixin SOURCE_WIDTH = 10.0 SOURCE_HEIGHT = 20.0 TARGET_WIDTH = 14.0 TARGET_HEIGHT = 26.0 TOLERANCE = 2e-4 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 _write_rectangular_box(path: Path) -> None: shape = BRepPrimAPI_MakeBox(20.0, 10.0, 6.0).Shape() _write_step(shape, path) def _plane_infos(model: StepModel) -> list[tuple[int, float, float]]: infos: list[tuple[int, float, float]] = [] for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") != "plane": continue width = float(info.get("local_face_width") or 0.0) height = float(info.get("local_face_height") or 0.0) infos.append((face_id, width, height)) return infos def _face_near_size(model: StepModel, width: float, height: float) -> int: for face_id, face_width, face_height in _plane_infos(model): if abs(face_width - width) <= TOLERANCE and abs(face_height - height) <= TOLERANCE: return face_id raise SystemExit(f"no plane Face near width={width:g}, height={height:g}; got {_plane_infos(model)}") def _assert_close(label: str, value: object, expected: float) -> None: try: number = float(value) except (TypeError, ValueError) as exc: raise SystemExit(f"{label} should be numeric, got {value!r}") from exc if abs(number - expected) > TOLERANCE: raise SystemExit(f"{label} should be {expected:g}, got {number:g}") 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 _assert_size_specs(model: StepModel, face_id: int, width: float, height: float, label: str) -> None: info = model.face_info(face_id) probe = _PropertySpecProbe(face_id, info) specs, _used = probe._editable_property_specs(info) by_key = {str(spec.get("key")): spec for spec in specs} width_spec = by_key.get("local_face_width") height_spec = by_key.get("local_face_height") if width_spec is None or height_spec is None: raise SystemExit(f"{label} should expose width and height specs: {sorted(by_key)}") _assert_close(f"{label} width current_raw", width_spec.get("current_raw"), width) _assert_close(f"{label} width target_text", width_spec.get("target_text"), width) _assert_close(f"{label} height current_raw", height_spec.get("current_raw"), height) _assert_close(f"{label} height target_text", height_spec.get("target_text"), height) if width_spec.get("status_text") == "不可修改" or height_spec.get("status_text") == "不可修改": raise SystemExit(f"{label} width/height should remain editable: {width_spec} {height_spec}") def _axis_index(direction: object) -> int: if not isinstance(direction, tuple) or len(direction) != 3: raise SystemExit(f"axis direction should be a vector, got {direction!r}") values = [abs(float(direction[0])), abs(float(direction[1])), abs(float(direction[2]))] return max(range(3), key=lambda index: values[index]) def _bbox_size(model: StepModel) -> tuple[float, float, float]: return tuple(float(value) for value in model.geometry_stats()["bbox_size"]) def _run_local_case(path: Path, axis: str, target_size: float, expected_width: float, expected_height: float) -> None: model = StepModel.load(path) face_id = _face_near_size(model, SOURCE_WIDTH, SOURCE_HEIGHT) before = model.stats() plan = model.face_size_local_resize_plan(face_id, target_size, axis) if plan.get("status") == "blocked": raise SystemExit(f"local rectangular {axis} plan was blocked: {plan}") expected_strategy = f"local-face-{axis}-only-deform" if plan.get("resize_strategy") != expected_strategy: raise SystemExit(f"local rectangular {axis} expected {expected_strategy}, got {plan}") result = model.resize_face_size_local(face_id, target_size, axis) _assert_single_solid(model, f"local rectangular {axis}") if model.stats().faces != before.faces: raise SystemExit(f"local rectangular {axis} should keep face count stable: before={before}, after={model.stats()}") edited_face_id = _face_near_size(model, expected_width, expected_height) _assert_size_specs(model, edited_face_id, expected_width, expected_height, f"local rectangular {axis}") print(f"local {axis}: Face {face_id} -> {edited_face_id}, size={expected_width:g} x {expected_height:g}") print(result.encode("ascii", "backslashreplace").decode("ascii")) def _run_owning_case(path: Path, axis: str, target_size: float, expected_width: float, expected_height: float) -> None: model = StepModel.load(path) face_id = _face_near_size(model, SOURCE_WIDTH, SOURCE_HEIGHT) before = model.stats() before_bbox = _bbox_size(model) plan = model.face_size_owning_scale_plan(face_id, target_size, axis) if plan.get("status") == "blocked": raise SystemExit(f"owning rectangular {axis} plan was blocked: {plan}") expected_strategy = f"axis-scale-owning-shape-from-face-{axis}" if plan.get("resize_strategy") != expected_strategy: raise SystemExit(f"owning rectangular {axis} expected {expected_strategy}, got {plan}") if plan.get("owning_face_size_rebuild_mode") != "planar-rebuild": raise SystemExit(f"simple rectangular box should use planar rebuild for owning {axis}: {plan}") resized_axis = _axis_index(plan.get("face_size_axis_direction")) result = model.resize_face_size_owning_scale(face_id, target_size, axis) _assert_single_solid(model, f"owning rectangular {axis}") if model.stats().faces != before.faces: raise SystemExit(f"owning rectangular {axis} should keep face count stable: before={before}, after={model.stats()}") after_bbox = _bbox_size(model) _assert_close(f"owning rectangular {axis} bbox axis {resized_axis}", after_bbox[resized_axis], target_size) for index, before_size in enumerate(before_bbox): if index == resized_axis: continue _assert_close(f"owning rectangular {axis} unchanged bbox axis {index}", after_bbox[index], before_size) edited_face_id = _face_near_size(model, expected_width, expected_height) _assert_size_specs(model, edited_face_id, expected_width, expected_height, f"owning rectangular {axis}") print(f"owning {axis}: Face {face_id} -> {edited_face_id}, size={expected_width:g} x {expected_height:g}") print(result.encode("ascii", "backslashreplace").decode("ascii")) def main() -> int: with tempfile.TemporaryDirectory(prefix="geom_param_face_rect_axes_") as temp_dir: path = Path(temp_dir) / "rectangular_box.step" _write_rectangular_box(path) _run_local_case(path, "width", TARGET_WIDTH, TARGET_WIDTH, SOURCE_HEIGHT) _run_local_case(path, "height", TARGET_HEIGHT, SOURCE_WIDTH, TARGET_HEIGHT) _run_owning_case(path, "width", TARGET_WIDTH, TARGET_WIDTH, SOURCE_HEIGHT) _run_owning_case(path, "height", TARGET_HEIGHT, SOURCE_WIDTH, TARGET_HEIGHT) print("rectangular Face width/height axis semantics ok") return 0 if __name__ == "__main__": raise SystemExit(main())