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("ChamferInfo" in script and "_chamfer_info_from_face" in script and "change_chamfer_distance" in script, "probe script should collect SCDM chamfer diagnostics") _assert("SlotInfo" in script and "_slot_info_from_face" in script and "change_slot_depth" in script, "probe script should collect SCDM slot 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}") for token in ("_component_entries", "_component_body_locator_map", "componentInstances", "componentLocators"): _assert(token in script, f"probe script should preserve SCDM component occurrence 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": "Chamfer", "available": True}, {"name": "ConstantRound", "available": True}, {"name": "ChamferInfo", "available": True}, {"name": "SlotInfo", "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, "chamfer": 1}, "surfaceTypeCounts": {"plane": 1, "cylinder": 3}, "curveTypeCounts": {"Line": 5, "Circle": 4}, "operationCounts": { "pull_face_offset": 1, "change_hole_diameter": 1, "change_slot_width": 1, "change_slot_depth": 1, "move_slot": 1, "change_boss_height": 1, "move_boss": 1, "change_chamfer_distance": 1, }, }, "componentInstances": [ { "backendId": "component:0", "componentIndex": 0, "componentPath": [0], "componentName": "零件 3", "contentBodyCount": 1, "placementTranslation": [0.0, 20.0, 0.0], } ], }, "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:2/face:100", "objectType": "face", "geometry": { "surfaceType": "plane", "center": [0.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0], }, "topologyHint": {"faceIds": [100], "bodyIndex": 2, "faceOrdinal": 100, "globalFaceOrdinal": 100}, "backendCommandCandidates": [ {"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}}, ], "rawLimitations": [], }, { "backendId": "body:2/face:101", "objectType": "face", "geometry": { "surfaceType": "plane", "center": [0.0, 0.0, 1.2], "normal": [0.0, 0.0, -1.0], }, "topologyHint": {"faceIds": [101], "bodyIndex": 2, "faceOrdinal": 101, "globalFaceOrdinal": 101}, "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, "roundInfo": { "radius": 1.0, "diameter": 2.0, "isConstant": True, "isRound": True, "type": "ConstantRound", }, }, "topologyHint": {"faceIds": [60], "bodyIndex": 0, "faceOrdinal": 60, "globalFaceOrdinal": 60}, "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, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]}, "topologyHint": { "faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30, "depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}], }, "backendCommandCandidates": [ {"operation": "change_slot_width", "enabled": True}, {"operation": "change_slot_depth", "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, "heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}], "diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}], }, "backendCommandCandidates": [ {"operation": "change_boss_height", "enabled": True}, {"operation": "change_boss_diameter", "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, "chamferInfo": { "distance": 0.8, "distance1": 0.8, "distance2": 0.8, "isEqualDistance": True, "isChamfer": True, "type": "EqualDistanceChamfer", }, }, "topologyHint": {"faceIds": [63], "bodyIndex": 0, "faceOrdinal": 63, "globalFaceOrdinal": 63}, "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": "body:20", "objectType": "body", "geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12}, "topologyHint": { "bodyIndex": 20, "faceIds": [300], "faceOrdinals": [0], "globalFaceOrdinals": [300], "componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}], }, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, { "backendId": "body:21", "objectType": "body", "geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12}, "topologyHint": { "bodyIndex": 21, "faceIds": [301], "faceOrdinals": [0], "globalFaceOrdinals": [301], "componentLocators": [{"componentIndex": 1, "componentPath": [1], "componentBodyIndex": 0, "bodyIndex": 21}], }, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, { "backendId": "body:22", "objectType": "body", "geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12}, "topologyHint": { "bodyIndex": 22, "faceIds": [302], "faceOrdinals": [0], "globalFaceOrdinals": [302], "componentLocators": [{"componentIndex": 2, "componentPath": [2], "componentBodyIndex": 0, "bodyIndex": 22}], }, "backendCommandCandidates": [{"operation": "move_body", "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}") component_signature = geometry_signature( { "backendId": "body:20", "objectType": "body", "geometry": {"center": [0.0, 20.0, 0.0]}, "topologyHint": { "bodyIndex": 20, "bodyLocators": [{"bodyIndex": 20, "componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0}], "componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}], }, } ) _assert(component_signature.get("componentLocators"), f"component locators should be preserved in geometry signatures: {component_signature}") _assert(cache.get("modelFingerprint") == "abc123", f"model fingerprint should be copied: {cache}") _assert(int(cache.get("mapperRevision") or 0) >= 2, f"SCDM cache should carry the mapper revision for disk reuse: {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.width", "slot.depth", "slot.position"} <= _capability_keys(cache, "slot:1"), f"slot width/depth/position should now be productized: {cache}") slot_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "slot:slot:1"), None) _assert(isinstance(slot_object, dict), f"slot object should be normalized: {cache}") slot_signature = slot_object.get("geometrySignature") _assert(isinstance(slot_signature, dict) and slot_signature.get("depthFaceLocators"), f"slot depth locator should be preserved: {slot_signature}") _assert(isinstance(slot_signature, dict) and slot_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_signature}") slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.width", "slot.depth", "slot.position"}) _assert( {str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.width", "slot.depth", "slot.position"}, f"slot cache should expose productized slot.width, slot.depth and slot.position: {slot_specs}", ) _assert(slot_specs and all(spec.get("enabled") is True for spec in slot_specs), f"slot width/depth/position should enable when runner gate is ready: {slot_specs}") slot_width_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.width"), None) _assert(slot_width_spec and slot_width_spec.get("value_type") == "positive", f"slot.width should use positive numeric input: {slot_specs}") slot_depth_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.depth"), None) _assert(slot_depth_spec and slot_depth_spec.get("value_type") == "positive", f"slot.depth should use positive numeric input: {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 capabilities should honor runner gate: {blocked_slot_specs}") _assert({"boss.diameter", "boss.height", "boss.position"} <= _capability_keys(cache, "boss:1"), f"boss diameter/height/position should now be productized: {cache}") boss_object = next((item for item in objects if isinstance(item, dict) and item.get("objectType") == "cylindrical_boss"), None) _assert(isinstance(boss_object, dict), f"boss object should be normalized: {cache}") boss_signature = boss_object.get("geometrySignature") _assert(isinstance(boss_signature, dict) and boss_signature.get("heightFaceLocators"), f"boss height locator should be preserved: {boss_signature}") _assert(isinstance(boss_signature, dict) and boss_signature.get("diameterFaceLocators"), f"boss diameter locator should be preserved: {boss_signature}") boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.diameter", "boss.height", "boss.position"}) _assert( {str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.diameter", "boss.height", "boss.position"}, f"boss cache should expose productized boss.diameter, boss.height and boss.position: {boss_specs}", ) _assert(boss_specs and all(spec.get("enabled") is True for spec in boss_specs), f"boss diameter/height/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 capabilities should honor runner gate: {blocked_boss_specs}") _assert({"round.radius", "feature.delete_round_or_chamfer"} <= _capability_keys(cache, "round:1"), f"round radius/delete should now be productized: {cache}") round_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "round:round:1"), None) _assert(isinstance(round_object, dict), f"round object should be normalized: {cache}") round_signature = round_object.get("geometrySignature") _assert(isinstance(round_signature, dict) and round_signature.get("isConstantRound") is True, f"round constant evidence should be preserved: {round_signature}") round_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(60,), execution_ready={"round.radius", "feature.delete_round_or_chamfer"}) _assert( {str(spec.get("scdm_capability_key")) for spec in round_specs} == {"round.radius", "feature.delete_round_or_chamfer"}, f"round cache should expose productized radius and delete command: {round_specs}", ) round_radius_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "round.radius"), None) round_delete_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "feature.delete_round_or_chamfer"), None) _assert(round_radius_spec and round_radius_spec.get("value_type") == "positive" and round_radius_spec.get("enabled") is True, f"round.radius should be executable when runner gate is ready: {round_specs}") _assert(round_delete_spec and round_delete_spec.get("value_type") == "command" and round_delete_spec.get("enabled") is True, f"round delete command should be executable when runner gate is ready: {round_specs}") _assert({"chamfer.distance"} <= _capability_keys(cache, "chamfer:1"), f"chamfer distance should now be productized: {cache}") chamfer_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "chamfer:chamfer:1"), None) _assert(isinstance(chamfer_object, dict), f"chamfer object should be normalized: {cache}") chamfer_signature = chamfer_object.get("geometrySignature") _assert(isinstance(chamfer_signature, dict) and chamfer_signature.get("isEqualDistanceChamfer") is True, f"chamfer equal-distance evidence should be preserved: {chamfer_signature}") chamfer_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(63,), execution_ready={"chamfer.distance"}) _assert( {str(spec.get("scdm_capability_key")) for spec in chamfer_specs} == {"chamfer.distance"}, f"chamfer cache should expose productized chamfer.distance: {chamfer_specs}", ) _assert(chamfer_specs and chamfer_specs[0].get("value_type") == "positive" and chamfer_specs[0].get("enabled") is True, f"chamfer.distance should be executable when runner gate is ready: {chamfer_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}") body_pattern_without_face_ids = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Move", "available": True}]}, "objects": [ { "backendId": "body:20", "objectType": "body", "geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12}, "topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]}, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, { "backendId": "body:21", "objectType": "body", "geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12}, "topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]}, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, { "backendId": "body:22", "objectType": "body", "geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12}, "topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]}, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, ], } enriched_body_pattern = attach_local_face_ids_to_scdm_cache( map_scdm_raw_features(body_pattern_without_face_ids), [ {"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane"}, {"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane"}, {"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane"}, ], ) body_pattern_specs_from_ordinals = property_specs_from_scdm_cache(enriched_body_pattern, selected_face_ids=(301,), execution_ready={"pattern.spacing"}) _assert( any(spec.get("scdm_capability_key") == "pattern.spacing" for spec in body_pattern_specs_from_ordinals), f"body pattern spacing should attach local Face IDs from SCDM ordinals: {enriched_body_pattern}", ) unit_scaled_face_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"}, "diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]}, "objects": [ { "backendId": "body:0/face:9", "objectType": "face", "geometry": {"surfaceType": "plane", "axis": [-1.0, 0.0, 0.0], "planeOffset": -0.00125}, "topologyHint": {"bodyIndex": 0, "faceOrdinal": 9, "globalFaceOrdinal": 9}, "backendCommandCandidates": [{"operation": "pull_face_offset", "enabled": True}], "rawLimitations": [], } ], } unit_scaled_face_cache = attach_local_face_ids_to_scdm_cache( map_scdm_raw_features(unit_scaled_face_raw), [ { "faceId": 9, "bodyIndex": 0, "faceOrdinal": 9, "globalFaceOrdinal": 9, "surfaceType": "plane", "axis": [-1.0, 0.0, 0.0], "planeOffset": -1.25, } ], ) unit_scaled_face_signature = unit_scaled_face_cache["objects"][0]["geometrySignature"] # type: ignore[index] _assert(abs(float(unit_scaled_face_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"ordinal-matched Face should infer SCDM/local unit scale: {unit_scaled_face_signature}") unit_scaled_face_specs = property_specs_from_scdm_cache(unit_scaled_face_cache, selected_face_ids=(9,), execution_ready=True) _assert( unit_scaled_face_specs and unit_scaled_face_specs[0].get("current_text") == "-1.25", f"SCDM length values should display in local model units: {unit_scaled_face_specs}", ) unit_scaled_body_pattern_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"}, "diagnostics": {"availableCommands": [{"name": "Move", "available": True}]}, "objects": [ { "backendId": "body:20", "objectType": "body", "geometry": {"center": [0.0, 0.020, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12}, "topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]}, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, { "backendId": "body:21", "objectType": "body", "geometry": {"center": [0.0, 0.025, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12}, "topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]}, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, { "backendId": "body:22", "objectType": "body", "geometry": {"center": [0.0, 0.030, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12}, "topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]}, "backendCommandCandidates": [{"operation": "move_body", "enabled": True}], "rawLimitations": [], }, ], } unit_scaled_body_pattern_cache = attach_local_face_ids_to_scdm_cache( map_scdm_raw_features(unit_scaled_body_pattern_raw), [ {"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane", "bboxMin": [-0.5, 19.0, 0.0], "bboxMax": [0.5, 21.0, 1.0]}, {"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane", "bboxMin": [-0.5, 24.0, 0.0], "bboxMax": [0.5, 26.0, 1.0]}, {"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane", "bboxMin": [-0.5, 29.0, 0.0], "bboxMax": [0.5, 31.0, 1.0]}, {"faceId": 399, "bodyIndex": 99, "faceOrdinal": 0, "globalFaceOrdinal": 399, "surfaceType": "plane", "axis": [0.0, 0.0, 1.0], "planeOffset": 0.0, "bboxMin": [-1.0, 18.0, 0.0], "bboxMax": [1.0, 32.0, 0.0], "area": 28.0}, ], ) unit_scaled_body_pattern = next( item for item in unit_scaled_body_pattern_cache["objects"] if item.get("objectType") == "linear_pattern" # type: ignore[index] ) unit_scaled_body_signature = unit_scaled_body_pattern["geometrySignature"] # type: ignore[index] _assert(abs(float(unit_scaled_body_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"body pattern should infer unit scale from local body centers: {unit_scaled_body_signature}") _assert(unit_scaled_body_signature.get("supportFaceIds") == [399], f"body pattern should expose its support Face: {unit_scaled_body_signature}") fit = unit_scaled_body_signature.get("supportPatternFit") _assert(isinstance(fit, dict) and fit.get("supportFaceIds") == [399], f"body pattern should record support fit limits: {unit_scaled_body_signature}") _assert(abs(float(fit.get("maxSpacingLocal") or 0.0) - 6.0) <= 1.0e-9, f"support fit should limit centered spacing: {fit}") unit_scaled_body_specs = property_specs_from_scdm_cache( {"objects": [unit_scaled_body_pattern]}, selected_face_ids=(399,), execution_ready={"pattern.spacing", "pattern.segment_spacing"}, ) _assert( unit_scaled_body_specs and unit_scaled_body_specs[0].get("current_text") == "5", f"support Face should expose pattern spacing in local units: {unit_scaled_body_specs}", ) _assert( unit_scaled_body_specs[0].get("max_value") == 6.0, f"support Face pattern spacing should expose a local-unit hard limit: {unit_scaled_body_specs}", ) local_segment_spec = next( (spec for spec in unit_scaled_body_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"), None, ) _assert( isinstance(local_segment_spec, dict) and str(local_segment_spec.get("label") or "").startswith("Solid20-Solid21") and str(local_segment_spec.get("range_hint") or "").startswith("固定前项,移动后侧:") and "固定 Solid20" in str(local_segment_spec.get("range_hint") or "") and "平移 Solid21" in str(local_segment_spec.get("range_hint") or ""), f"local mapped pattern segment labels should use visible Solid IDs and explain motion semantics: {unit_scaled_body_specs}", ) 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}") missing_current_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Move", "available": True}]}, "objects": [ { "backendId": "slot:missing-center", "objectType": "slot", "geometry": {"width": 2.0}, "topologyHint": {"faceIds": [44]}, "backendCommandCandidates": [{"operation": "move_slot", "enabled": True}], "rawLimitations": [], } ], } missing_current_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_current_raw), selected_face_ids=(44,), execution_ready={"slot.width", "slot.position"}) slot_position_missing_current = next((spec for spec in missing_current_specs if spec.get("scdm_capability_key") == "slot.position"), None) _assert(slot_position_missing_current and slot_position_missing_current.get("enabled") is False, f"missing current value should disable slot.position: {missing_current_specs}") _assert("没有返回可用于编辑的当前值" in str(slot_position_missing_current.get("disabled_tip") or ""), f"missing current value should be explained: {missing_current_specs}") slot_missing_depth_locator_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Move", "available": True}]}, "objects": [ { "backendId": "slot:missing-depth-locator", "objectType": "slot", "geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]}, "topologyHint": {"faceIds": [45], "bodyIndex": 0, "faceOrdinal": 45}, "backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}], "rawLimitations": [], } ], } slot_missing_depth_locator_specs = property_specs_from_scdm_cache( map_scdm_raw_features(slot_missing_depth_locator_raw), selected_face_ids=(45,), execution_ready={"slot.depth"}, ) slot_depth_without_locator = next((spec for spec in slot_missing_depth_locator_specs if spec.get("scdm_capability_key") == "slot.depth"), None) _assert(slot_depth_without_locator and slot_depth_without_locator.get("enabled") is False, f"slot.depth should require a bottom face locator: {slot_missing_depth_locator_specs}") _assert("槽底面定位信息" in str(slot_depth_without_locator.get("disabled_tip") or ""), f"slot.depth missing locator should be explained: {slot_missing_depth_locator_specs}") slot_missing_depth_axis_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Move", "available": True}]}, "objects": [ { "backendId": "slot:missing-depth-axis", "objectType": "slot", "geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]}, "topologyHint": {"faceIds": [46], "bodyIndex": 0, "faceOrdinal": 46, "depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 46}]}, "backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}], "rawLimitations": [], } ], } slot_missing_depth_axis_specs = property_specs_from_scdm_cache( map_scdm_raw_features(slot_missing_depth_axis_raw), selected_face_ids=(46,), execution_ready={"slot.depth"}, ) slot_depth_without_axis = next((spec for spec in slot_missing_depth_axis_specs if spec.get("scdm_capability_key") == "slot.depth"), None) _assert(slot_depth_without_axis and slot_depth_without_axis.get("enabled") is False, f"slot.depth should require a depth axis: {slot_missing_depth_axis_specs}") _assert("槽深方向" in str(slot_depth_without_axis.get("disabled_tip") or ""), f"slot.depth missing axis should be explained: {slot_missing_depth_axis_specs}") boss_missing_height_locator_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Move", "available": True}]}, "objects": [ { "backendId": "boss:missing-height-locator", "objectType": "cylindrical_boss", "geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]}, "topologyHint": {"faceIds": [55], "bodyIndex": 0, "faceOrdinal": 55}, "backendCommandCandidates": [{"operation": "change_boss_height", "enabled": True}], "rawLimitations": [], } ], } boss_missing_height_locator_specs = property_specs_from_scdm_cache( map_scdm_raw_features(boss_missing_height_locator_raw), selected_face_ids=(55,), execution_ready={"boss.height"}, ) boss_height_without_locator = next((spec for spec in boss_missing_height_locator_specs if spec.get("scdm_capability_key") == "boss.height"), None) _assert(boss_height_without_locator and boss_height_without_locator.get("enabled") is False, f"boss.height should require a top face locator: {boss_missing_height_locator_specs}") _assert("顶面定位信息" in str(boss_height_without_locator.get("disabled_tip") or ""), f"boss.height missing locator should be explained: {boss_missing_height_locator_specs}") boss_missing_diameter_locator_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]}, "objects": [ { "backendId": "boss:missing-diameter-locator", "objectType": "cylindrical_boss", "geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]}, "topologyHint": {"faceIds": [56], "bodyIndex": 0, "faceOrdinal": 56}, "backendCommandCandidates": [{"operation": "change_boss_diameter", "enabled": True}], "rawLimitations": [], } ], } boss_missing_diameter_locator_specs = property_specs_from_scdm_cache( map_scdm_raw_features(boss_missing_diameter_locator_raw), selected_face_ids=(56,), execution_ready={"boss.diameter"}, ) boss_diameter_without_locator = next((spec for spec in boss_missing_diameter_locator_specs if spec.get("scdm_capability_key") == "boss.diameter"), None) _assert(boss_diameter_without_locator and boss_diameter_without_locator.get("enabled") is False, f"boss.diameter should require a side face locator: {boss_missing_diameter_locator_specs}") _assert("侧壁定位信息" in str(boss_diameter_without_locator.get("disabled_tip") or ""), f"boss.diameter missing locator should be explained: {boss_missing_diameter_locator_specs}") round_missing_constant_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]}, "objects": [ { "backendId": "round:missing-constant", "objectType": "round", "geometry": {"roundInfo": {"radius": 1.0, "isRound": True}}, "topologyHint": {"faceIds": [61], "bodyIndex": 0, "faceOrdinal": 61}, "backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}], "rawLimitations": [], } ], } round_missing_constant_specs = property_specs_from_scdm_cache( map_scdm_raw_features(round_missing_constant_raw), selected_face_ids=(61,), execution_ready={"round.radius"}, ) round_without_constant = next((spec for spec in round_missing_constant_specs if spec.get("scdm_capability_key") == "round.radius"), None) _assert(round_without_constant and round_without_constant.get("enabled") is False, f"round.radius should require constant-round evidence: {round_missing_constant_specs}") _assert("等半径圆角证据" in str(round_without_constant.get("disabled_tip") or ""), f"round.radius missing constant evidence should be explained: {round_missing_constant_specs}") round_missing_locator_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]}, "objects": [ { "backendId": "round:missing-locator", "objectType": "round", "geometry": {"roundInfo": {"radius": 1.0, "isConstant": True, "isRound": True}}, "topologyHint": {"faceIds": [62]}, "backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}], "rawLimitations": [], } ], } round_missing_locator_specs = property_specs_from_scdm_cache( map_scdm_raw_features(round_missing_locator_raw), selected_face_ids=(62,), execution_ready={"round.radius"}, ) round_without_locator = next((spec for spec in round_missing_locator_specs if spec.get("scdm_capability_key") == "round.radius"), None) _assert(round_without_locator and round_without_locator.get("enabled") is False, f"round.radius should require SCDM face locator evidence: {round_missing_locator_specs}") _assert("可定位的圆角面" in str(round_without_locator.get("disabled_tip") or ""), f"round.radius missing locator should be explained: {round_missing_locator_specs}") chamfer_missing_equal_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]}, "objects": [ { "backendId": "chamfer:missing-equal", "objectType": "chamfer", "geometry": {"chamferInfo": {"distance": 0.8, "isChamfer": True}}, "topologyHint": {"faceIds": [64], "bodyIndex": 0, "faceOrdinal": 64}, "backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}], "rawLimitations": [], } ], } chamfer_missing_equal_specs = property_specs_from_scdm_cache( map_scdm_raw_features(chamfer_missing_equal_raw), selected_face_ids=(64,), execution_ready={"chamfer.distance"}, ) chamfer_without_equal = next((spec for spec in chamfer_missing_equal_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None) _assert(chamfer_without_equal and chamfer_without_equal.get("enabled") is False, f"chamfer.distance should require equal-distance evidence: {chamfer_missing_equal_specs}") _assert("等距倒角证据" in str(chamfer_without_equal.get("disabled_tip") or ""), f"chamfer.distance missing equal evidence should be explained: {chamfer_missing_equal_specs}") chamfer_missing_locator_raw = { "schemaVersion": 1, "backend": {"name": "SCDM", "version": "v222"}, "model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"}, "diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]}, "objects": [ { "backendId": "chamfer:missing-locator", "objectType": "chamfer", "geometry": {"chamferInfo": {"distance": 0.8, "isEqualDistance": True, "isChamfer": True}}, "topologyHint": {"faceIds": [65]}, "backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}], "rawLimitations": [], } ], } chamfer_missing_locator_specs = property_specs_from_scdm_cache( map_scdm_raw_features(chamfer_missing_locator_raw), selected_face_ids=(65,), execution_ready={"chamfer.distance"}, ) chamfer_without_locator = next((spec for spec in chamfer_missing_locator_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None) _assert(chamfer_without_locator and chamfer_without_locator.get("enabled") is False, f"chamfer.distance should require SCDM face locator evidence: {chamfer_missing_locator_specs}") _assert("可定位的倒角面" in str(chamfer_without_locator.get("disabled_tip") or ""), f"chamfer.distance missing locator should be explained: {chamfer_missing_locator_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}", ) component_instances = diagnostics.get("component_instances") _assert( isinstance(component_instances, list) and component_instances and component_instances[0].get("componentPath") == [0], f"SCDM component occurrence diagnostics 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( {"pattern.instance_position", "shell.thickness"} <= hint_keys and "slot.depth" not in hint_keys and "pattern.spacing" not in 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}") _assert(len(pattern_signature.get("patternInstances") or []) == 3, f"linear pattern instance locators should be preserved: {pattern_signature}") _assert({"pattern.spacing"} <= _capability_keys(cache, "derived:linear_pattern"), f"derived linear pattern should expose productized spacing: {cache}") thin_wall_candidate = next((item for item in derived_candidates if isinstance(item, dict) and item.get("objectType") == "thin_wall"), None) _assert(isinstance(thin_wall_candidate, dict), f"paired planar faces should produce a thin-wall candidate: {derived_candidates}") thin_wall_signature = thin_wall_candidate.get("geometrySignature") _assert(isinstance(thin_wall_signature, dict), f"thin-wall candidate should keep a geometry signature: {thin_wall_candidate}") _assert(thin_wall_signature.get("thickness") == 1.2, f"thin-wall thickness should be preserved: {thin_wall_signature}") _assert(thin_wall_signature.get("thicknessAxis") == [0.0, 0.0, 1.0], f"thin-wall axis should be preserved: {thin_wall_signature}") _assert(len(thin_wall_signature.get("wallFaceLocators") or []) == 2, f"thin-wall face locators should be preserved: {thin_wall_signature}") body_pattern_candidate = next( ( item for item in derived_candidates if isinstance(item, dict) and item.get("objectType") == "linear_pattern" and isinstance(item.get("geometrySignature"), dict) and item.get("geometrySignature", {}).get("patternKind") == "body" ), None, ) _assert(isinstance(body_pattern_candidate, dict), f"repeated bodies should produce a body linear-pattern candidate: {derived_candidates}") body_pattern_signature = body_pattern_candidate.get("geometrySignature") _assert(isinstance(body_pattern_signature, dict), f"body pattern should keep a geometry signature: {body_pattern_candidate}") _assert(body_pattern_signature.get("spacing") == 5.0, f"body pattern spacing should be preserved: {body_pattern_signature}") _assert(body_pattern_signature.get("axis") == [0.0, 1.0, 0.0], f"body pattern axis should be preserved: {body_pattern_signature}") _assert(body_pattern_signature.get("bodyIndices") == [20, 21, 22], f"body pattern body indices should be preserved: {body_pattern_signature}") body_instances = body_pattern_signature.get("patternInstances") or [] _assert(len(body_instances) == 3 and all(item.get("instanceKind") == "body" for item in body_instances), f"body pattern instances should stay body-level: {body_pattern_signature}") _assert(all(item.get("componentLocators") for item in body_instances if isinstance(item, dict)), f"body pattern instances should preserve component occurrence locators: {body_pattern_signature}") body_pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(301,), execution_ready={"pattern.spacing", "pattern.segment_spacing"}) body_pattern_spacing = [ spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing" ] _assert(body_pattern_spacing, f"selecting a body member face should keep body pattern spacing as a recognized capability: {body_pattern_specs}") _assert( body_pattern_spacing[0].get("enabled") is True, f"body pattern spacing should be executable when every member has a component occurrence locator: {body_pattern_spacing}", ) body_segment_specs = [ spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing" ] _assert(len(body_segment_specs) == 2, f"three body pattern members should expose two local spacing segments: {body_pattern_specs}") _assert( "零件" in str(body_segment_specs[0].get("label") or "") and body_segment_specs[0].get("enabled") is True and body_segment_specs[0].get("scdm_backend_operation") == "change_pattern_segment_spacing", f"local body pattern spacing should be executable and name the adjacent members: {body_segment_specs}", ) body_segment_modes = body_segment_specs[0].get("scope_modes") _assert( isinstance(body_segment_modes, dict) and {"fix_left_move_right", "fix_right_move_left", "split_keep_center", "move_single_right"} <= set(body_segment_modes), f"local pattern spacing should expose several explicit modeling intents: {body_segment_specs}", ) _assert( body_segment_modes["fix_left_move_right"].get("enabled") is True and body_segment_modes["fix_right_move_left"].get("enabled") is True and body_segment_modes["split_keep_center"].get("enabled") is True and body_segment_modes["move_single_right"].get("enabled") is False, f"implemented and blocked local spacing intents should be explicit: {body_segment_modes}", ) for mode in body_segment_modes.values(): if not isinstance(mode, dict): continue label = str(mode.get("label") or "").strip() tip = str(mode.get("enabled_tip") or mode.get("disabled_tip") or mode.get("range_hint") or "").strip() _assert(label and tip.startswith(label), f"modeling-intent tooltip should start with its intent label: {body_segment_modes}") _assert( "固定前项" in str(body_segment_specs[0].get("scope_text") or "") and "移动后侧" in str(body_segment_specs[0].get("scope_text") or ""), f"local pattern spacing should expose motion semantics in the intent column: {body_segment_specs}", ) body_segment_signature = body_segment_specs[0].get("scdm_geometry_signature") _assert( isinstance(body_segment_signature, dict) and body_segment_signature.get("segmentIndex") == 0 and body_segment_signature.get("movingSide") == "after", f"local segment specs should carry segment execution metadata: {body_segment_specs}", ) pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(201,), execution_ready={"pattern.spacing", "pattern.segment_spacing"}) pattern_spacing_spec = next((spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"), None) _assert(pattern_spacing_spec and pattern_spacing_spec.get("enabled") is True, f"pattern.spacing should enable for selected pattern member: {pattern_specs}") _assert(pattern_spacing_spec.get("value_type") == "positive", f"pattern.spacing should use positive numeric input: {pattern_specs}") face_segment_specs = [ spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing" ] _assert(len(face_segment_specs) == 2, f"three face pattern members should expose two local spacing segments: {pattern_specs}") _assert( str(face_segment_specs[0].get("label") or "").startswith("Face"), f"face pattern local spacing should name the adjacent Face members instead of raw ordinal numbers: {face_segment_specs}", ) not_productized = diagnostics.get("discovered_not_productized") _assert(isinstance(not_productized, list) and not_productized, f"non-productized candidates 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 = { "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())