feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import math
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||
for path in (PROJECT_ROOT, SCRIPTS_DIR):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from step_editor.model import StepModel
|
||||
|
||||
from verify_hole_resize import ( # noqa: E402
|
||||
_axis_center,
|
||||
_first_hole_face,
|
||||
_write_blind_hole_model,
|
||||
_write_through_hole_model,
|
||||
)
|
||||
from verify_slot_resize import ( # noqa: E402
|
||||
_first_slot_face,
|
||||
_slot_axis_center,
|
||||
_slot_metric,
|
||||
_write_half_round_slot_model,
|
||||
_write_obround_slot_model,
|
||||
)
|
||||
|
||||
|
||||
def _assert(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def _assert_common_topology(topology: dict[str, object], label: str) -> None:
|
||||
_assert(topology.get("topology_relation_depth") == 1, f"{label}: topology depth should be 1")
|
||||
_assert(
|
||||
topology.get("topology_relation_boundary") == "shared-edge",
|
||||
f"{label}: topology boundary should be shared-edge",
|
||||
)
|
||||
ignored = tuple(topology.get("topology_ignored_relation_depths", ()) or ())
|
||||
_assert("second-level" in ignored, f"{label}: second-level propagation should be explicitly ignored")
|
||||
_assert("third-level" in ignored, f"{label}: third-level propagation should be explicitly ignored")
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_side_face_count", 0) or 0) >= 1,
|
||||
f"{label}: selected cylindrical side region is missing",
|
||||
)
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_boundary_edge_count", 0) or 0) >= 1,
|
||||
f"{label}: cylindrical boundary edges are missing",
|
||||
)
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_adjacent_face_count", 0) or 0) >= 1,
|
||||
f"{label}: direct adjacent Faces are missing",
|
||||
)
|
||||
|
||||
|
||||
def _assert_plan_topology(plan: dict[str, object], label: str) -> None:
|
||||
_assert(plan.get("topology_relation_depth") == 1, f"{label}: plan topology depth should be 1")
|
||||
_assert(
|
||||
plan.get("topology_relation_status") == "ready",
|
||||
f"{label}: plan topology should be ready, got {plan.get('topology_relation_status')!r}",
|
||||
)
|
||||
_assert(
|
||||
int(plan.get("first_level_boundary_edge_count", 0) or 0) >= 1,
|
||||
f"{label}: plan boundary edges are missing",
|
||||
)
|
||||
_assert(
|
||||
int(plan.get("first_level_adjacent_face_count", 0) or 0) >= 1,
|
||||
f"{label}: plan adjacent Faces are missing",
|
||||
)
|
||||
_assert(
|
||||
"second-level" in tuple(plan.get("topology_ignored_relation_depths", ()) or ()),
|
||||
f"{label}: plan should document ignored deeper topology",
|
||||
)
|
||||
_assert(
|
||||
plan.get("first_level_topology_status") == "ready",
|
||||
f"{label}: first-level topology guard should be ready",
|
||||
)
|
||||
_assert(
|
||||
plan.get("first_level_topology_guard_note"),
|
||||
f"{label}: first-level topology guard note is missing",
|
||||
)
|
||||
|
||||
|
||||
def _verify_through_hole() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_cyl_topology_hole_") as temp_dir:
|
||||
model_path = Path(temp_dir) / "through_hole.step"
|
||||
_write_through_hole_model(model_path)
|
||||
model = StepModel.load(model_path)
|
||||
face_id = _first_hole_face(model, blind=False)
|
||||
topology = model.cylindrical_feature_first_level_topology(face_id)
|
||||
_assert_common_topology(topology, "through hole")
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_end_face_count", 0) or 0) >= 1,
|
||||
"through hole: opening/end Faces should be part of first-level topology",
|
||||
)
|
||||
|
||||
center = _axis_center(model, face_id)
|
||||
diameter_plan = model.cylindrical_resize_plan(face_id, 8.0)
|
||||
axis_plan = model.cylindrical_axis_move_plan(face_id, (center[0] + 1.0, center[1], center[2]))
|
||||
suppress_plan = model.cylindrical_suppress_plan(face_id)
|
||||
_assert_plan_topology(diameter_plan, "through hole diameter plan")
|
||||
_assert_plan_topology(axis_plan, "through hole axis plan")
|
||||
_assert_plan_topology(suppress_plan, "through hole suppress plan")
|
||||
|
||||
|
||||
def _verify_blind_hole() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_cyl_topology_blind_") as temp_dir:
|
||||
model_path = Path(temp_dir) / "blind_hole.step"
|
||||
_write_blind_hole_model(model_path)
|
||||
model = StepModel.load(model_path)
|
||||
face_id = _first_hole_face(model, blind=True)
|
||||
topology = model.cylindrical_feature_first_level_topology(face_id)
|
||||
_assert_common_topology(topology, "blind hole")
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_bottom_face_count", 0) or 0) >= 1,
|
||||
"blind hole: bottom Face should be part of first-level topology",
|
||||
)
|
||||
|
||||
feature = model.feature_info(face_id)
|
||||
bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ()) or ())
|
||||
bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None
|
||||
depth_plan = model.cylindrical_depth_plan(face_id, 8.0, bottom_face_id=bottom_face_id)
|
||||
_assert_plan_topology(depth_plan, "blind hole depth plan")
|
||||
_assert(
|
||||
depth_plan.get("feature_bottom_face_ids"),
|
||||
"blind hole depth plan should keep bottom Face context",
|
||||
)
|
||||
|
||||
|
||||
def _verify_half_round_slot() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_cyl_topology_slot_") as temp_dir:
|
||||
model_path = Path(temp_dir) / "half_round_slot.step"
|
||||
_write_half_round_slot_model(model_path)
|
||||
model = StepModel.load(model_path)
|
||||
face_id = _first_slot_face(model)
|
||||
topology = model.cylindrical_feature_first_level_topology(face_id)
|
||||
_assert_common_topology(topology, "half-round slot")
|
||||
_assert(
|
||||
topology.get("slot_kind") == "partial-cylindrical-groove",
|
||||
"half-round slot: slot kind should be preserved",
|
||||
)
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_slot_boundary_face_count", 0) or 0) >= 1,
|
||||
"half-round slot: direct slot boundary Faces are missing",
|
||||
)
|
||||
|
||||
center = _slot_axis_center(model, face_id)
|
||||
width = _slot_metric(model, face_id, "slot_chord_width_estimate")
|
||||
depth = _slot_metric(model, face_id, "slot_sagitta_depth_estimate")
|
||||
arc_length = _slot_metric(model, face_id, "slot_arc_length_estimate")
|
||||
angular_span = _slot_metric(model, face_id, "slot_angular_span")
|
||||
plans = (
|
||||
("slot width plan", model.cylindrical_slot_resize_plan(face_id, width + 1.0, "width")),
|
||||
("slot depth plan", model.cylindrical_slot_resize_plan(face_id, depth + 0.5, "depth")),
|
||||
("slot arc plan", model.cylindrical_slot_resize_plan(face_id, arc_length + 0.5, "arc_length")),
|
||||
("slot angular plan", model.cylindrical_slot_angular_span_plan(face_id, max(0.25, angular_span * 0.8))),
|
||||
("slot axis plan", model.cylindrical_slot_axis_move_plan(face_id, (center[0], center[1] + 0.5, center[2]))),
|
||||
)
|
||||
for label, plan in plans:
|
||||
_assert_plan_topology(plan, label)
|
||||
|
||||
|
||||
def _verify_obround_slot_length_plan() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_cyl_topology_obround_") as temp_dir:
|
||||
model_path = Path(temp_dir) / "obround_slot.step"
|
||||
_write_obround_slot_model(model_path)
|
||||
model = StepModel.load(model_path)
|
||||
face_id = _first_slot_face(model)
|
||||
topology = model.cylindrical_feature_first_level_topology(face_id)
|
||||
_assert_common_topology(topology, "obround slot end")
|
||||
_assert(
|
||||
int(topology.get("cylindrical_feature_adjacent_face_count", 0) or 0) >= 2,
|
||||
"obround slot end: direct side-wall neighbors are missing",
|
||||
)
|
||||
|
||||
length_plan = model.cylindrical_slot_total_length_plan(face_id, 20.0)
|
||||
center_plan = model.cylindrical_slot_center_distance_plan(face_id, 14.0)
|
||||
_assert_plan_topology(length_plan, "obround slot total length plan")
|
||||
_assert_plan_topology(center_plan, "obround slot center distance plan")
|
||||
_assert(
|
||||
length_plan.get("slot_pair_face_id") not in {None, ""},
|
||||
"obround slot length plan should still expose the paired slot end",
|
||||
)
|
||||
|
||||
|
||||
def _verify_missing_first_level_neighbor_blocks_plan() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_cyl_topology_guard_") as temp_dir:
|
||||
model_path = Path(temp_dir) / "through_hole.step"
|
||||
_write_through_hole_model(model_path)
|
||||
model = StepModel.load(model_path)
|
||||
face_id = _first_hole_face(model, blind=False)
|
||||
original_topology = model.cylindrical_feature_first_level_topology
|
||||
|
||||
def broken_topology(target_face_id: int) -> dict[str, object]:
|
||||
topology = dict(original_topology(target_face_id))
|
||||
topology["cylindrical_feature_adjacent_face_ids"] = ()
|
||||
topology["cylindrical_feature_adjacent_face_count"] = 0
|
||||
topology["cylindrical_feature_first_level_face_ids"] = topology.get(
|
||||
"cylindrical_feature_side_face_ids",
|
||||
(target_face_id,),
|
||||
)
|
||||
topology["cylindrical_feature_first_level_face_count"] = int(
|
||||
topology.get("cylindrical_feature_side_face_count", 1) or 1
|
||||
)
|
||||
return topology
|
||||
|
||||
model.cylindrical_feature_first_level_topology = broken_topology # type: ignore[method-assign]
|
||||
plan = model.cylindrical_resize_plan(face_id, 8.0)
|
||||
_assert(plan.get("status") == "blocked", "missing first-level adjacent Faces should block diameter plan")
|
||||
_assert(
|
||||
plan.get("first_level_topology_status") == "blocked",
|
||||
"missing first-level adjacent Faces should mark topology guard blocked",
|
||||
)
|
||||
_assert(
|
||||
"相邻 Face" in str(plan.get("blockers") or plan.get("message") or ""),
|
||||
"blocked plan should explain the missing direct adjacent Faces",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_verify_through_hole()
|
||||
_verify_blind_hole()
|
||||
_verify_half_round_slot()
|
||||
_verify_obround_slot_length_plan()
|
||||
_verify_missing_first_level_neighbor_blocks_plan()
|
||||
print("cylindrical first-level topology verification passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user