from __future__ import annotations import sys import tempfile from pathlib import Path from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder 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 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_cylinder(path: Path) -> None: cylinder = BRepPrimAPI_MakeCylinder(4.0, 8.0).Shape() _write_step(cylinder, path) def _first_planar_cap_face(model: StepModel) -> int: for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") == "plane": return face_id raise SystemExit("no planar cylinder cap Face was found") def _assert_blocked(plan: dict[str, object], label: str) -> None: if plan.get("status") != "blocked": raise SystemExit(f"{label} should be blocked for a planar Face on a curved Solid: {plan}") message = str(plan.get("message") or plan.get("blockers") or "") if "曲面" not in message: raise SystemExit(f"{label} should explain that the owning Solid contains curved faces: {plan}") 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(specs: list[dict[str, object]], key: str) -> dict[str, object]: for item in specs: if item.get("key") == key: return item raise SystemExit(f"{key} spec was not found") def _scope_mode(specs: list[dict[str, object]], key: str, mode: str) -> dict[str, object]: spec = _spec(specs, key) modes = spec.get("scope_modes") if not isinstance(modes, dict) or mode not in modes: raise SystemExit(f"{key} has no scope mode {mode}") selected = modes[mode] if not isinstance(selected, dict): raise SystemExit(f"{key} scope mode {mode} is invalid") return selected def _assert_local_scope_explains_curved_owner(specs: list[dict[str, object]], key: str) -> None: local_mode = _scope_mode(specs, key, "local") if bool(local_mode.get("enabled", True)): raise SystemExit(f"{key}/local should be disabled for a planar Face on a curved Solid") disabled_tip = str(local_mode.get("disabled_tip") or "") if "曲面" not in disabled_tip: raise SystemExit(f"{key}/local disabled tip should mention curved owner: {disabled_tip}") def _bbox_height(model: StepModel) -> float: info = model.part_info(1) bbox_size = info.get("bbox_size") if not isinstance(bbox_size, tuple) or len(bbox_size) != 3: raise SystemExit(f"part bbox_size is missing: {info}") return float(bbox_size[2]) def main() -> int: with tempfile.TemporaryDirectory(prefix="geom_param_face_mixed_surface_") as temp_dir: path = Path(temp_dir) / "cylinder.step" _write_cylinder(path) model = StepModel.load(path) face_id = _first_planar_cap_face(model) info = model.face_info(face_id) if bool(info.get("local_face_deform_ready", True)): raise SystemExit(f"planar cylinder cap should not allow local Face deformation: {info}") blocker = str(info.get("local_face_deform_blocker") or "") if "曲面" not in blocker: raise SystemExit(f"planar cylinder cap blocker should mention curved owner: {info}") center = info.get("area_center") or info.get("bbox_center") if not isinstance(center, tuple) or len(center) != 3: raise SystemExit(f"Face {face_id} does not expose a stable center") target_center = (float(center[0]), float(center[1]), float(center[2]) + 1.0) area = float(info.get("area") or 0.0) if area <= 0: raise SystemExit(f"Face {face_id} does not expose a stable area") _assert_blocked(model.face_center_local_move_plan(face_id, target_center), "Face center local move") _assert_blocked(model.face_area_local_resize_plan(face_id, area * 1.1), "Face area local resize") _assert_blocked(model.face_plane_offset_local_plan(face_id, 1.0), "Face plane offset local move") specs = _specs(face_id, info) semantics = _spec(specs, "face_edit_semantics") if "不能只改当前面" not in str(semantics.get("current_text") or ""): raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}") if "曲面" not in str(semantics.get("disabled_tip") or ""): raise SystemExit(f"Face edit semantics tip should include the curved-owner blocker: {semantics}") for key in ("area", "face_center_position", "face_target_normal_position"): _assert_local_scope_explains_curved_owner(specs, key) push_pull_mode = _scope_mode(specs, "face_target_normal_position", "push_pull") if not bool(push_pull_mode.get("enabled", False)): raise SystemExit("planar cylinder cap push/pull scope should remain available") huge_push_plan = model.push_pull_plan(face_id, 50.0) if huge_push_plan.get("status") != "blocked": raise SystemExit(f"huge planar cylinder cap push/pull should be blocked early: {huge_push_plan}") old_height = float(huge_push_plan.get("cylindrical_cap_extension_old_height") or 0.0) new_height = float(huge_push_plan.get("cylindrical_cap_extension_new_height") or 0.0) if old_height <= 0.0 or new_height <= old_height * 3.0: raise SystemExit(f"huge cap push/pull should expose the blocked height growth: {huge_push_plan}") huge_message = str(huge_push_plan.get("message") or "") if "圆柱高度" not in huge_message and "cylindrical" not in huge_message.lower(): raise SystemExit(f"huge cap push/pull should explain the cylinder-height risk: {huge_push_plan}") before_height = _bbox_height(model) push_plan = model.push_pull_plan(face_id, 1.0) if push_plan.get("status") == "blocked": raise SystemExit(f"planar cylinder cap push/pull should remain available: {push_plan}") push_result = model.push_pull_face(face_id, 1.0) stats = model.stats() if stats.solids != 1: raise SystemExit(f"planar cylinder cap push/pull should keep one solid: {stats}") after_height = _bbox_height(model) if after_height <= before_height: raise SystemExit(f"planar cylinder cap push/pull should increase bbox height: {before_height} -> {after_height}") print( "mixed-surface planar Face guard ok: " f"face_id={face_id}, blocker={blocker}, height={before_height:g}->{after_height:g}, " f"push_pull={push_result}" ) return 0 if __name__ == "__main__": raise SystemExit(main())