Files
pythonocc-step-editor/scripts/verify_first_level_fact_graph.py
T

330 lines
15 KiB
Python
Raw Normal View History

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))
2026-08-10 18:05:18 +08:00
from step_editor.geometry_utils import _int_values
from step_editor.model import StepModel
from step_editor.ui_helpers import INFO_LABELS
from step_editor.window_state import WindowStateMixin
2026-08-10 18:05:18 +08:00
from verify_face_coplanar_push_pull import _top_faces, _write_split_top_box # noqa: E402
from verify_face_mixed_surface_guard import _top_planar_cap_face, _write_hollow_cylinder # noqa: E402
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")
_assert("first_level_planar_relation_summary" in facts, f"{label}: planar relation 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}")
_assert(
facts.get("first_level_planar_relation_status") == "ready",
f"cube planar Face should expose planar relation facts: {facts}",
)
_assert(
int(facts.get("first_level_planar_relation_count", 0) or 0) == 4,
f"cube top Face should have four subject-adjacent planar relations: {facts}",
)
_assert(
int(facts.get("first_level_planar_relation_perpendicular_count", 0) or 0) == 4,
f"cube top Face should have four perpendicular adjacent side Faces: {facts}",
)
_assert(
"一级平面关系" in str(facts.get("first_level_fact_summary") or ""),
f"cube planar Face fact summary should include planar relation facts: {facts}",
)
2026-08-10 18:05:18 +08:00
_assert(facts.get("first_level_same_domain_status") == "ready", f"cube same-domain status: {facts}")
_assert(
facts.get("first_level_same_domain_relation") == "coplanar",
f"cube same-domain relation should be coplanar: {facts}",
)
_assert(
int(facts.get("first_level_same_domain_face_count", 0) or 0) == 1,
f"cube should expose a single same-domain planar Face: {facts}",
)
_assert(
"first_level_coaxial_cylinder_summary" in facts,
f"cube should still expose coaxial-cylinder fact fields: {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")
2026-08-10 18:05:18 +08:00
def _verify_coplanar_same_domain_facts(root: Path) -> None:
split_path = root / "split_top_box.step"
_write_split_top_box(split_path)
model = StepModel.load(split_path)
top_faces = _top_faces(model, 10.0, 2e-4)
_assert(len(top_faces) == 2, f"split-top box should have two coplanar top Faces, got {top_faces}")
face_id = top_faces[0]
facts = model.face_first_level_facts(face_id, scope="face")
_assert_common_facts(facts, "split coplanar Face")
_assert(
facts.get("first_level_same_domain_status") == "ready",
f"split coplanar Face should expose same-domain facts: {facts}",
)
_assert(
facts.get("first_level_same_domain_relation") == "coplanar",
f"split coplanar Face should be marked coplanar: {facts}",
)
_assert(
set(_int_values(facts.get("first_level_same_domain_face_ids"))) == set(top_faces),
f"split coplanar facts should include both top Faces: {facts}",
)
_assert(
int(facts.get("first_level_same_domain_fragment_face_count", 0) or 0) == 1,
f"split coplanar facts should expose one synchronized fragment: {facts}",
)
_assert(
"共面" in str(facts.get("first_level_same_domain_summary") or ""),
f"split coplanar summary should mention coplanar relation: {facts}",
)
plan = model.push_pull_plan(face_id, 1.0)
_assert_plan_facts(plan, "split coplanar push-pull plan", "face")
_assert(
set(_int_values(plan.get("first_level_same_domain_face_ids"))) == set(top_faces),
f"split coplanar plan should carry same-domain facts: {plan}",
)
def _verify_coaxial_cylinder_facts(root: Path) -> None:
hollow_path = root / "hollow_cylinder.step"
_write_hollow_cylinder(hollow_path)
model = StepModel.load(hollow_path)
cap_face_id = _top_planar_cap_face(model)
facts = model.face_first_level_facts(cap_face_id, scope="face")
_assert_common_facts(facts, "hollow cylinder cap")
_assert(
facts.get("first_level_coaxial_cylinder_status") == "ready",
f"hollow cylinder cap should expose ready coaxial-cylinder facts: {facts}",
)
_assert(
int(facts.get("first_level_coaxial_cylinder_count", 0) or 0) >= 1,
f"hollow cylinder cap should have at least one coaxial cylinder relation: {facts}",
)
_assert(
int(facts.get("first_level_coaxial_cylinder_face_count", 0) or 0) >= 2,
f"hollow cylinder cap should include inner and outer cylindrical Faces: {facts}",
)
rows = tuple(row for row in facts.get("first_level_coaxial_cylinder_rows", ()) or () if isinstance(row, dict))
radii = sorted({round(float(radius), 4) for row in rows for radius in tuple(row.get("radii") or ())})
_assert(radii == [3.0, 8.0], f"hollow cylinder cap should expose inner/outer coaxial radii, got {radii}: {facts}")
_assert(
"同轴圆柱" in str(facts.get("first_level_coaxial_cylinder_summary") or ""),
f"hollow cylinder cap summary should mention coaxial cylinders: {facts}",
)
plan = model.push_pull_plan(cap_face_id, 1.0)
_assert_plan_facts(plan, "hollow cylinder cap push-pull plan", "face")
_assert(
plan.get("first_level_coaxial_cylinder_status") == "ready",
f"hollow cylinder cap plan should carry coaxial-cylinder facts: {plan}",
)
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",
2026-08-10 18:05:18 +08:00
"first_level_same_domain_summary",
"first_level_coaxial_cylinder_summary",
):
_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)
2026-08-10 18:05:18 +08:00
_verify_coplanar_same_domain_facts(root)
_verify_coaxial_cylinder_facts(root)
_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())