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())