2026-08-04 09:35:39 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
|
|
|
|
|
2026-08-04 18:15:29 +08:00
|
|
|
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
|
|
|
|
|
|
2026-08-04 09:35:39 +08:00
|
|
|
|
|
|
|
|
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:
|
2026-08-04 18:15:29 +08:00
|
|
|
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:
|
2026-08-04 09:35:39 +08:00
|
|
|
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)
|
2026-08-04 18:15:29 +08:00
|
|
|
if best is None or (center[2] > best[0] if highest else center[2] < best[0]):
|
2026-08-04 09:35:39 +08:00
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
2026-08-04 18:15:29 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-08-04 09:35:39 +08:00
|
|
|
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)
|
2026-08-10 16:51:52 +08:00
|
|
|
topology_row = _spec(editable_specs, "face_first_level_topology")
|
2026-08-04 09:35:39 +08:00
|
|
|
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}")
|
2026-08-10 16:51:52 +08:00
|
|
|
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}"
|
|
|
|
|
)
|
2026-08-04 09:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
2026-08-04 18:15:29 +08:00
|
|
|
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",
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-04 09:35:39 +08:00
|
|
|
print(f"model={DEFAULT_MODEL}")
|
|
|
|
|
print(f"face_id={face_id}")
|
|
|
|
|
print(f"topology={topology}")
|
|
|
|
|
print(f"z_values_after_local_move={z_values}")
|
2026-08-04 18:15:29 +08:00
|
|
|
print(f"moved_face_id={moved_face_id}")
|
|
|
|
|
print(f"moved_boundary_edges={moved_topology.get('first_level_boundary_edge_ids')}")
|
2026-08-04 09:35:39 +08:00
|
|
|
print(result.encode("ascii", "backslashreplace").decode("ascii"))
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|