from __future__ import annotations from pathlib import Path import sys from OCC.Core.BRep import BRep_Tool from OCC.Core.TopAbs import TopAbs_VERTEX from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopoDS import topods 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 scripts.verify_property_editor_specs import _PropertySpecProbe, _spec DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" TOLERANCE = 1e-5 def _center(info: dict[str, object]) -> tuple[float, float, float]: value = info.get("area_center") or info.get("bbox_center") if not isinstance(value, tuple) or len(value) != 3: raise SystemExit(f"Face has no stable center: {info}") return float(value[0]), float(value[1]), float(value[2]) def _top_plane_face(model: StepModel) -> int: return _extreme_plane_face(model, highest=True) def _bottom_plane_face(model: StepModel) -> int: return _extreme_plane_face(model, highest=False) def _extreme_plane_face(model: StepModel, *, highest: bool) -> int: best: tuple[float, int] | None = None for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") != "plane": continue center = _center(info) if best is None or (center[2] > best[0] if highest else center[2] < best[0]): best = (center[2], face_id) if best is None: raise SystemExit("No planar Face found.") return best[1] def _plane_center_z_values(model: StepModel) -> list[float]: values: list[float] = [] for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") != "plane": continue values.append(round(_center(info)[2], 6)) return sorted(values) def _assert_close(actual: float, expected: float, label: str) -> None: if abs(float(actual) - float(expected)) > TOLERANCE: raise SystemExit(f"{label}: expected {expected:g}, got {actual:g}") def _edge_vertex_points(model: StepModel, edge_id: int) -> list[tuple[float, float, float]]: points: list[tuple[float, float, float]] = [] explorer = TopExp_Explorer(model.edges[edge_id], TopAbs_VERTEX) while explorer.More(): vertex = topods.Vertex(explorer.Current()) point = BRep_Tool.Pnt(vertex) item = (float(point.X()), float(point.Y()), float(point.Z())) if not any( abs(item[0] - existing[0]) <= TOLERANCE and abs(item[1] - existing[1]) <= TOLERANCE and abs(item[2] - existing[2]) <= TOLERANCE for existing in points ): points.append(item) explorer.Next() return points def _assert_points_z(points: object, expected: float, label: str) -> None: if not isinstance(points, (tuple, list)) or len(points) == 0: raise SystemExit(f"{label}: expected non-empty 3D points, got {points!r}") for index, point in enumerate(points): if not isinstance(point, (tuple, list)) or len(point) != 3: raise SystemExit(f"{label}: point {index} is not a 3D coordinate: {point!r}") _assert_close(float(point[2]), expected, f"{label} point {index} Z") def _assert_first_level_topology(model: StepModel, face_id: int) -> dict[str, object]: topology = model.face_first_level_topology(face_id) if topology.get("topology_relation_depth") != 1: raise SystemExit(f"Face topology depth should be 1, got {topology}") if topology.get("topology_relation_boundary") != "shared-edge": raise SystemExit(f"Face topology should use shared-edge boundary, got {topology}") if int(topology.get("same_domain_face_count", 0)) != 1: raise SystemExit(f"Cube top Face should be a single same-domain region, got {topology}") if int(topology.get("first_level_boundary_edge_count", 0)) != 4: raise SystemExit(f"Cube top Face should expose 4 first-level boundary Edges, got {topology}") if int(topology.get("first_level_boundary_vertex_count", 0)) != 4: raise SystemExit(f"Cube top Face should expose 4 first-level boundary Vertices, got {topology}") adjacent = tuple(topology.get("first_level_adjacent_face_ids", ())) if len(adjacent) != 4: raise SystemExit(f"Cube top Face should have 4 shared-edge adjacent Faces, got {topology}") first_level = set(int(item) for item in topology.get("first_level_face_ids", ())) if len(first_level) != 5 or face_id not in first_level: raise SystemExit(f"Cube top Face first-level set should contain selected Face + 4 side Faces, got {topology}") ignored = tuple(topology.get("topology_ignored_relation_depths", ())) if "second-level" not in ignored or "third-level" not in ignored: raise SystemExit(f"Face topology should explicitly leave deeper relations for later, got {topology}") return topology def _assert_plan_exposes_first_level(plan: dict[str, object], label: str) -> None: if plan.get("topology_relation_depth") != 1: raise SystemExit(f"{label} should expose topology_relation_depth=1, got {plan}") if int(plan.get("first_level_adjacent_face_count", 0)) != 4: raise SystemExit(f"{label} should expose 4 first-level adjacent Faces, got {plan}") if int(plan.get("first_level_boundary_edge_count", 0)) != 4: raise SystemExit(f"{label} should expose 4 first-level boundary Edges, got {plan}") if "二级" not in str(plan.get("topology_ignored_relation_note", "")): raise SystemExit(f"{label} should explain that second/third-level relations are not propagated yet, got {plan}") def _assert_selection_exposes_first_level(model: StepModel, face_id: int) -> None: probe = _PropertySpecProbe() probe.model = model probe.selected_face_id = face_id probe.selected_kind = "feature" quick_info = model.quick_face_info(face_id) feature_info = probe._feature_info_for_selected_face(face_id, quick_info) probe.selected_part_id = int(feature_info.get("part_id", 1)) solid_id = int(feature_info.get("solid_id", -1)) probe.selected_solid_id = solid_id if solid_id >= 0 else None if int(feature_info.get("topology_relation_depth", 0) or 0) != 1: raise SystemExit(f"Selected Face feature info should expose first-level topology, got {feature_info}") if int(feature_info.get("first_level_adjacent_face_count", 0) or 0) != 4: raise SystemExit(f"Selected Face should expose 4 first-level adjacent Faces, got {feature_info}") editable_specs, _used = probe._editable_property_specs(feature_info) topology_row = _spec(editable_specs, "face_first_level_topology") topology_text = str(topology_row.get("current_text") or "") for fragment in ("Face 区域 1 个", "边界 Edge 4 条", "共享边相邻 Face 4 个"): if fragment not in topology_text: raise SystemExit(f"Selected Face topology row is not clear enough: {topology_row}") feature_rows = probe._feature_property_specs(editable_specs, feature_info) feature_row_keys = {str(spec.get("key", "")) for spec in feature_rows} if "face_first_level_topology" in feature_row_keys: raise SystemExit( "Feature parameter table should keep first-level topology in diagnostics, " f"not in editable feature rows: {feature_rows}" ) def main() -> int: model = StepModel.load(DEFAULT_MODEL) face_id = _top_plane_face(model) topology = _assert_first_level_topology(model, face_id) _assert_selection_exposes_first_level(model, face_id) info = model.face_info(face_id) current_center = _center(info) target_center = (current_center[0], current_center[1], current_center[2] + 2.0) plans = ( ("center local", model.face_center_local_move_plan(face_id, target_center)), ("area local", model.face_area_local_resize_plan(face_id, 144.0)), ("width local", model.face_size_local_resize_plan(face_id, 15.0, "width")), ("offset local", model.face_plane_offset_local_plan(face_id, 2.0)), ("push pull", model.push_pull_plan(face_id, 2.0)), ) for label, plan in plans: if plan.get("status") == "blocked": raise SystemExit(f"{label} unexpectedly blocked: {plan}") _assert_plan_exposes_first_level(plan, label) before = model.stats() result = model.move_face_center_local(face_id, target_center) after = model.stats() if after.solids != before.solids: raise SystemExit(f"Local Face move changed solid count: before={before}, after={after}") if after.faces != before.faces: raise SystemExit(f"Cube first-level local move should keep face count stable: before={before}, after={after}") z_values = _plane_center_z_values(model) if len(z_values) != 6: raise SystemExit(f"Expected 6 planar Faces after local move, got z values {z_values}") _assert_close(z_values[0], 0.0, "Second-level bottom Face center Z") for index, value in enumerate(z_values[1:5], start=1): _assert_close(value, 6.0, f"First-level side Face {index} center Z") _assert_close(z_values[-1], 12.0, "Moved source Face center Z") moved_face_id = _top_plane_face(model) moved_topology = _assert_first_level_topology(model, moved_face_id) _assert_points_z( moved_topology.get("first_level_boundary_vertex_points"), 12.0, "Moved Face first-level boundary Vertex", ) for edge_id in moved_topology.get("first_level_boundary_edge_ids", ()): _assert_points_z( _edge_vertex_points(model, int(edge_id)), 12.0, f"Moved Face first-level boundary Edge {edge_id}", ) bottom_face_id = _bottom_plane_face(model) bottom_topology = model.face_first_level_topology(bottom_face_id) _assert_points_z( bottom_topology.get("first_level_boundary_vertex_points"), 0.0, "Second-level bottom Face boundary Vertex", ) print(f"model={DEFAULT_MODEL}") print(f"face_id={face_id}") print(f"topology={topology}") print(f"z_values_after_local_move={z_values}") print(f"moved_face_id={moved_face_id}") print(f"moved_boundary_edges={moved_topology.get('first_level_boundary_edge_ids')}") print(result.encode("ascii", "backslashreplace").decode("ascii")) return 0 if __name__ == "__main__": raise SystemExit(main())