Files
pythonocc-step-editor/step_editor/scdm_probe.py
T

681 lines
31 KiB
Python
Raw Normal View History

from __future__ import annotations
import subprocess
from pathlib import Path
from typing import Callable
from .scdm_backend import ScdmBackendInfo, resolve_scdm_backend, save_scdm_backend_cache, scdm_run_script_command
from .scdm_schema import ScdmProbeJob, default_scdm_work_dir, file_fingerprint, read_json, utc_now, write_json
def prepare_scdm_probe_job(
step_path: str | Path,
*,
output_dir: str | Path | None = None,
project_root: str | Path | None = None,
backend: ScdmBackendInfo | None = None,
unit: str = "model",
scan_scope: str = "all",
) -> dict[str, object]:
source = Path(step_path).expanduser()
if not source.is_file():
return {"ok": False, "reason": "missing-step", "message": f"STEP file not found: {source}"}
fingerprint = file_fingerprint(source)
work_dir = Path(output_dir).expanduser() if output_dir else default_scdm_work_dir(source, project_root=project_root, fingerprint=fingerprint)
work_dir = work_dir.resolve(strict=False)
work_dir.mkdir(parents=True, exist_ok=True)
job = ScdmProbeJob(
step_path=source.resolve(strict=False),
output_dir=work_dir,
raw_features_path=work_dir / "scdm_raw_features.json",
error_path=work_dir / "error.json",
model_fingerprint=fingerprint,
unit=unit,
scan_scope=scan_scope,
backend_path=str(backend.path) if backend else "",
backend_version=backend.version if backend else "",
)
job_path = work_dir / "scdm_probe_job.json"
script_path = work_dir / "scdm_probe.py"
write_json(job_path, job.to_payload())
script_path.write_text(generate_scdm_probe_script(job_path), encoding="utf-8")
return {
"ok": True,
"reason": "ok",
"work_dir": str(work_dir),
"job_path": str(job_path),
"script_path": str(script_path),
"raw_features_path": str(job.raw_features_path),
"error_path": str(job.error_path),
"model_fingerprint": fingerprint,
}
def run_scdm_probe(
step_path: str | Path,
*,
backend: ScdmBackendInfo | None = None,
output_dir: str | Path | None = None,
project_root: str | Path | None = None,
timeout_seconds: float = 120.0,
runner: Callable[..., subprocess.CompletedProcess[str]] | None = None,
) -> dict[str, object]:
if backend is None:
resolved = resolve_scdm_backend(project_root_override=project_root, validate=False)
if not resolved.get("ok") or not isinstance(resolved.get("backend"), ScdmBackendInfo):
return {
"ok": False,
"reason": str(resolved.get("reason") or "missing-scdm"),
"message": str(resolved.get("message") or "SCDM backend is not available."),
"backend_resolution": {
"ok": bool(resolved.get("ok")),
"reason": str(resolved.get("reason") or ""),
"message": str(resolved.get("message") or ""),
},
}
backend = resolved["backend"] # type: ignore[assignment]
prepared = prepare_scdm_probe_job(step_path, output_dir=output_dir, project_root=project_root, backend=backend)
if not prepared.get("ok"):
return {"backend": backend.to_cache(), **prepared}
script_path = Path(str(prepared["script_path"]))
raw_path = Path(str(prepared["raw_features_path"]))
error_path = Path(str(prepared["error_path"]))
command = scdm_run_script_command(backend.path, script_path)
run = runner or subprocess.run
try:
completed = run(
command,
cwd=str(script_path.parent),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=max(float(timeout_seconds), 0.1),
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
check=False,
)
except subprocess.TimeoutExpired:
write_json(error_path, {"ok": False, "reason": "timeout", "message": "SCDM probe timed out."})
return {"ok": False, "reason": "timeout", "message": "SCDM probe timed out.", "backend": backend.to_cache(), **prepared}
except OSError as exc:
write_json(error_path, {"ok": False, "reason": "launch-failed", "message": str(exc)})
return {"ok": False, "reason": "launch-failed", "message": str(exc), "backend": backend.to_cache(), **prepared}
returncode = int(getattr(completed, "returncode", -1))
if returncode != 0:
message = (str(getattr(completed, "stderr", "") or "") or str(getattr(completed, "stdout", "") or "")).strip()
write_json(
error_path,
{
"ok": False,
"reason": "probe-failed",
"returncode": returncode,
"message": message,
},
)
return {"ok": False, "reason": "probe-failed", "returncode": returncode, "message": message, "backend": backend.to_cache(), **prepared}
if not raw_path.is_file():
write_json(error_path, {"ok": False, "reason": "missing-raw-output", "message": "SCDM probe did not write raw features."})
return {
"ok": False,
"reason": "missing-raw-output",
"message": "SCDM probe did not write raw features.",
"backend": backend.to_cache(),
**prepared,
}
raw = read_json(raw_path)
verified_backend = _probe_verified_backend(backend)
try:
save_scdm_backend_cache(verified_backend, project_root_override=project_root)
except Exception:
pass
return {"ok": True, "reason": "ok", "raw": raw, "backend": verified_backend.to_cache(), **prepared}
def _probe_verified_backend(backend: ScdmBackendInfo) -> ScdmBackendInfo:
return ScdmBackendInfo(
path=backend.path,
source=backend.source,
version=backend.version,
verified_at=utc_now(),
run_script_ok=True,
license_ok=True,
message="SCDM probe completed.",
)
def generate_scdm_probe_script(job_path: str | Path) -> str:
job_literal = repr(str(Path(job_path).expanduser()))
return (
"from __future__ import print_function\n"
"import json\n"
"import traceback\n"
f"JOB_PATH = {job_literal}\n"
"\n"
"def _write_json(path, payload):\n"
" handle = open(path, 'w')\n"
" try:\n"
" handle.write(json.dumps(payload, indent=2))\n"
" finally:\n"
" handle.close()\n"
"\n"
"def _safe_name(value):\n"
" try:\n"
" return type(value).__name__\n"
" except Exception:\n"
" return ''\n"
"\n"
"def _float_attr(value, names):\n"
" for name in names:\n"
" try:\n"
" result = getattr(value, name)\n"
" return float(result)\n"
" except Exception:\n"
" pass\n"
" return None\n"
"\n"
"def _xyz(value):\n"
" if value is None:\n"
" return []\n"
" result = []\n"
" for name in ('X', 'Y', 'Z'):\n"
" try:\n"
" result.append(float(getattr(value, name)))\n"
" except Exception:\n"
" return []\n"
" return result\n"
"\n"
"def _items(collection):\n"
" if collection is None:\n"
" return []\n"
" try:\n"
" return list(collection)\n"
" except Exception:\n"
" items = []\n"
" try:\n"
" count = int(collection.Count)\n"
" for index in range(count):\n"
" items.append(collection[index])\n"
" except Exception:\n"
" pass\n"
" return items\n"
"\n"
"def _geometry_from_face(face):\n"
" geometry = {}\n"
" surface = None\n"
" for expr in ('Shape.Geometry', 'Geometry', 'Surface'):\n"
" try:\n"
" current = face\n"
" for part in expr.split('.'):\n"
" current = getattr(current, part)\n"
" surface = current\n"
" break\n"
" except Exception:\n"
" pass\n"
" surface_name = _safe_name(surface)\n"
" geometry['surfaceType'] = surface_name\n"
" lowered = surface_name.lower()\n"
" radius = _float_attr(surface, ('Radius', 'radius'))\n"
" if radius is not None:\n"
" geometry['radius'] = radius\n"
" geometry['diameter'] = radius * 2.0\n"
" try:\n"
" geometry['center'] = _xyz(surface.Frame.Origin)\n"
" except Exception:\n"
" pass\n"
" try:\n"
" geometry['axis'] = _xyz(surface.Frame.DirZ)\n"
" except Exception:\n"
" pass\n"
" try:\n"
" center = geometry.get('center') or []\n"
" axis = geometry.get('axis') or []\n"
" if len(center) == 3 and len(axis) == 3:\n"
" geometry['planeOffset'] = center[0] * axis[0] + center[1] * axis[1] + center[2] * axis[2]\n"
" except Exception:\n"
" pass\n"
" if 'plane' in lowered:\n"
" geometry['surfaceType'] = 'plane'\n"
" elif 'cylinder' in lowered:\n"
" geometry['surfaceType'] = 'cylinder'\n"
" round_info = _round_info_from_face(face, geometry)\n"
" if round_info:\n"
" geometry['roundInfo'] = round_info\n"
" return geometry\n"
"\n"
"def _round_info_from_face(face, geometry):\n"
" if str(geometry.get('surfaceType', '')).lower() != 'cylinder':\n"
" return {}\n"
" round_info_type = globals().get('RoundInfo')\n"
" if round_info_type is None:\n"
" return {}\n"
" try:\n"
" info = round_info_type.Create(face)\n"
" except Exception:\n"
" return {}\n"
" payload = {'available': True, 'type': _safe_name(info)}\n"
" for attr in ('Radius', 'RoundRadius', 'ConstantRadius'):\n"
" value = _float_attr(info, (attr, attr[0].lower() + attr[1:]))\n"
" if value is not None:\n"
" payload['radius'] = value\n"
" payload['diameter'] = value * 2.0\n"
" break\n"
" for attr in ('IsConstant', 'IsRound'):\n"
" try:\n"
" payload[attr[0].lower() + attr[1:]] = bool(getattr(info, attr))\n"
" except Exception:\n"
" pass\n"
" try:\n"
" payload['attributes'] = [name for name in dir(info) if not name.startswith('_')][:40]\n"
" except Exception:\n"
" pass\n"
" return payload\n"
"\n"
"def _path_value(value, expr):\n"
" current = value\n"
" for part in expr.split('.'):\n"
" try:\n"
" current = getattr(current, part)\n"
" except Exception:\n"
" return None\n"
" return current\n"
"\n"
"def _first_path_value(value, exprs):\n"
" for expr in exprs:\n"
" result = _path_value(value, expr)\n"
" if result is not None:\n"
" return result\n"
" return None\n"
"\n"
"def _geometry_from_edge(edge):\n"
" geometry = {}\n"
" shape = getattr(edge, 'Shape', edge)\n"
" curve = _first_path_value(edge, ('Shape.Geometry', 'Geometry', 'Shape.Curve', 'Curve', 'Shape')) or shape\n"
" geometry['curveShapeType'] = _safe_name(shape)\n"
" geometry['curveType'] = _safe_name(curve)\n"
" length = _float_attr(edge, ('Length', 'length'))\n"
" if length is None:\n"
" length = _float_attr(shape, ('Length', 'length'))\n"
" if length is not None:\n"
" geometry['length'] = length\n"
" start = _xyz(_first_path_value(edge, ('StartPoint', 'Shape.StartPoint')))\n"
" end = _xyz(_first_path_value(edge, ('EndPoint', 'Shape.EndPoint')))\n"
" if start:\n"
" geometry['startPoint'] = start\n"
" if end:\n"
" geometry['endPoint'] = end\n"
" if len(start) == 3 and len(end) == 3:\n"
" geometry['midPoint'] = [(start[i] + end[i]) * 0.5 for i in range(3)]\n"
" radius = _float_attr(curve, ('Radius', 'radius'))\n"
" if radius is not None:\n"
" geometry['radius'] = radius\n"
" geometry['diameter'] = radius * 2.0\n"
" center = _xyz(_first_path_value(curve, ('Frame.Origin', 'Circle.Frame.Origin')))\n"
" if center:\n"
" geometry['center'] = center\n"
" axis = _xyz(_first_path_value(curve, ('Frame.DirZ', 'Circle.Frame.DirZ')))\n"
" if axis:\n"
" geometry['axis'] = axis\n"
" return geometry\n"
"\n"
"def _edge_adjacent_face_ordinals(edge, face_ordinals_by_marker):\n"
" faces = []\n"
" for expr in ('Faces', 'Shape.Faces', 'GetFaces'):\n"
" value = _path_value(edge, expr)\n"
" if value is None and expr == 'GetFaces':\n"
" value = _maybe_call(edge, 'GetFaces')\n"
" faces = _items(value)\n"
" if faces:\n"
" break\n"
" ordinals = []\n"
" for face in faces:\n"
" marker = str(id(face))\n"
" if marker in face_ordinals_by_marker:\n"
" ordinals.append(face_ordinals_by_marker[marker])\n"
" return {'adjacentFaceCount': len(faces), 'adjacentFaceOrdinals': ordinals}\n"
"\n"
"def _int_or_none(value):\n"
" try:\n"
" return int(value)\n"
" except Exception:\n"
" return None\n"
"\n"
"def _edge_kind(geometry):\n"
" curve_type = str(geometry.get('curveType', '') or geometry.get('curveShapeType', '')).lower()\n"
" if geometry.get('radius') is not None or 'circle' in curve_type or 'arc' in curve_type:\n"
" return 'circular'\n"
" if 'line' in curve_type or 'segment' in curve_type:\n"
" return 'linear'\n"
" return 'other'\n"
"\n"
"def _add_edge_geometry_summary(summary, geometry):\n"
" summary['totalEdgeCount'] = int(summary.get('totalEdgeCount', 0)) + 1\n"
" kind = _edge_kind(geometry)\n"
" kind_counts = summary.setdefault('edgeKindCounts', {})\n"
" kind_counts[kind] = int(kind_counts.get(kind, 0)) + 1\n"
" radius = geometry.get('radius')\n"
" if radius is not None:\n"
" try:\n"
" radius = float(radius)\n"
" summary['circularEdgeCount'] = int(summary.get('circularEdgeCount', 0)) + 1\n"
" values = summary.setdefault('circularRadii', [])\n"
" if len(values) < 80:\n"
" values.append(radius)\n"
" except Exception:\n"
" pass\n"
" length = geometry.get('length')\n"
" if length is not None:\n"
" try:\n"
" length = float(length)\n"
" summary['minEdgeLength'] = min(float(summary.get('minEdgeLength', length)), length)\n"
" summary['maxEdgeLength'] = max(float(summary.get('maxEdgeLength', length)), length)\n"
" except Exception:\n"
" pass\n"
"\n"
"def _final_edge_geometry_summary(summary):\n"
" result = dict(summary)\n"
" radii = result.get('circularRadii')\n"
" if isinstance(radii, list) and radii:\n"
" buckets = {}\n"
" for value in radii:\n"
" try:\n"
" key = '%.6g' % float(value)\n"
" buckets[key] = int(buckets.get(key, 0)) + 1\n"
" except Exception:\n"
" pass\n"
" result['circularRadiusBuckets'] = [\n"
" {'radius': key, 'count': buckets[key]} for key in sorted(buckets.keys())[:40]\n"
" ]\n"
" result.pop('circularRadii', None)\n"
" return result\n"
"\n"
"def _record_face_adjacency(adjacency_map, body_index, edge_topology, geometry):\n"
" ordinals = []\n"
" for value in edge_topology.get('adjacentFaceOrdinals', []) or []:\n"
" number = _int_or_none(value)\n"
" if number is not None and number not in ordinals:\n"
" ordinals.append(number)\n"
" if len(ordinals) < 2:\n"
" return\n"
" ordinals.sort()\n"
" kind = _edge_kind(geometry)\n"
" for left_index in range(len(ordinals)):\n"
" for right_index in range(left_index + 1, len(ordinals)):\n"
" left = ordinals[left_index]\n"
" right = ordinals[right_index]\n"
" key = (body_index, left, right)\n"
" item = adjacency_map.setdefault(\n"
" key,\n"
" {'bodyIndex': body_index, 'faceOrdinals': [left, right], 'edgeCount': 0, 'edgeKinds': {}, 'edges': []},\n"
" )\n"
" item['edgeCount'] = int(item.get('edgeCount', 0)) + 1\n"
" edge_kinds = item.setdefault('edgeKinds', {})\n"
" edge_kinds[kind] = int(edge_kinds.get(kind, 0)) + 1\n"
" edges = item.setdefault('edges', [])\n"
" if len(edges) < 6:\n"
" edges.append({\n"
" 'edgeOrdinal': edge_topology.get('edgeOrdinal'),\n"
" 'globalEdgeOrdinal': edge_topology.get('globalEdgeOrdinal'),\n"
" 'curveType': geometry.get('curveType'),\n"
" 'kind': kind,\n"
" 'length': geometry.get('length'),\n"
" 'radius': geometry.get('radius'),\n"
" })\n"
"\n"
"def _face_adjacency_rows(adjacency_map):\n"
" rows = list(adjacency_map.values())\n"
" rows.sort(key=lambda item: (int(item.get('bodyIndex') or 0), item.get('faceOrdinals') or []))\n"
" return rows\n"
"\n"
"def _count_key(counts, key):\n"
" key = str(key or '').strip() or 'unknown'\n"
" counts[key] = int(counts.get(key, 0)) + 1\n"
"\n"
"def _feature_inventory(objects):\n"
" result = {'objectTypeCounts': {}, 'surfaceTypeCounts': {}, 'curveTypeCounts': {}, 'operationCounts': {}}\n"
" for item in objects:\n"
" if not isinstance(item, dict):\n"
" continue\n"
" _count_key(result['objectTypeCounts'], item.get('objectType'))\n"
" geometry = item.get('geometry')\n"
" if not isinstance(geometry, dict):\n"
" geometry = {}\n"
" if geometry.get('surfaceType') is not None:\n"
" _count_key(result['surfaceTypeCounts'], geometry.get('surfaceType'))\n"
" if geometry.get('curveType') is not None:\n"
" _count_key(result['curveTypeCounts'], geometry.get('curveType'))\n"
" for command in item.get('backendCommandCandidates', []) or []:\n"
" if isinstance(command, dict):\n"
" _count_key(result['operationCounts'], command.get('operation'))\n"
" return result\n"
"\n"
"def _command_candidates(object_type, geometry):\n"
" surface_type = str(geometry.get('surfaceType', '')).lower()\n"
" result = []\n"
" if object_type == 'face' and surface_type == 'plane':\n"
" result.append({'operation': 'pull_face_offset', 'enabled': True, 'parameterFields': {'distance': 0}})\n"
" if object_type in ('face', 'hole') and surface_type == 'cylinder':\n"
" result.append({'operation': 'change_hole_diameter', 'enabled': True, 'parameterFields': {'diameter': geometry.get('diameter')}})\n"
" result.append({'operation': 'move_hole_axis', 'enabled': True, 'parameterFields': {'center': geometry.get('center')}})\n"
" if object_type == 'hole' and surface_type == 'cylinder':\n"
" result.append({'operation': 'fill_feature', 'enabled': True, 'parameterFields': {}})\n"
" round_info = geometry.get('roundInfo')\n"
" if isinstance(round_info, dict) and round_info.get('radius') is not None:\n"
" result.append({'operation': 'change_round_radius', 'enabled': True, 'parameterFields': {'radius': round_info.get('radius')}})\n"
" result.append({'operation': 'delete_round_or_chamfer', 'enabled': True, 'parameterFields': {}})\n"
" return result\n"
"\n"
"def _open_step(path):\n"
" errors = []\n"
" for opener in ('DocumentOpen.Execute', 'Application.OpenDocument'):\n"
" try:\n"
" current = globals()\n"
" target = None\n"
" for part in opener.split('.'):\n"
" target = current.get(part) if isinstance(current, dict) else getattr(current, part)\n"
" current = target\n"
" target(path)\n"
" return\n"
" except Exception as exc:\n"
" errors.append(str(exc))\n"
" raise Exception('Could not open STEP: ' + '; '.join(errors))\n"
"\n"
"def _root_part():\n"
" try:\n"
" return GetRootPart()\n"
" except Exception:\n"
" pass\n"
" try:\n"
" return Application.ActiveWindow.Document.MainPart\n"
" except Exception:\n"
" return None\n"
"\n"
"def _maybe_call(target, name):\n"
" try:\n"
" value = getattr(target, name)\n"
" except Exception:\n"
" return None\n"
" try:\n"
" return value()\n"
" except Exception:\n"
" return value\n"
"\n"
"def _body_faces(body):\n"
" for name in ('Faces', 'GetFaces'):\n"
" items = _items(_maybe_call(body, name))\n"
" if items:\n"
" return items\n"
" return []\n"
"\n"
"def _body_edges(body):\n"
" for name in ('Edges', 'GetEdges'):\n"
" items = _items(_maybe_call(body, name))\n"
" if items:\n"
" return items\n"
" return []\n"
"\n"
"def _child_parts(part):\n"
" children = []\n"
" for name in ('Components', 'GetAllComponents'):\n"
" for component in _items(_maybe_call(part, name)):\n"
" for attr in ('Content', 'ContentMaster', 'Template', 'Part'):\n"
" try:\n"
" value = getattr(component, attr)\n"
" if value is not None:\n"
" children.append(value)\n"
" break\n"
" except Exception:\n"
" pass\n"
" return children\n"
"\n"
"def _all_bodies(root):\n"
" if root is None:\n"
" return []\n"
" for name in ('GetAllBodies', 'Bodies'):\n"
" items = _items(_maybe_call(root, name))\n"
" if items:\n"
" return items\n"
" bodies = []\n"
" queue = [root]\n"
" seen = set()\n"
" while queue:\n"
" part = queue.pop(0)\n"
" marker = str(id(part))\n"
" if marker in seen:\n"
" continue\n"
" seen.add(marker)\n"
" bodies.extend(_items(_maybe_call(part, 'Bodies')))\n"
" queue.extend(_child_parts(part))\n"
" return bodies\n"
"\n"
"def _hole_face_markers(bodies):\n"
" standard_holes = globals().get('StandardHoles')\n"
" if standard_holes is None:\n"
" return set()\n"
" faces = []\n"
" options = None\n"
" options_cls = globals().get('FindStandardHoleOptions')\n"
" if options_cls is not None:\n"
" try:\n"
" options = options_cls()\n"
" except Exception:\n"
" options = None\n"
" identified = []\n"
" find = getattr(standard_holes, 'Find', None)\n"
" if find is not None:\n"
" for args in ((bodies, options, None), (bodies, options), (options, None), (options,), (None,)):\n"
" try:\n"
" identified = _items(find(*args))\n"
" if identified:\n"
" break\n"
" except Exception:\n"
" pass\n"
" if identified:\n"
" try:\n"
" faces = _items(standard_holes.GetHoleFaces(identified))\n"
" except Exception:\n"
" faces = []\n"
" if not faces:\n"
" for hole in identified:\n"
" try:\n"
" faces.extend(_items(getattr(hole, 'Faces')))\n"
" except Exception:\n"
" pass\n"
" if faces:\n"
" return set(str(id(face)) for face in faces)\n"
" for args in ((bodies,), ()):\n"
" try:\n"
" faces = _items(standard_holes.GetHoleFaces(*args))\n"
" if faces:\n"
" break\n"
" except Exception:\n"
" pass\n"
" return set(str(id(face)) for face in faces)\n"
"\n"
"def _available_commands():\n"
" names = ('StandardHoles', 'OffsetFaces', 'Move', 'Fill', 'Delete', 'Chamfer', 'ConstantRound', 'RoundInfo')\n"
" result = []\n"
" for name in names:\n"
" result.append({'name': name, 'available': globals().get(name) is not None})\n"
" return result\n"
"\n"
"def main():\n"
" job = json.load(open(JOB_PATH, 'r'))\n"
" model = job.get('model', {})\n"
" outputs = job.get('outputs', {})\n"
" raw_path = outputs.get('rawFeatures')\n"
" error_path = outputs.get('error')\n"
" try:\n"
" _open_step(model.get('path'))\n"
" root = _root_part()\n"
" bodies = _all_bodies(root)\n"
" hole_face_markers = _hole_face_markers(bodies)\n"
" objects = []\n"
" face_adjacency = {}\n"
" edge_geometry_summary = {}\n"
" face_counter = 0\n"
" edge_counter = 0\n"
" for body_index, body in enumerate(bodies):\n"
" body_faces = _body_faces(body)\n"
" face_ordinals_by_marker = dict((str(id(face)), index) for index, face in enumerate(body_faces))\n"
" for face_index, face in enumerate(body_faces):\n"
" geometry = _geometry_from_face(face)\n"
" object_type = 'hole' if str(id(face)) in hole_face_markers else 'face'\n"
" if object_type == 'face' and isinstance(geometry.get('roundInfo'), dict) and geometry.get('roundInfo', {}).get('radius') is not None:\n"
" object_type = 'round'\n"
" objects.append({\n"
" 'backendId': 'body:%d/face:%d' % (body_index, face_index),\n"
" 'objectType': object_type,\n"
" 'geometry': geometry,\n"
" 'topologyHint': {'bodyIndex': body_index, 'faceOrdinal': face_index, 'globalFaceOrdinal': face_counter},\n"
" 'backendCommandCandidates': _command_candidates(object_type, geometry),\n"
" 'rawLimitations': [],\n"
" })\n"
" face_counter += 1\n"
" for edge_index, edge in enumerate(_body_edges(body)):\n"
" geometry = _geometry_from_edge(edge)\n"
" edge_topology = {'bodyIndex': body_index, 'edgeOrdinal': edge_index, 'globalEdgeOrdinal': edge_counter}\n"
" edge_topology.update(_edge_adjacent_face_ordinals(edge, face_ordinals_by_marker))\n"
" _add_edge_geometry_summary(edge_geometry_summary, geometry)\n"
" _record_face_adjacency(face_adjacency, body_index, edge_topology, geometry)\n"
" objects.append({\n"
" 'backendId': 'body:%d/edge:%d' % (body_index, edge_index),\n"
" 'objectType': 'edge',\n"
" 'geometry': geometry,\n"
" 'topologyHint': edge_topology,\n"
" 'backendCommandCandidates': [],\n"
" 'rawLimitations': [],\n"
" })\n"
" edge_counter += 1\n"
" payload = {\n"
" 'schemaVersion': 1,\n"
" 'backend': job.get('backend', {}),\n"
" 'model': model,\n"
" 'scan': job.get('scan', {}),\n"
" 'objects': objects,\n"
" 'diagnostics': {\n"
" 'availableCommands': _available_commands(),\n"
" 'faceAdjacency': _face_adjacency_rows(face_adjacency),\n"
" 'edgeGeometrySummary': _final_edge_geometry_summary(edge_geometry_summary),\n"
" 'featureInventory': _feature_inventory(objects),\n"
" },\n"
" 'summary': {'bodyCount': len(bodies), 'objectCount': len(objects), 'faceCount': face_counter, 'edgeCount': edge_counter, 'holeFaceCount': len(hole_face_markers)},\n"
" }\n"
" _write_json(raw_path, payload)\n"
" except Exception as exc:\n"
" _write_json(error_path, {'ok': False, 'reason': 'probe-exception', 'message': str(exc), 'traceback': traceback.format_exc()})\n"
" raise\n"
"\n"
"main()\n"
)
__all__ = [
"generate_scdm_probe_script",
"prepare_scdm_probe_job",
"run_scdm_probe",
]