2026-08-04 18:15:29 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeTorus
|
|
|
|
|
|
|
|
|
|
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.step_io import _write_step
|
|
|
|
|
from step_editor.ui_helpers import INFO_LABELS
|
|
|
|
|
|
|
|
|
|
from verify_hole_resize import _first_hole_face, _write_through_hole_model # noqa: E402
|
|
|
|
|
from verify_slot_resize import _first_slot_face, _write_half_round_slot_model # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assert(condition: bool, message: str) -> None:
|
|
|
|
|
if not condition:
|
|
|
|
|
raise AssertionError(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _top_planar_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 = info.get("area_center")
|
|
|
|
|
if not isinstance(center, tuple) or len(center) != 3:
|
|
|
|
|
continue
|
|
|
|
|
z_value = float(center[2])
|
|
|
|
|
if best is None or z_value > best[0]:
|
|
|
|
|
best = (z_value, face_id)
|
|
|
|
|
if best is None:
|
|
|
|
|
raise AssertionError("no planar Face was found")
|
|
|
|
|
return best[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _first_planar_face_with_inner_boundary(model: StepModel) -> int:
|
|
|
|
|
for face_id in range(len(model.faces)):
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
if info.get("surface") == "plane" and int(info.get("inner_boundary_wires", 0) or 0) > 0:
|
|
|
|
|
return face_id
|
|
|
|
|
raise AssertionError("no planar Face with an inner boundary was found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _first_surface_face(model: StepModel, surface: str) -> int:
|
|
|
|
|
for face_id in range(len(model.faces)):
|
|
|
|
|
if model.face_info(face_id).get("surface") == surface:
|
|
|
|
|
return face_id
|
|
|
|
|
raise AssertionError(f"no {surface} Face was found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assert_summary(info: dict[str, object], label: str, required_keys: set[str]) -> None:
|
|
|
|
|
summary = str(info.get("recognition_summary") or "")
|
|
|
|
|
candidate = str(info.get("recognition_candidate") or "")
|
|
|
|
|
confidence = str(info.get("recognition_confidence") or "")
|
|
|
|
|
risk = str(info.get("recognition_risk") or "")
|
|
|
|
|
decision = str(info.get("recognition_decision") or "")
|
|
|
|
|
evidence_keys = set(str(item) for item in tuple(info.get("recognition_evidence_keys") or ()))
|
|
|
|
|
evidence = str(info.get("recognition_evidence") or "")
|
|
|
|
|
score = int(info.get("recognition_score", -1))
|
|
|
|
|
|
|
|
|
|
_assert(summary, f"{label}: recognition_summary is missing")
|
|
|
|
|
_assert(candidate, f"{label}: recognition_candidate is missing")
|
|
|
|
|
_assert(confidence in {"unchecked", "none", "low", "medium", "high"}, f"{label}: bad confidence {confidence!r}")
|
|
|
|
|
_assert(risk in {"low", "medium", "high", "blocked"}, f"{label}: bad risk {risk!r}")
|
|
|
|
|
_assert(0 <= score <= 100, f"{label}: bad recognition_score {score!r}")
|
|
|
|
|
_assert(
|
|
|
|
|
decision in {"高可信候选", "可尝试候选", "需人工确认", "不建议自动修改", "已阻止"},
|
|
|
|
|
f"{label}: bad recognition_decision {decision!r}",
|
|
|
|
|
)
|
|
|
|
|
_assert(required_keys <= evidence_keys, f"{label}: evidence keys missing {required_keys - evidence_keys}")
|
|
|
|
|
_assert(evidence, f"{label}: recognition_evidence is missing")
|
|
|
|
|
_assert(candidate in summary, f"{label}: summary should mention candidate {candidate!r}: {summary!r}")
|
|
|
|
|
_assert(f"评分={score}" in summary, f"{label}: summary should mention score {score}: {summary!r}")
|
|
|
|
|
_assert(decision in summary, f"{label}: summary should mention decision {decision!r}: {summary!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verify_planar_summary(root: Path) -> None:
|
|
|
|
|
path = root / "box.step"
|
|
|
|
|
_write_step(BRepPrimAPI_MakeBox(10.0, 8.0, 6.0).Shape(), path)
|
|
|
|
|
model = StepModel.load(path)
|
|
|
|
|
face_id = _top_planar_face(model)
|
|
|
|
|
quick_info = model.quick_face_info(face_id)
|
|
|
|
|
full_info = model.feature_info(face_id)
|
|
|
|
|
_assert_summary(quick_info, "quick planar Face", {"surface", "boundary_edges"})
|
|
|
|
|
_assert_summary(full_info, "feature planar Face", {"surface", "boundary_edges", "first_level_topology"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verify_hole_summary(root: Path) -> None:
|
|
|
|
|
path = root / "through_hole.step"
|
|
|
|
|
_write_through_hole_model(path)
|
|
|
|
|
model = StepModel.load(path)
|
|
|
|
|
face_id = _first_hole_face(model, blind=False)
|
|
|
|
|
info = model.feature_info(face_id)
|
|
|
|
|
_assert(info.get("feature_guess") == "hole/groove candidate", f"hole was not recognized: {info}")
|
|
|
|
|
_assert_summary(info, "through-hole feature", {"surface", "material_votes", "first_level_topology"})
|
|
|
|
|
_assert(info.get("resize_status") == "ready", f"hole diameter resize should be ready: {info}")
|
|
|
|
|
_assert(info.get("recognition_risk") != "blocked", f"ready hole candidate should not be globally blocked: {info}")
|
|
|
|
|
_assert(info.get("recognition_decision") != "已阻止", f"ready hole candidate should not be marked blocked: {info}")
|
|
|
|
|
_assert(not str(info.get("recognition_blockers") or ""), f"ready hole blockers should stay empty: {info}")
|
|
|
|
|
ready_actions = str(info.get("recognition_ready_actions") or "")
|
|
|
|
|
limited_actions = str(info.get("recognition_limited_actions") or "")
|
|
|
|
|
_assert("孔/槽/圆柱直径" in ready_actions, f"hole ready actions should include diameter resize: {info}")
|
|
|
|
|
_assert("盲孔/盲槽深度" in limited_actions, f"through-hole limited actions should include blind depth: {info}")
|
|
|
|
|
limitations = str(info.get("recognition_limitations") or "")
|
|
|
|
|
_assert(limitations, f"ready hole should still explain unsupported sub-capabilities: {info}")
|
|
|
|
|
_assert("受限能力" in str(info.get("recognition_summary") or ""), f"hole summary should show limitations: {info}")
|
|
|
|
|
_assert("可改:" in str(info.get("recognition_summary") or ""), f"hole summary should show ready actions: {info}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verify_holed_planar_summary(root: Path) -> None:
|
|
|
|
|
path = root / "holed_plate.step"
|
|
|
|
|
_write_through_hole_model(path)
|
|
|
|
|
model = StepModel.load(path)
|
|
|
|
|
face_id = _first_planar_face_with_inner_boundary(model)
|
|
|
|
|
info = model.feature_info(face_id)
|
|
|
|
|
_assert_summary(info, "holed planar Face", {"surface", "boundary_edges", "first_level_topology"})
|
|
|
|
|
_assert(info.get("local_face_deform_ready") is False, f"holed Face should block local deformation: {info}")
|
|
|
|
|
_assert(info.get("recognition_risk") != "blocked", f"holed Face should keep push/pull available: {info}")
|
|
|
|
|
_assert(info.get("recognition_decision") != "已阻止", f"holed Face should not be globally blocked: {info}")
|
|
|
|
|
_assert(not str(info.get("recognition_blockers") or ""), f"holed Face blockers should stay empty: {info}")
|
|
|
|
|
ready_actions = str(info.get("recognition_ready_actions") or "")
|
|
|
|
|
limited_actions = str(info.get("recognition_limited_actions") or "")
|
2026-08-05 15:08:16 +08:00
|
|
|
_assert("平面拉伸/切除" in ready_actions, f"holed Face should expose push/pull as ready: {info}")
|
2026-08-04 18:15:29 +08:00
|
|
|
_assert(
|
2026-08-05 15:08:16 +08:00
|
|
|
"局部重建尺寸/中心/偏移" in limited_actions,
|
2026-08-04 18:15:29 +08:00
|
|
|
f"holed Face should expose local deformation as limited: {info}",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verify_slot_summary(root: Path) -> None:
|
|
|
|
|
path = root / "half_round_slot.step"
|
|
|
|
|
_write_half_round_slot_model(path)
|
|
|
|
|
model = StepModel.load(path)
|
|
|
|
|
face_id = _first_slot_face(model)
|
|
|
|
|
info = model.feature_info(face_id)
|
|
|
|
|
_assert(info.get("slot_status") == "candidate", f"slot was not recognized: {info}")
|
|
|
|
|
_assert_summary(info, "half-round slot feature", {"surface", "material_votes", "slot_geometry"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verify_torus_summary(root: Path) -> None:
|
|
|
|
|
path = root / "torus.step"
|
|
|
|
|
_write_step(BRepPrimAPI_MakeTorus(12.0, 2.0).Shape(), path)
|
|
|
|
|
model = StepModel.load(path)
|
|
|
|
|
face_id = _first_surface_face(model, "torus")
|
|
|
|
|
info = model.feature_info(face_id)
|
|
|
|
|
_assert(info.get("feature_type") == "环面候选", f"torus was not recognized safely: {info}")
|
|
|
|
|
_assert(info.get("feature_highlight_face_ids") == (face_id,), f"torus highlight should stay on source Face: {info}")
|
|
|
|
|
_assert_summary(info, "torus feature", {"surface", "boundary_edges"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
for key in (
|
|
|
|
|
"recognition_summary",
|
|
|
|
|
"recognition_candidate",
|
|
|
|
|
"recognition_confidence",
|
|
|
|
|
"recognition_risk",
|
|
|
|
|
"recognition_score",
|
|
|
|
|
"recognition_decision",
|
|
|
|
|
"recognition_evidence",
|
|
|
|
|
"recognition_ready_actions",
|
|
|
|
|
"recognition_limited_actions",
|
|
|
|
|
"recognition_blockers",
|
|
|
|
|
"recognition_limitations",
|
|
|
|
|
):
|
|
|
|
|
_assert(key in INFO_LABELS, f"{key} should have a user-facing label")
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="geom_param_recognition_summary_") as temp_dir:
|
|
|
|
|
root = Path(temp_dir)
|
|
|
|
|
_verify_planar_summary(root)
|
|
|
|
|
_verify_holed_planar_summary(root)
|
|
|
|
|
_verify_hole_summary(root)
|
|
|
|
|
_verify_slot_summary(root)
|
|
|
|
|
_verify_torus_summary(root)
|
|
|
|
|
|
|
|
|
|
print("feature recognition summary ok")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|