feat: 完善 STEP/B-Rep 一级关系参数化编辑
This commit is contained in:
@@ -16,8 +16,10 @@ 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_hole_resize import _first_hole_face, _write_blind_hole_model, _write_through_hole_model # noqa: E402
|
||||
from verify_slot_resize import _first_slot_face, _write_half_round_slot_model # noqa: E402
|
||||
from verify_boss_resize import _first_boss_face, _write_boss_model # noqa: E402
|
||||
from verify_ellipse_edge_resize import _write_ellipse_face_model # noqa: E402
|
||||
|
||||
|
||||
def _assert(condition: bool, message: str) -> None:
|
||||
@@ -57,6 +59,19 @@ def _first_surface_face(model: StepModel, surface: str) -> int:
|
||||
raise AssertionError(f"no {surface} Face was found")
|
||||
|
||||
|
||||
def _first_quick_candidate(
|
||||
model: StepModel,
|
||||
*,
|
||||
feature_guess: str,
|
||||
feature_type: str,
|
||||
) -> tuple[int, dict[str, object]]:
|
||||
for face_id in range(len(model.faces)):
|
||||
info = model.quick_face_info(face_id)
|
||||
if info.get("feature_guess") == feature_guess and info.get("feature_type") == feature_type:
|
||||
return face_id, info
|
||||
raise AssertionError(f"no quick {feature_type} 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 "")
|
||||
@@ -66,12 +81,18 @@ def _assert_summary(info: dict[str, object], label: str, required_keys: set[str]
|
||||
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))
|
||||
user_priority = int(info.get("recognition_user_priority", -1))
|
||||
user_priority_label = str(info.get("recognition_user_priority_label") or "")
|
||||
user_priority_reason = str(info.get("recognition_user_priority_reason") or "")
|
||||
|
||||
_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(1 <= user_priority <= 99, f"{label}: bad user priority {user_priority!r}")
|
||||
_assert(user_priority_label, f"{label}: recognition_user_priority_label is missing")
|
||||
_assert(user_priority_reason, f"{label}: recognition_user_priority_reason is missing")
|
||||
_assert(
|
||||
decision in {"高可信候选", "可尝试候选", "需人工确认", "不建议自动修改", "已阻止"},
|
||||
f"{label}: bad recognition_decision {decision!r}",
|
||||
@@ -79,6 +100,7 @@ def _assert_summary(info: dict[str, object], label: str, required_keys: set[str]
|
||||
_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("优先级=" in summary, f"{label}: summary should mention user priority: {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}")
|
||||
|
||||
@@ -92,6 +114,7 @@ def _verify_planar_summary(root: Path) -> None:
|
||||
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"})
|
||||
_assert(int(full_info.get("recognition_user_priority", 99)) == 10, f"planar Face should be first priority: {full_info}")
|
||||
|
||||
|
||||
def _verify_hole_summary(root: Path) -> None:
|
||||
@@ -105,6 +128,7 @@ def _verify_hole_summary(root: Path) -> None:
|
||||
_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(int(info.get("recognition_user_priority", 99)) == 20, f"hole should use hole priority: {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 "")
|
||||
@@ -116,6 +140,64 @@ def _verify_hole_summary(root: Path) -> None:
|
||||
_assert("可改:" in str(info.get("recognition_summary") or ""), f"hole summary should show ready actions: {info}")
|
||||
|
||||
|
||||
def _verify_quick_cylinder_recognition(root: Path) -> None:
|
||||
hole_path = root / "quick_through_hole.step"
|
||||
_write_through_hole_model(hole_path)
|
||||
hole_model = StepModel.load(hole_path)
|
||||
_hole_face_id, hole_info = _first_quick_candidate(
|
||||
hole_model,
|
||||
feature_guess="hole/groove candidate",
|
||||
feature_type="圆柱孔候选",
|
||||
)
|
||||
_assert_summary(hole_info, "quick through-hole feature", {"surface", "user_operation_priority"})
|
||||
_assert(int(hole_info.get("recognition_user_priority", 99)) == 20, f"quick hole should use hole priority: {hole_info}")
|
||||
_assert("孔/槽/圆柱直径" in str(hole_info.get("recognition_ready_actions") or ""), f"quick hole should expose diameter: {hole_info}")
|
||||
|
||||
blind_path = root / "quick_blind_hole_cached.step"
|
||||
_write_blind_hole_model(blind_path)
|
||||
blind_model = StepModel.load(blind_path)
|
||||
blind_face_id = _first_hole_face(blind_model, blind=True)
|
||||
cached_quick = blind_model.quick_face_info(blind_face_id)
|
||||
_assert(
|
||||
cached_quick.get("feature_type") == "圆柱孔候选",
|
||||
f"quick hole should keep its feature label after face_info cache is populated: {cached_quick}",
|
||||
)
|
||||
_assert(cached_quick.get("cylinder_end_type") == "blind", f"quick blind hole should expose end type: {cached_quick}")
|
||||
_assert(cached_quick.get("depth_status") == "ready", f"quick blind hole depth should be ready: {cached_quick}")
|
||||
_assert(
|
||||
"盲孔/盲槽深度" in str(cached_quick.get("recognition_ready_actions") or ""),
|
||||
f"quick blind hole should expose depth in ready actions: {cached_quick}",
|
||||
)
|
||||
|
||||
slot_path = root / "quick_half_round_slot.step"
|
||||
_write_half_round_slot_model(slot_path)
|
||||
slot_model = StepModel.load(slot_path)
|
||||
_slot_face_id, slot_info = _first_quick_candidate(
|
||||
slot_model,
|
||||
feature_guess="hole/groove candidate",
|
||||
feature_type="槽/半孔候选",
|
||||
)
|
||||
_assert_summary(slot_info, "quick half-round slot feature", {"surface", "slot_geometry", "user_operation_priority"})
|
||||
_assert(int(slot_info.get("recognition_user_priority", 99)) == 30, f"quick slot should use slot priority: {slot_info}")
|
||||
_assert(slot_info.get("slot_status") == "candidate", f"quick slot should expose slot candidate fields: {slot_info}")
|
||||
|
||||
boss_path = root / "quick_boss.step"
|
||||
_write_boss_model(boss_path)
|
||||
boss_model = StepModel.load(boss_path)
|
||||
_boss_face_id, boss_info = _first_quick_candidate(
|
||||
boss_model,
|
||||
feature_guess="boss/outer-round candidate",
|
||||
feature_type="凸台/外圆候选",
|
||||
)
|
||||
_assert_summary(boss_info, "quick boss feature", {"surface", "user_operation_priority"})
|
||||
_assert(int(boss_info.get("recognition_user_priority", 99)) == 40, f"quick boss should use boss priority: {boss_info}")
|
||||
ready_actions = str(boss_info.get("recognition_ready_actions") or "")
|
||||
limited_actions = str(boss_info.get("recognition_limited_actions") or "")
|
||||
_assert("圆柱凸台直径/高度/轴心" in ready_actions, f"quick boss should expose boss editing: {boss_info}")
|
||||
_assert("孔/槽/圆柱直径" not in ready_actions, f"quick boss should not be exposed as hole resize: {boss_info}")
|
||||
_assert("孔/槽/圆柱直径" not in limited_actions, f"quick boss should not show cross-feature hole limits: {boss_info}")
|
||||
|
||||
|
||||
def _verify_holed_planar_summary(root: Path) -> None:
|
||||
path = root / "holed_plate.step"
|
||||
_write_through_hole_model(path)
|
||||
@@ -144,6 +226,18 @@ def _verify_slot_summary(root: Path) -> None:
|
||||
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"})
|
||||
_assert(int(info.get("recognition_user_priority", 99)) == 30, f"slot should use slot priority: {info}")
|
||||
|
||||
|
||||
def _verify_boss_summary(root: Path) -> None:
|
||||
path = root / "boss.step"
|
||||
_write_boss_model(path)
|
||||
model = StepModel.load(path)
|
||||
face_id = _first_boss_face(model)
|
||||
info = model.feature_info(face_id)
|
||||
_assert(info.get("feature_guess") == "boss/outer-round candidate", f"boss was not recognized: {info}")
|
||||
_assert_summary(info, "boss feature", {"surface", "material_votes", "first_level_topology"})
|
||||
_assert(int(info.get("recognition_user_priority", 99)) == 40, f"boss should use boss priority: {info}")
|
||||
|
||||
|
||||
def _verify_torus_summary(root: Path) -> None:
|
||||
@@ -155,6 +249,43 @@ def _verify_torus_summary(root: Path) -> None:
|
||||
_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"})
|
||||
_assert(int(info.get("recognition_user_priority", 0)) == 80, f"torus should be lower-priority analytic surface: {info}")
|
||||
|
||||
|
||||
def _verify_user_priority_scan_order(root: Path) -> None:
|
||||
path = root / "through_hole_scan.step"
|
||||
_write_through_hole_model(path)
|
||||
model = StepModel.load(path)
|
||||
candidates = model.editable_feature_candidates(limit=12, detailed=False)
|
||||
operations = tuple(str(item.get("operation_key")) for item in candidates)
|
||||
_assert(operations, "editable feature scan returned no candidates")
|
||||
_assert(operations[0] == "push_pull_plane", f"Face push/pull should be first in common-user scan order: {operations}")
|
||||
if "resize_cylinder" in operations:
|
||||
_assert(
|
||||
operations.index("push_pull_plane") < operations.index("resize_cylinder"),
|
||||
f"Face push/pull should rank before hole diameter: {operations}",
|
||||
)
|
||||
|
||||
|
||||
def _verify_ellipse_edge_scan_entries(root: Path) -> None:
|
||||
path = root / "ellipse_edge_scan.step"
|
||||
_write_ellipse_face_model(path)
|
||||
model = StepModel.load(path)
|
||||
candidates = model.editable_feature_candidates(limit=20, detailed=False)
|
||||
operations = tuple(str(item.get("operation_key")) for item in candidates)
|
||||
labels = {str(item.get("current_value_label")) for item in candidates}
|
||||
_assert(
|
||||
"resize_ellipse_edge_major_radius" in operations,
|
||||
f"ellipse Edge scan should expose major radius instead of generic length: {operations}",
|
||||
)
|
||||
_assert(
|
||||
"resize_ellipse_edge_minor_radius" in operations,
|
||||
f"ellipse Edge scan should expose minor radius instead of generic length: {operations}",
|
||||
)
|
||||
_assert("major_radius" in labels and "minor_radius" in labels, f"ellipse Edge scan labels are unclear: {labels}")
|
||||
for item in candidates:
|
||||
if item.get("operation_key") == "resize_edge_length" and item.get("current_value_label") == "length":
|
||||
raise AssertionError(f"ellipse Edge scan should not expose generic length editing: {item}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -165,6 +296,9 @@ def main() -> int:
|
||||
"recognition_risk",
|
||||
"recognition_score",
|
||||
"recognition_decision",
|
||||
"recognition_user_priority",
|
||||
"recognition_user_priority_label",
|
||||
"recognition_user_priority_reason",
|
||||
"recognition_evidence",
|
||||
"recognition_ready_actions",
|
||||
"recognition_limited_actions",
|
||||
@@ -177,9 +311,13 @@ def main() -> int:
|
||||
root = Path(temp_dir)
|
||||
_verify_planar_summary(root)
|
||||
_verify_holed_planar_summary(root)
|
||||
_verify_quick_cylinder_recognition(root)
|
||||
_verify_hole_summary(root)
|
||||
_verify_slot_summary(root)
|
||||
_verify_boss_summary(root)
|
||||
_verify_torus_summary(root)
|
||||
_verify_user_priority_scan_order(root)
|
||||
_verify_ellipse_edge_scan_entries(root)
|
||||
|
||||
print("feature recognition summary ok")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user