Files
pythonocc-step-editor/scripts/verify_face_first_level_topology.py
T

162 lines
7.5 KiB
Python

from __future__ import annotations
from pathlib import Path
import sys
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:
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]:
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 _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)
feature_rows = probe._feature_property_specs(editable_specs, feature_info)
topology_row = _spec(feature_rows, "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}")
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")
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(result.encode("ascii", "backslashreplace").decode("ascii"))
return 0
if __name__ == "__main__":
raise SystemExit(main())