feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user