Files
pythonocc-step-editor/scripts/verify_cylindrical_first_level_topology.py

288 lines
12 KiB
Python

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 _verify_result_check_rejects_degraded_first_level_topology() -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_cyl_topology_result_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)
plan = model.cylindrical_resize_plan(face_id, 8.0)
healthy = model._attach_cylindrical_first_level_result_check( # noqa: SLF001
{"matched": True, "face_id": face_id},
plan,
)
_assert(
healthy.get("first_level_topology_matched") is True,
f"healthy result topology should pass: {healthy}",
)
minimums = dict(healthy.get("first_level_topology_minimums") or {})
_assert(
int(minimums.get("adjacent_face_count", 0) or 0) >= 2,
f"through-hole result guard should derive a direct-neighbor minimum: {healthy}",
)
original_topology = model.cylindrical_feature_first_level_topology
def degraded_topology(target_face_id: int) -> dict[str, object]:
topology = dict(original_topology(target_face_id))
topology["cylindrical_feature_adjacent_face_ids"] = tuple(
tuple(topology.get("cylindrical_feature_adjacent_face_ids", ()) or ())[:1]
)
topology["cylindrical_feature_adjacent_face_count"] = 1
return topology
model.cylindrical_feature_first_level_topology = degraded_topology # type: ignore[method-assign]
degraded = model._attach_cylindrical_first_level_result_check( # noqa: SLF001
{"matched": True, "face_id": face_id},
plan,
)
_assert(
degraded.get("matched") is False,
f"result topology with too few direct adjacent Faces should fail: {degraded}",
)
_assert(
degraded.get("first_level_topology_matched") is False,
f"degraded result should mark first-level topology mismatch: {degraded}",
)
detail = str(degraded.get("detail") or "")
_assert(
"first-level topology" in detail and "adjacent_face_count" in detail,
f"degraded result should explain the weaker first-level topology: {degraded}",
)
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()
_verify_result_check_rejects_degraded_first_level_topology()
print("cylindrical first-level topology verification passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())