feat: 完善 STEP 一级参数化编辑识别与关系式建模
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.asitus_bridge import (
|
||||
default_asitus_recognize_holes_path,
|
||||
parse_asitus_hole_groups,
|
||||
parse_asitus_probe_payload,
|
||||
run_asitus_hole_recognition,
|
||||
)
|
||||
from step_editor.model import StepModel
|
||||
from step_editor.recognition_graph import recognition_summary
|
||||
|
||||
|
||||
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "ICEPAK-NATURAL.stp"
|
||||
|
||||
|
||||
def _assert(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def _install_external_regions(model: StepModel, groups: list[tuple[int, ...]]) -> None:
|
||||
model._asitus_hole_regions_attempted = True # noqa: SLF001
|
||||
model._asitus_hole_region_cache.clear() # noqa: SLF001
|
||||
for group in groups:
|
||||
for face_id in group:
|
||||
model._asitus_hole_region_cache[int(face_id)] = list(group) # noqa: SLF001
|
||||
model._same_domain_face_ids_cache.clear() # noqa: SLF001
|
||||
|
||||
|
||||
def main() -> int:
|
||||
payload = {
|
||||
"validBreP": True,
|
||||
"faceCount": 158,
|
||||
"aagNodeCount": 158,
|
||||
"holeFaceIds": [85, 86, 87, 88, 95, 96, 97, 98],
|
||||
"holeCount": 4,
|
||||
"holes": [
|
||||
{"index": 1, "faceIds": [85, 98]},
|
||||
{"index": 2, "faceIds": [86, 97]},
|
||||
{"index": 3, "faceIds": [87, 96]},
|
||||
{"index": 4, "faceIds": [88, 95]},
|
||||
],
|
||||
"surfaceSummary": {"plane": 106, "cylinder": 52},
|
||||
"angleSummary": {"smooth": 4, "convex": 8},
|
||||
"geometricRelationMode": "all-pairs",
|
||||
"geometricRelationSummary": {
|
||||
"coaxial": 1,
|
||||
"coplanar": 1,
|
||||
"parallel": 1,
|
||||
"parallel_axis": 1,
|
||||
"perpendicular": 1,
|
||||
"tangent": 1,
|
||||
},
|
||||
"faces": [
|
||||
{"id": 88, "surface": "cylinder", "neighbors": [12, 95]},
|
||||
{"id": 95, "surface": "cylinder", "neighbors": [88, 42]},
|
||||
],
|
||||
"adjacency": [
|
||||
{"faceIds": [88, 95], "angleType": "smooth", "angleRad": 0.0, "edgeIds": [501, 502]},
|
||||
],
|
||||
"geometricRelations": [
|
||||
{"faceIds": [88, 95], "type": "coaxial", "residual": 0.0, "source": "analysis-situs-probe"},
|
||||
{"faceIds": [88, 95], "type": "tangent", "residual": 0.0, "source": "analysis-situs-aag-angle"},
|
||||
{"faceIds": [88, 95], "type": "coplanar", "residual": 0.0, "source": "analysis-situs-probe"},
|
||||
{"faceIds": [88, 95], "type": "parallel", "residual": 2.0, "source": "analysis-situs-probe"},
|
||||
{"faceIds": [88, 95], "type": "perpendicular", "residual": 0.0, "source": "analysis-situs-probe"},
|
||||
{"faceIds": [88, 95], "type": "parallel_axis", "residual": 0.0, "source": "analysis-situs-probe"},
|
||||
],
|
||||
}
|
||||
raw_groups = parse_asitus_hole_groups(payload)
|
||||
_assert(raw_groups == [(85, 98), (86, 97), (87, 96), (88, 95)], f"unexpected parsed groups: {raw_groups}")
|
||||
parsed = parse_asitus_probe_payload(payload)
|
||||
_assert(parsed.get("surface_summary") == {"plane": 106, "cylinder": 52}, f"bad surface summary: {parsed}")
|
||||
_assert(parsed.get("geometric_relation_mode") == "all-pairs", f"bad geometric relation mode: {parsed}")
|
||||
expected_geometric_summary = {
|
||||
"coaxial": 1,
|
||||
"coplanar": 1,
|
||||
"parallel": 1,
|
||||
"parallel_axis": 1,
|
||||
"perpendicular": 1,
|
||||
"tangent": 1,
|
||||
}
|
||||
_assert(
|
||||
parsed.get("geometric_relation_summary") == expected_geometric_summary,
|
||||
f"bad geometric summary: {parsed}",
|
||||
)
|
||||
_assert(len(tuple(parsed.get("faces", ()))) == 2, f"bad face summary parse: {parsed}")
|
||||
_assert(len(tuple(parsed.get("adjacency", ()))) == 1, f"bad adjacency parse: {parsed}")
|
||||
_assert(len(tuple(parsed.get("geometric_relations", ()))) == 6, f"bad geometric relation parse: {parsed}")
|
||||
|
||||
if not MODEL_PATH.exists():
|
||||
print("asitus hole bridge mapping skipped: local ICEPAK-NATURAL.stp is not present")
|
||||
return 0
|
||||
|
||||
model = StepModel.load(MODEL_PATH)
|
||||
mapped = model._map_asitus_hole_groups(raw_groups) # noqa: SLF001
|
||||
expected = [(84, 97), (85, 96), (86, 95), (87, 94)]
|
||||
_assert(mapped == expected, f"Analysis Situs 1-based AAG face ids should map to Python face ids: {mapped}")
|
||||
|
||||
async_model = StepModel.load(MODEL_PATH)
|
||||
_assert(async_model.begin_asitus_hole_region_load() is True, "background preload should be accepted once")
|
||||
_assert(
|
||||
async_model._asitus_hole_region_ids(87) == [], # noqa: SLF001
|
||||
"selection should not synchronously run Analysis Situs while background preload is pending",
|
||||
)
|
||||
async_mapped = async_model.install_asitus_hole_recognition_result({"ok": True, "reason": "ok", "groups": raw_groups})
|
||||
_assert(async_mapped == expected, f"background result should install mapped groups: {async_mapped}")
|
||||
_assert(async_model.face_region_ids(87) == [87, 94], "installed background result should drive whole-hole selection")
|
||||
|
||||
relation_model = StepModel.load(MODEL_PATH)
|
||||
relation_result = {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"groups": raw_groups,
|
||||
"faces": parsed.get("faces", ()),
|
||||
"adjacency": parsed.get("adjacency", ()),
|
||||
"geometric_relations": parsed.get("geometric_relations", ()),
|
||||
"surface_summary": parsed.get("surface_summary", {}),
|
||||
"angle_summary": parsed.get("angle_summary", {}),
|
||||
"geometric_relation_summary": parsed.get("geometric_relation_summary", {}),
|
||||
}
|
||||
relation_mapped = relation_model.install_asitus_hole_recognition_result(relation_result)
|
||||
_assert(relation_mapped == expected, f"relation install should preserve hole mapping: {relation_mapped}")
|
||||
relation_info = relation_model.quick_face_info(87)
|
||||
_assert(relation_info.get("asitus_relation_status") == "ready", f"missing AAG relation fields: {relation_info}")
|
||||
_assert(
|
||||
relation_info.get("asitus_adjacent_face_ids") == (11, 94),
|
||||
f"Analysis Situs 1-based neighbors should map to Python face ids: {relation_info}",
|
||||
)
|
||||
_assert(
|
||||
"smooth:1" in str(relation_info.get("asitus_adjacent_relation_summary") or ""),
|
||||
f"missing AAG angle relation summary: {relation_info}",
|
||||
)
|
||||
_assert(
|
||||
"coaxial:1" in str(relation_info.get("asitus_geometric_relation_summary") or ""),
|
||||
f"missing AAG geometric relation summary: {relation_info}",
|
||||
)
|
||||
_assert(
|
||||
"tangent:1" in str(relation_info.get("asitus_geometric_relation_summary") or ""),
|
||||
f"missing AAG tangent relation summary: {relation_info}",
|
||||
)
|
||||
graph_summary = recognition_summary(relation_model)
|
||||
relation_counts = dict(graph_summary.get("relation_counts", {}) or {})
|
||||
_assert(
|
||||
relation_counts.get("external_coaxial", 0) >= 1,
|
||||
f"recognition graph should include external coaxial evidence: {graph_summary}",
|
||||
)
|
||||
for relation_type in (
|
||||
"external_coplanar",
|
||||
"external_parallel",
|
||||
"external_parallel_axis",
|
||||
"external_perpendicular",
|
||||
"external_tangent",
|
||||
):
|
||||
_assert(
|
||||
relation_counts.get(relation_type, 0) >= 1,
|
||||
f"recognition graph should include {relation_type} evidence: {graph_summary}",
|
||||
)
|
||||
_assert(
|
||||
int(relation_info.get("recognition_external_relation_score_bonus") or 0) > 0,
|
||||
f"feature summary should expose an external relation confidence bonus: {relation_info}",
|
||||
)
|
||||
candidates = relation_model.editable_feature_candidates(limit=160, detailed=False, max_scan_faces=120)
|
||||
related_candidates = [
|
||||
item
|
||||
for item in candidates
|
||||
if int(item.get("face_id", -1) or -1) == 87
|
||||
or int(item.get("target_id", -1) or -1) == 87
|
||||
]
|
||||
_assert(related_candidates, f"editable scan should include the Analysis Situs-backed Face 87 candidate: {candidates}")
|
||||
_assert(
|
||||
any(int(item.get("recognition_external_relation_score_bonus") or 0) > 0 for item in related_candidates),
|
||||
f"editable candidates should carry external relation sorting support: {related_candidates}",
|
||||
)
|
||||
_assert(
|
||||
any("coaxial" in str(item.get("external_recognition_relation_summary") or "") for item in related_candidates),
|
||||
f"editable candidates should expose external relation summary: {related_candidates}",
|
||||
)
|
||||
|
||||
_install_external_regions(model, mapped)
|
||||
_assert(model.face_region_ids(87) == [87, 94], "Face 87 should select the complete external hole group")
|
||||
_assert(model.face_region_ids(94) == [87, 94], "Face 94 should select the complete external hole group")
|
||||
info = model.quick_face_info(87)
|
||||
_assert(list(info.get("feature_highlight_face_ids") or []) == [87, 94], "quick UI highlight should use external hole group")
|
||||
|
||||
cli = default_asitus_recognize_holes_path(PROJECT_ROOT)
|
||||
if cli is not None:
|
||||
result = run_asitus_hole_recognition(MODEL_PATH, project_root=PROJECT_ROOT, timeout_seconds=6.0)
|
||||
_assert(result.get("ok") is True, f"local Analysis Situs CLI should recognize ICEPAK holes: {result}")
|
||||
external_mapped = model._map_asitus_hole_groups(result.get("groups", ())) # noqa: SLF001
|
||||
for group in expected:
|
||||
_assert(group in external_mapped, f"local Analysis Situs CLI mapping missed {group}: {external_mapped}")
|
||||
|
||||
print("asitus hole bridge ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user