from __future__ import annotations import sys import tempfile from pathlib import Path 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 step_editor.ui_helpers import INFO_LABELS from step_editor.window_state import WindowStateMixin from verify_hole_resize import _first_hole_face, _write_through_hole_model # noqa: E402 from verify_shell_thickness_resize import _first_shell_face, _write_plate_model # noqa: E402 from verify_slot_resize import _first_slot_face, _write_half_round_slot_model # noqa: E402 DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" class _FactProbe(WindowStateMixin): def __init__(self, model: StepModel) -> None: self.model = model def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) 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 AssertionError(f"Face has no stable center: {info}") return float(value[0]), float(value[1]), float(value[2]) def _number(value: object, default: float) -> float: try: return float(value) except (TypeError, ValueError): return default 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 AssertionError("No planar Face found.") return best[1] def _assert_common_facts(facts: dict[str, object], label: str) -> None: _assert(facts.get("first_level_fact_model") == "STEP/B-Rep first-level fact graph", f"{label}: bad model") _assert(facts.get("first_level_fact_status") == "ready", f"{label}: bad status {facts}") _assert(facts.get("first_level_fact_relation_depth") == 1, f"{label}: bad relation depth") _assert(facts.get("first_level_fact_relation_boundary") == "shared-edge", f"{label}: bad boundary") _assert( int(facts.get("first_level_fact_subject_face_count", 0) or 0) >= 1, f"{label}: missing subject Faces", ) _assert( int(facts.get("first_level_fact_boundary_edge_count", 0) or 0) >= 1, f"{label}: missing boundary Edges", ) _assert( int(facts.get("first_level_fact_adjacent_face_count", 0) or 0) >= 1, f"{label}: missing first-level adjacent Faces", ) ignored = tuple(facts.get("first_level_fact_ignored_relation_depths", ()) or ()) _assert("second-level" in ignored and "third-level" in ignored, f"{label}: ignored depths are missing") _assert(str(facts.get("first_level_fact_summary") or ""), f"{label}: summary is missing") def _assert_plan_facts(plan: dict[str, object], label: str, scope: str) -> None: _assert_common_facts(plan, label) _assert(plan.get("first_level_fact_scope") == scope, f"{label}: wrong fact scope {plan}") _assert( int(plan.get("first_level_fact_included_face_count", 0) or 0) >= int(plan.get("first_level_fact_subject_face_count", 0) or 0), f"{label}: included facts should cover subject Faces", ) def _verify_planar_face_facts() -> None: model = StepModel.load(DEFAULT_MODEL) face_id = _top_plane_face(model) info = model.face_info(face_id) facts = model.face_first_level_facts(face_id, scope="face") _assert_common_facts(facts, "cube planar Face") _assert(facts.get("first_level_fact_scope") == "face", f"cube planar Face: wrong scope {facts}") _assert(int(facts.get("first_level_fact_subject_face_count", 0) or 0) == 1, f"cube subject count: {facts}") _assert(int(facts.get("first_level_fact_boundary_edge_count", 0) or 0) == 4, f"cube boundary count: {facts}") _assert(int(facts.get("first_level_fact_boundary_vertex_count", 0) or 0) == 4, f"cube vertex count: {facts}") _assert(int(facts.get("first_level_fact_adjacent_face_count", 0) or 0) == 4, f"cube adjacent count: {facts}") _assert(int(facts.get("first_level_fact_included_face_count", 0) or 0) == 5, f"cube included count: {facts}") probe = _FactProbe(model) fields = probe._face_first_level_selection_fields(face_id) _assert(fields.get("first_level_fact_summary"), f"selection fields should expose fact summary: {fields}") enriched = model.quick_face_info(face_id) enriched.update(fields) enriched.update(model._recognition_summary_fields(enriched)) # noqa: SLF001 _assert("一级事实=" in str(enriched.get("recognition_summary") or ""), f"summary missed facts: {enriched}") center = _center(info) area = _number(info.get("area"), 100.0) width = _number(info.get("local_face_width"), 10.0) plans = ( ( model.face_center_local_move_plan(face_id, (center[0], center[1], center[2] + 2.0)), "cube Face center local move plan", ), ( model.face_center_owning_translation_plan(face_id, (center[0], center[1], center[2] + 2.0)), "cube Face center owning translation plan", ), (model.face_area_local_resize_plan(face_id, area * 1.2), "cube Face area local resize plan"), (model.face_area_scale_plan(face_id, area * 1.2), "cube Face area owning scale plan"), (model.face_size_local_resize_plan(face_id, width * 1.2, "width"), "cube Face width local resize plan"), (model.face_size_owning_scale_plan(face_id, width * 1.2, "width"), "cube Face width owning scale plan"), (model.face_plane_offset_local_plan(face_id, 2.0), "cube Face plane offset local plan"), (model.face_plane_offset_owning_translation_plan(face_id, 2.0), "cube Face plane offset owning plan"), (model.push_pull_plan(face_id, 2.0), "cube Face push-pull plan"), ) for plan, label in plans: _assert_plan_facts(plan, label, "face") def _verify_shell_plan_facts(root: Path) -> None: shell_path = root / "thin_plate.step" _write_plate_model(shell_path) model = StepModel.load(shell_path) face_id = _first_shell_face(model, 2.0, 2e-4) plans = ( (model.shell_thickness_plan(face_id, 3.0), "thin wall thickness local plan"), (model.shell_thickness_owning_scale_plan(face_id, 3.0), "thin wall thickness owning plan"), ) for plan, label in plans: _assert_plan_facts(plan, label, "face") def _verify_cylindrical_facts(root: Path) -> None: hole_path = root / "through_hole.step" _write_through_hole_model(hole_path) hole_model = StepModel.load(hole_path) hole_face_id = _first_hole_face(hole_model, blind=False) hole_facts = hole_model.face_first_level_facts(hole_face_id, scope="cylindrical-feature") _assert_common_facts(hole_facts, "through-hole cylinder") _assert( hole_facts.get("first_level_fact_scope") == "cylindrical-feature", f"through-hole cylinder: wrong scope {hole_facts}", ) role_groups = tuple(hole_facts.get("first_level_fact_role_groups") or ()) _assert(any(isinstance(item, dict) and item.get("role") == "cylindrical-side" for item in role_groups), role_groups) _assert(any(isinstance(item, dict) and item.get("role") == "end/opening" for item in role_groups), role_groups) probe = _FactProbe(hole_model) fields = probe._cylindrical_first_level_selection_fields(hole_face_id) enriched = hole_model.feature_info(hole_face_id) enriched.update(fields) enriched.update(hole_model._recognition_summary_fields(enriched)) # noqa: SLF001 _assert("一级事实=" in str(enriched.get("recognition_summary") or ""), f"hole summary missed facts: {enriched}") diameter = _number(hole_model.face_info(hole_face_id).get("diameter"), 6.0) plan = hole_model.cylindrical_resize_plan(hole_face_id, diameter * 1.2) _assert_plan_facts(plan, "through-hole diameter resize plan", "cylindrical-feature") def _verify_slot_facts(root: Path) -> None: slot_path = root / "half_round_slot.step" _write_half_round_slot_model(slot_path) model = StepModel.load(slot_path) face_id = _first_slot_face(model) facts = model.face_first_level_facts(face_id, scope="cylindrical-feature") _assert_common_facts(facts, "half-round slot") role_groups = tuple(facts.get("first_level_fact_role_groups") or ()) _assert(any(isinstance(item, dict) and item.get("role") == "slot-boundary" for item in role_groups), role_groups) feature = model.feature_info(face_id) width = _number(feature.get("slot_width"), _number(model.face_info(face_id).get("diameter"), 6.0)) plan = model.cylindrical_slot_resize_plan(face_id, width * 1.1, "width") _assert_plan_facts(plan, "half-round slot width resize plan", "cylindrical-feature") def main() -> int: for key in ( "first_level_fact_model", "first_level_fact_status", "first_level_fact_summary", "first_level_fact_subject_face_count", "first_level_fact_adjacent_face_count", ): _assert(key in INFO_LABELS, f"{key} should have a user-facing label") _verify_planar_face_facts() with tempfile.TemporaryDirectory(prefix="geom_param_first_level_facts_") as temp_dir: root = Path(temp_dir) _verify_shell_plan_facts(root) _verify_cylindrical_facts(root) _verify_slot_facts(root) print("first-level fact graph ok") return 0 if __name__ == "__main__": raise SystemExit(main())