feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
@@ -0,0 +1,680 @@
|
||||
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",
|
||||
]
|
||||
Reference in New Issue
Block a user