feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
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.scdm_backend import ScdmBackendInfo, load_scdm_backend_cache # noqa: E402
|
||||
from step_editor.scdm_feature_mapper import attach_local_face_ids_to_scdm_cache, geometry_signature, map_scdm_raw_features, map_scdm_raw_features_file # noqa: E402
|
||||
from step_editor.scdm_probe import generate_scdm_probe_script, prepare_scdm_probe_job, run_scdm_probe # noqa: E402
|
||||
from step_editor.scdm_property_specs import property_specs_from_scdm_cache # noqa: E402
|
||||
from step_editor.scdm_schema import read_json, write_json # noqa: E402
|
||||
|
||||
|
||||
def _assert(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def _capability_keys(cache: dict[str, object], object_id_part: str) -> set[str]:
|
||||
objects = cache.get("objects")
|
||||
_assert(isinstance(objects, list), f"cache objects should be a list: {cache}")
|
||||
for item in objects:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if object_id_part not in str(item.get("objectId") or ""):
|
||||
continue
|
||||
capabilities = item.get("capabilities")
|
||||
_assert(isinstance(capabilities, list), f"capabilities should be a list: {item}")
|
||||
return {str(capability.get("key")) for capability in capabilities if isinstance(capability, dict)}
|
||||
return set()
|
||||
|
||||
|
||||
def _script_path_from_command(command: list[str] | tuple[str, ...]) -> Path:
|
||||
for item in command:
|
||||
if item.startswith("/RunScript="):
|
||||
return Path(item.split("=", 1)[1])
|
||||
raise AssertionError(f"missing /RunScript argument: {command}")
|
||||
|
||||
|
||||
def _job_path_from_script(script_path: Path) -> Path:
|
||||
text = script_path.read_text(encoding="utf-8")
|
||||
match = re.search(r"^JOB_PATH = (.+)$", text, flags=re.MULTILINE)
|
||||
_assert(match is not None, f"generated probe script should embed JOB_PATH: {script_path}")
|
||||
return Path(ast.literal_eval(match.group(1)))
|
||||
|
||||
|
||||
def _successful_probe_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
||||
script_path = _script_path_from_command(command)
|
||||
job_path = _job_path_from_script(script_path)
|
||||
job = read_json(job_path)
|
||||
outputs = job.get("outputs")
|
||||
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
||||
write_json(
|
||||
outputs["rawFeatures"],
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"backend": job.get("backend", {}),
|
||||
"model": job.get("model", {}),
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [],
|
||||
},
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with tempfile.TemporaryDirectory(prefix="step_editor_scdm_probe_") as temp:
|
||||
root = Path(temp)
|
||||
step_path = root / "sample.step"
|
||||
step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
||||
backend = ScdmBackendInfo(path=root / "SpaceClaim.exe", source="test", version="v222")
|
||||
backend.path.write_text("fake", encoding="utf-8")
|
||||
|
||||
prepared = prepare_scdm_probe_job(step_path, output_dir=root / "probe", project_root=root, backend=backend)
|
||||
_assert(prepared.get("ok") is True, f"probe job should be prepared: {prepared}")
|
||||
job_path = Path(str(prepared["job_path"]))
|
||||
script_path = Path(str(prepared["script_path"]))
|
||||
raw_path = Path(str(prepared["raw_features_path"]))
|
||||
_assert(job_path.is_file(), "scdm_probe_job.json should be written")
|
||||
_assert(script_path.is_file(), "scdm_probe.py should be written")
|
||||
job = read_json(job_path)
|
||||
_assert(job.get("adapter") == "spaceclaim-v1", f"bad adapter: {job}")
|
||||
_assert(job.get("outputs", {}).get("rawFeatures") == str(raw_path), f"bad raw output path: {job}")
|
||||
script = script_path.read_text(encoding="utf-8")
|
||||
_assert("GetRootPart" in script, "probe script should inspect the active root part")
|
||||
_assert("GetHoleFaces" in script, "probe script should ask SCDM for standard hole faces")
|
||||
_assert("StandardHoles" in script and "FindStandardHoleOptions" in script and "getattr(standard_holes, 'Find'" in script, "probe script should try SCDM StandardHoles.Find before geometric fallback")
|
||||
_assert("availableCommands" in script and "ConstantRound" in script and "Chamfer" in script, "probe script should report SCDM command availability")
|
||||
_assert("RoundInfo" in script and "_round_info_from_face" in script and "change_round_radius" in script, "probe script should collect SCDM round diagnostics")
|
||||
_assert("backendCommandCandidates" in script, "probe script should write raw command candidates")
|
||||
for token in ("_geometry_from_edge", "Length", "StartPoint", "EndPoint", "adjacentFaceOrdinals"):
|
||||
_assert(token in script, f"probe script should enrich Edge raw geometry with {token}")
|
||||
for token in ("_record_face_adjacency", "_face_adjacency_rows", "edgeGeometrySummary", "_final_edge_geometry_summary", "_feature_inventory"):
|
||||
_assert(token in script, f"probe script should summarize SCDM topology evidence with {token}")
|
||||
generated = generate_scdm_probe_script(job_path)
|
||||
_assert("JOB_PATH =" in generated and job_path.name in generated, "generated probe script should embed the job path")
|
||||
|
||||
probe_result = run_scdm_probe(step_path, backend=backend, output_dir=root / "run-probe", project_root=root, runner=_successful_probe_runner)
|
||||
_assert(probe_result.get("ok") is True, f"fake run_scdm_probe should pass: {probe_result}")
|
||||
probe_backend = probe_result.get("backend")
|
||||
_assert(isinstance(probe_backend, dict), f"probe result should carry backend status: {probe_result}")
|
||||
_assert(probe_backend.get("runScriptOk") is True, f"successful probe should mark /RunScript usable: {probe_backend}")
|
||||
_assert(probe_backend.get("licenseOk") is True, f"successful probe should mark license usable: {probe_backend}")
|
||||
cached_backend = load_scdm_backend_cache(project_root_override=root)
|
||||
_assert(cached_backend is not None and cached_backend.run_script_ok is True, f"successful probe should update backend cache: {cached_backend}")
|
||||
|
||||
raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {
|
||||
"availableCommands": [
|
||||
{"name": "StandardHoles", "available": True},
|
||||
{"name": "OffsetFaces", "available": True},
|
||||
{"name": "Move", "available": True},
|
||||
{"name": "Fill", "available": True},
|
||||
{"name": "Delete", "available": True},
|
||||
{"name": "ConstantRound", "available": True},
|
||||
{"name": "SomeFutureCommand", "available": False},
|
||||
],
|
||||
"faceAdjacency": [
|
||||
{
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinals": [1, 2],
|
||||
"edgeCount": 1,
|
||||
"edgeKinds": {"circular": 1},
|
||||
"edges": [{"edgeOrdinal": 7, "globalEdgeOrdinal": 9, "kind": "circular", "radius": 0.25}],
|
||||
}
|
||||
],
|
||||
"edgeGeometrySummary": {
|
||||
"totalEdgeCount": 9,
|
||||
"edgeKindCounts": {"linear": 5, "circular": 4},
|
||||
"circularEdgeCount": 4,
|
||||
"circularRadiusBuckets": [{"radius": "0.25", "count": 4}],
|
||||
"minEdgeLength": 0.5,
|
||||
"maxEdgeLength": 8.0,
|
||||
},
|
||||
"featureInventory": {
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1},
|
||||
"surfaceTypeCounts": {"plane": 1, "cylinder": 3},
|
||||
"curveTypeCounts": {"Line": 5, "Circle": 4},
|
||||
"operationCounts": {
|
||||
"pull_face_offset": 1,
|
||||
"change_hole_diameter": 1,
|
||||
"change_slot_width": 1,
|
||||
"move_slot": 1,
|
||||
"change_boss_height": 1,
|
||||
"move_boss": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": {"bodyCount": 1, "objectCount": 10, "faceCount": 6, "edgeCount": 1, "holeFaceCount": 0},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "hole:1",
|
||||
"objectType": "hole",
|
||||
"geometry": {
|
||||
"diameter": 0.5,
|
||||
"center": [0.5, 1.0, 9.5],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [85, 94], "bodyIndex": 0, "faceOrdinal": 12},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_hole_diameter", "enabled": True, "parameterFields": {"diameter": 0.5}},
|
||||
{"operation": "move_hole_axis", "enabled": True, "parameterFields": {"center": [0.5, 1.0, 9.5]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "hole:pattern-a",
|
||||
"objectType": "hole",
|
||||
"geometry": {
|
||||
"diameter": 0.5,
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [200], "bodyIndex": 2, "faceOrdinal": 1},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_hole_diameter", "enabled": True, "parameterFields": {"diameter": 0.5}},
|
||||
{"operation": "move_hole_axis", "enabled": True, "parameterFields": {"center": [0.0, 0.0, 0.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "hole:pattern-b",
|
||||
"objectType": "hole",
|
||||
"geometry": {
|
||||
"diameter": 0.5,
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [201], "bodyIndex": 2, "faceOrdinal": 2},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_hole_diameter", "enabled": True, "parameterFields": {"diameter": 0.5}},
|
||||
{"operation": "move_hole_axis", "enabled": True, "parameterFields": {"center": [5.0, 0.0, 0.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "hole:pattern-c",
|
||||
"objectType": "hole",
|
||||
"geometry": {
|
||||
"diameter": 0.5,
|
||||
"center": [10.0, 0.0, 0.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [202], "bodyIndex": 2, "faceOrdinal": 3},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_hole_diameter", "enabled": True, "parameterFields": {"diameter": 0.5}},
|
||||
{"operation": "move_hole_axis", "enabled": True, "parameterFields": {"center": [10.0, 0.0, 0.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "face:9",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 10.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [9]},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:8/face:5",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "cylinder",
|
||||
"center": [0.5, 1.0, 9.5],
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"radius": 0.25,
|
||||
},
|
||||
"topologyHint": {"bodyIndex": 8, "faceOrdinal": 5, "globalFaceOrdinal": 85},
|
||||
"backendCommandCandidates": [],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:8/face:16",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "cylinder",
|
||||
"center": [0.5, 1.0, 9.5],
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"radius": 0.25,
|
||||
},
|
||||
"topologyHint": {"bodyIndex": 8, "faceOrdinal": 16, "globalFaceOrdinal": 96},
|
||||
"backendCommandCandidates": [],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "round:1",
|
||||
"objectType": "round",
|
||||
"geometry": {"radius": 1.0},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_round_radius", "enabled": True},
|
||||
{"operation": "delete_round_or_chamfer", "enabled": True},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "slot:1",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_slot_width", "enabled": True},
|
||||
{"operation": "move_slot", "enabled": True, "parameterFields": {"center": [1.0, 2.0, 3.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "boss:1",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [50, 51, 52], "bodyIndex": 0, "faceOrdinal": 50},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_boss_height", "enabled": True},
|
||||
{"operation": "move_boss", "enabled": True, "parameterFields": {"center": [0.0, 0.0, 2.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "chamfer:1",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"distance": 0.8},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "pattern:1",
|
||||
"objectType": "linear_pattern",
|
||||
"geometry": {"spacing": 5.0, "instanceCenter": [0.0, 5.0, 0.0]},
|
||||
"backendCommandCandidates": [{"operation": "change_pattern_spacing", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "shell:1",
|
||||
"objectType": "shell",
|
||||
"geometry": {"thickness": 1.2},
|
||||
"backendCommandCandidates": [{"operation": "change_shell_thickness", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
cache = map_scdm_raw_features(raw)
|
||||
edge_signature = geometry_signature(
|
||||
{
|
||||
"backendId": "edge:1",
|
||||
"objectType": "edge",
|
||||
"geometry": {
|
||||
"curveType": "Circle",
|
||||
"length": 3.14,
|
||||
"startPoint": [0.0, 0.0, 0.0],
|
||||
"endPoint": [1.0, 0.0, 0.0],
|
||||
"midPoint": [0.5, 0.0, 0.0],
|
||||
"radius": 0.5,
|
||||
"center": [0.5, 0.5, 0.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 0,
|
||||
"edgeOrdinal": 7,
|
||||
"globalEdgeOrdinal": 9,
|
||||
"adjacentFaceCount": 2,
|
||||
"adjacentFaceOrdinals": [3, 4],
|
||||
},
|
||||
}
|
||||
)
|
||||
_assert(edge_signature.get("curveType") == "Circle", f"Edge curve type should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("length") == 3.14, f"Edge length should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("startPoint") == [0.0, 0.0, 0.0], f"Edge start point should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("adjacentFaceOrdinals") == [3, 4], f"Edge adjacent faces should be preserved: {edge_signature}")
|
||||
_assert(cache.get("modelFingerprint") == "abc123", f"model fingerprint should be copied: {cache}")
|
||||
_assert(cache.get("backendVersion") == "v222", f"backend version should be copied: {cache}")
|
||||
_assert({"hole.diameter", "hole.position"} <= _capability_keys(cache, "hole:1"), f"hole caps missing: {cache}")
|
||||
objects = cache.get("objects")
|
||||
_assert(isinstance(objects, list), f"cache objects should be a list: {cache}")
|
||||
hole_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "hole:hole:1"), None)
|
||||
_assert(isinstance(hole_object, dict), f"hole object should be normalized: {cache}")
|
||||
signature = hole_object.get("geometrySignature")
|
||||
_assert(isinstance(signature, dict), f"hole signature should be present: {hole_object}")
|
||||
_assert(signature.get("bodyIndex") == 0 and signature.get("faceOrdinal") == 12, f"SCDM script locator hints should be preserved: {signature}")
|
||||
_assert({"face.offset"} <= _capability_keys(cache, "face:9"), f"face caps missing: {cache}")
|
||||
cylinder_group = next(
|
||||
(item for item in objects if isinstance(item, dict) and "cylindrical_group:body:8/face:5" in str(item.get("objectId") or "")),
|
||||
None,
|
||||
)
|
||||
_assert(isinstance(cylinder_group, dict), f"split cylinder faces should produce a grouped SCDM object: {cache}")
|
||||
cylinder_signature = cylinder_group.get("geometrySignature")
|
||||
_assert(isinstance(cylinder_signature, dict), f"cylinder group should carry a geometry signature: {cylinder_group}")
|
||||
_assert(cylinder_signature.get("faceOrdinals") == [5, 16], f"cylinder group should preserve all face ordinals: {cylinder_signature}")
|
||||
_assert(len(cylinder_signature.get("scdmFaceLocators") or []) == 2, f"cylinder group should preserve SCDM face locators: {cylinder_signature}")
|
||||
_assert({"hole.diameter", "hole.position", "feature.fill"} <= _capability_keys(cache, "cylindrical_group:body:8/face:5"), f"cylinder group caps missing: {cache}")
|
||||
hole_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(94,), execution_ready=False)
|
||||
hole_keys = {str(spec.get("scdm_capability_key")) for spec in hole_specs}
|
||||
_assert({"hole.diameter", "hole.position"} <= hole_keys, f"SCDM cache should map selected hole Face to UI specs: {hole_specs}")
|
||||
_assert(all(spec.get("enabled") is False for spec in hole_specs), f"SCDM specs should stay disabled before S5: {hole_specs}")
|
||||
executable_hole_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(85,), execution_ready=True)
|
||||
_assert(any(spec.get("enabled") is True for spec in executable_hole_specs), f"SCDM specs should enable once runner is ready: {executable_hole_specs}")
|
||||
gated_hole_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(85,), execution_ready={"face.offset"})
|
||||
_assert(all(spec.get("enabled") is False for spec in gated_hole_specs), f"capability gate should keep unverified hole edits disabled: {gated_hole_specs}")
|
||||
face_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(9,), execution_ready=True)
|
||||
_assert({str(spec.get("scdm_capability_key")) for spec in face_specs} == {"face.offset"}, f"Face cache should map to offset only: {face_specs}")
|
||||
gated_face_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(9,), execution_ready={"face.offset"})
|
||||
_assert(gated_face_specs and all(spec.get("enabled") is True for spec in gated_face_specs), f"capability gate should enable verified face offset: {gated_face_specs}")
|
||||
_assert({"slot.position"} <= _capability_keys(cache, "slot:1"), f"slot.position should now be productized: {cache}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.position"},
|
||||
f"slot cache should expose only productized slot.position: {slot_specs}",
|
||||
)
|
||||
_assert(slot_specs and slot_specs[0].get("enabled") is True, f"slot.position should enable when runner gate is ready: {slot_specs}")
|
||||
blocked_slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"face.offset"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot.position should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.position"} <= _capability_keys(cache, "boss:1"), f"boss.position should now be productized: {cache}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.position"},
|
||||
f"boss cache should expose only productized boss.position: {boss_specs}",
|
||||
)
|
||||
_assert(boss_specs and boss_specs[0].get("enabled") is True, f"boss.position should enable when runner gate is ready: {boss_specs}")
|
||||
blocked_boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"slot.position"})
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss.position should honor runner gate: {blocked_boss_specs}")
|
||||
|
||||
raw_without_local_ids = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "face:no-local-id",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 0.01],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"planeOffset": 0.01,
|
||||
},
|
||||
"topologyHint": {"bodyIndex": 0, "faceOrdinal": 3},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
enriched = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(raw_without_local_ids),
|
||||
[
|
||||
{
|
||||
"faceId": 9,
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 10.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"planeOffset": 10.0,
|
||||
}
|
||||
],
|
||||
)
|
||||
enriched_specs = property_specs_from_scdm_cache(enriched, selected_face_ids=(9,), execution_ready=True)
|
||||
_assert(enriched_specs and enriched_specs[0].get("scdm_capability_key") == "face.offset", f"local Face IDs should be attached from geometry signatures: {enriched}")
|
||||
enriched_group = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(raw),
|
||||
[
|
||||
{
|
||||
"faceId": 85,
|
||||
"surfaceType": "cylinder",
|
||||
"center": [0.5, 1.0, 9.5],
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"radius": 0.25,
|
||||
},
|
||||
{
|
||||
"faceId": 96,
|
||||
"surfaceType": "cylinder",
|
||||
"center": [0.5, 1.0, 9.5],
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"radius": 0.25,
|
||||
},
|
||||
],
|
||||
)
|
||||
grouped_specs = property_specs_from_scdm_cache(enriched_group, selected_face_ids=(96,), execution_ready=True)
|
||||
grouped_keys = {str(spec.get("scdm_capability_key")) for spec in grouped_specs}
|
||||
_assert({"hole.diameter", "hole.position", "feature.fill"} <= grouped_keys, f"local Face IDs should attach to SCDM cylinder groups: {enriched_group}")
|
||||
|
||||
missing_command_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": False}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "face:no-offset-command",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 1.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"planeOffset": 1.0,
|
||||
},
|
||||
"topologyHint": {"faceIds": [12]},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
missing_command_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_command_raw), selected_face_ids=(12,), execution_ready=True)
|
||||
_assert(missing_command_specs and missing_command_specs[0].get("enabled") is False, f"missing SCDM command should disable capability: {missing_command_specs}")
|
||||
_assert("OffsetFaces" in str(missing_command_specs[0].get("disabled_tip") or ""), f"disabled reason should name the missing SCDM command: {missing_command_specs}")
|
||||
diagnostics = cache.get("diagnostics")
|
||||
_assert(isinstance(diagnostics, dict), f"cache diagnostics should be present: {cache}")
|
||||
raw_summary = diagnostics.get("raw_summary")
|
||||
_assert(isinstance(raw_summary, dict), f"SCDM raw summary should be preserved in cache diagnostics: {cache}")
|
||||
_assert(raw_summary.get("faceCount") is None or isinstance(raw_summary.get("faceCount"), int), f"raw summary should stay JSON-like: {raw_summary}")
|
||||
backend_commands = diagnostics.get("backend_commands")
|
||||
_assert(isinstance(backend_commands, list), f"SCDM backend command inventory should be preserved: {cache}")
|
||||
command_names = {str(item.get("name")) for item in backend_commands if isinstance(item, dict)}
|
||||
_assert({"StandardHoles", "ConstantRound"} <= command_names, f"backend command names should be available in diagnostics: {backend_commands}")
|
||||
face_adjacency = diagnostics.get("face_adjacency")
|
||||
_assert(isinstance(face_adjacency, list) and face_adjacency, f"SCDM Face adjacency should be preserved in cache diagnostics: {cache}")
|
||||
edge_summary = diagnostics.get("edge_geometry_summary")
|
||||
_assert(isinstance(edge_summary, dict) and edge_summary.get("circularEdgeCount") == 4, f"SCDM Edge geometry summary should be preserved: {cache}")
|
||||
feature_inventory = diagnostics.get("feature_inventory")
|
||||
_assert(
|
||||
isinstance(feature_inventory, dict)
|
||||
and feature_inventory.get("objectTypeCounts", {}).get("hole") == 1
|
||||
and feature_inventory.get("operationCounts", {}).get("change_slot_width") == 1,
|
||||
f"SCDM feature inventory should be preserved: {cache}",
|
||||
)
|
||||
geometry_hints = diagnostics.get("geometry_candidate_hints")
|
||||
_assert(isinstance(geometry_hints, list) and geometry_hints, f"SCDM structural candidate hints should be produced: {cache}")
|
||||
hint_keys = {str(item.get("capabilityKey")) for item in geometry_hints if isinstance(item, dict)}
|
||||
_assert(
|
||||
{"slot.width", "boss.height", "round.radius", "chamfer.distance", "pattern.spacing", "shell.thickness"} <= hint_keys,
|
||||
f"S7 geometry hints should cover planned feature families: {hint_keys}",
|
||||
)
|
||||
derived_candidates = diagnostics.get("derived_feature_candidates")
|
||||
_assert(isinstance(derived_candidates, list) and derived_candidates, f"repeated holes should produce derived S7 candidates: {cache}")
|
||||
pattern_candidate = next((item for item in derived_candidates if isinstance(item, dict) and item.get("objectType") == "linear_pattern"), None)
|
||||
_assert(isinstance(pattern_candidate, dict), f"linear pattern candidate should be derived: {derived_candidates}")
|
||||
pattern_signature = pattern_candidate.get("geometrySignature")
|
||||
_assert(isinstance(pattern_signature, dict), f"linear pattern should keep a geometry signature: {pattern_candidate}")
|
||||
_assert(pattern_signature.get("spacing") == 5.0, f"linear pattern spacing should be preserved: {pattern_signature}")
|
||||
_assert(pattern_signature.get("instanceCount") == 3, f"linear pattern count should be preserved: {pattern_signature}")
|
||||
_assert(pattern_signature.get("axis") == [1.0, 0.0, 0.0], f"linear pattern axis should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("instanceCenters") or []) == 3, f"linear pattern centers should be preserved: {pattern_signature}")
|
||||
not_productized = diagnostics.get("discovered_not_productized")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"round candidate should stay diagnostic only: {cache}")
|
||||
planned = diagnostics.get("planned_not_productized")
|
||||
_assert(isinstance(planned, list), f"planned diagnostics should be present: {cache}")
|
||||
planned_keys = {str(item.get("capabilityKey")) for item in planned if isinstance(item, dict)}
|
||||
expected_planned = {
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"boss.height",
|
||||
"boss.diameter",
|
||||
"round.radius",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"chamfer.distance",
|
||||
"pattern.spacing",
|
||||
"pattern.instance_position",
|
||||
"shell.thickness",
|
||||
}
|
||||
_assert(expected_planned <= planned_keys, f"S7 planned capabilities should be diagnosed but not productized: {planned_keys}")
|
||||
|
||||
raw_file = root / "raw.json"
|
||||
cache_file = root / "cache.json"
|
||||
write_json(raw_file, raw)
|
||||
from_file = map_scdm_raw_features_file(raw_file, cache_file)
|
||||
_assert(cache_file.is_file(), "cache file should be written")
|
||||
_assert(from_file.get("objects") == read_json(cache_file).get("objects"), "file mapper should match in-memory mapper")
|
||||
|
||||
print("scdm probe pipeline ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user