from __future__ import annotations import subprocess from collections.abc import Mapping 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_capabilities import capability_definition from .scdm_schema import default_scdm_work_dir, file_fingerprint, read_json, utc_now, write_json SCDM_EDIT_SCHEMA_VERSION = 1 SCDM_EDIT_ADAPTER = "spaceclaim-v1" def prepare_scdm_edit_job( source_step: str | Path, *, capability_key: str, target_value: object, object_signature: Mapping[str, object] | None = None, object_id: str = "", backend_operation: str = "", output_step: str | Path | None = None, output_dir: str | Path | None = None, project_root: str | Path | None = None, backend: ScdmBackendInfo | None = None, timeout_seconds: float = 180.0, rollback_step: str | Path | None = None, context: Mapping[str, object] | None = None, ) -> dict[str, object]: source = Path(source_step).expanduser() if not source.is_file(): return {"ok": False, "reason": "missing-step", "message": f"STEP file not found: {source}"} definition = capability_definition(capability_key) if definition is None: return { "ok": False, "reason": "unsupported-capability", "message": f"SCDM capability is not productized yet: {capability_key}", } if not definition.productized: return { "ok": False, "reason": "capability-not-productized", "message": definition.block_reason or f"SCDM capability is planned but not executable yet: {capability_key}", "capability_key": definition.key, "roadmap_stage": definition.roadmap_stage, } fingerprint = file_fingerprint(source) work_dir = _edit_work_dir(source, fingerprint=fingerprint, output_dir=output_dir, project_root=project_root).resolve(strict=False) work_dir.mkdir(parents=True, exist_ok=True) output_path = Path(output_step).expanduser().resolve(strict=False) if output_step else work_dir / "result.step" result_path = work_dir / "result.json" error_path = work_dir / "error.json" job_path = work_dir / "scdm_edit_job.json" script_path = work_dir / "scdm_edit.py" operation = backend_operation or definition.backend_operation _clear_stale_outputs((output_path, result_path, error_path)) job = { "schemaVersion": SCDM_EDIT_SCHEMA_VERSION, "adapter": SCDM_EDIT_ADAPTER, "createdAt": utc_now(), "backend": { "name": "SCDM", "path": str(backend.path) if backend else "", "version": backend.version if backend else "", }, "model": { "sourceStep": str(source.resolve(strict=False)), "rollbackStep": str(Path(rollback_step).expanduser().resolve(strict=False)) if rollback_step else str(source.resolve(strict=False)), "fingerprint": fingerprint, }, "object": { "objectId": object_id, "geometrySignature": _json_safe(dict(object_signature or {})), }, "target": { "capabilityKey": definition.key, "displayName": definition.display_name, "valueKind": definition.value_kind, "value": _target_value_for_job(target_value, value_kind=definition.value_kind), "text": str(target_value), "backendOperation": operation, "postCheck": definition.post_check, }, "execution": { "mode": "SpaceClaim.exe /RunScript", "timeoutSeconds": max(float(timeout_seconds), 0.1), "isolatedProcess": True, }, "outputs": { "outputStep": str(output_path), "result": str(result_path), "error": str(error_path), }, "context": _json_safe(dict(context or {})), } write_json(job_path, job) script_path.write_text(generate_scdm_edit_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), "output_step": str(output_path), "result_path": str(result_path), "error_path": str(error_path), "model_fingerprint": fingerprint, "capability_key": definition.key, "backend_operation": operation, } def run_scdm_edit_job( source_step: str | Path, *, capability_key: str, target_value: object, object_signature: Mapping[str, object] | None = None, object_id: str = "", backend_operation: str = "", output_step: str | Path | None = None, output_dir: str | Path | None = None, project_root: str | Path | None = None, backend: ScdmBackendInfo | None = None, timeout_seconds: float = 180.0, rollback_step: str | Path | None = None, context: Mapping[str, object] | None = None, 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 = resolved["backend"] # type: ignore[assignment] prepared = prepare_scdm_edit_job( source_step, capability_key=capability_key, target_value=target_value, object_signature=object_signature, object_id=object_id, backend_operation=backend_operation, output_step=output_step, output_dir=output_dir, project_root=project_root, backend=backend, timeout_seconds=timeout_seconds, rollback_step=rollback_step, context=context, ) if not prepared.get("ok"): return prepared result = run_prepared_scdm_edit_job(prepared, backend=backend, timeout_seconds=timeout_seconds, runner=runner) if result.get("ok") is True: try: save_scdm_backend_cache(_edit_verified_backend(backend), project_root_override=project_root) except Exception: pass return result def run_prepared_scdm_edit_job( prepared: Mapping[str, object], *, backend: ScdmBackendInfo, timeout_seconds: float = 180.0, runner: Callable[..., subprocess.CompletedProcess[str]] | None = None, ) -> dict[str, object]: script_path = Path(str(prepared.get("script_path") or "")).expanduser() result_path = Path(str(prepared.get("result_path") or "")).expanduser() error_path = Path(str(prepared.get("error_path") or "")).expanduser() output_step = Path(str(prepared.get("output_step") or "")).expanduser() if not script_path.is_file(): return _with_prepared( prepared, ok=False, reason="missing-script", message=f"SCDM edit script not found: {script_path}", backend=backend.to_cache(), ) 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_error( error_path, reason="timeout", message="SCDM edit timed out.", prepared=prepared, ) return _with_prepared(prepared, ok=False, reason="timeout", message="SCDM edit timed out.") except OSError as exc: _write_error( error_path, reason="launch-failed", message=str(exc), prepared=prepared, ) return _with_prepared(prepared, ok=False, reason="launch-failed", message=str(exc)) return _result_from_completed(completed, prepared=prepared, result_path=result_path, error_path=error_path, output_step=output_step, backend=backend) def generate_scdm_edit_script(job_path: str | Path) -> str: job_literal = repr(str(Path(job_path).expanduser())) return _SCDM_EDIT_SCRIPT_TEMPLATE.replace("__STEP_EDITOR_SCDM_EDIT_JOB_PATH__", job_literal) def _edit_work_dir( source: Path, *, fingerprint: str, output_dir: str | Path | None, project_root: str | Path | None, ) -> Path: if output_dir: return Path(output_dir).expanduser() return default_scdm_work_dir(source, project_root=project_root, fingerprint=fingerprint) / "edit" def _clear_stale_outputs(paths: tuple[Path, ...]) -> None: for path in paths: try: if path.exists(): path.unlink() except OSError: pass def _target_value_for_job(value: object, *, value_kind: str) -> object: if value_kind == "vector3": parsed = _parse_vector3(value) return parsed if parsed else _json_safe(value) if value_kind in {"number", "length", "angle"}: try: return float(str(value).strip()) except (TypeError, ValueError): return _json_safe(value) if value_kind == "command": return True if value in ("", None) else _json_safe(value) return _json_safe(value) def _parse_vector3(value: object) -> list[float]: if isinstance(value, (list, tuple)): items = list(value) elif isinstance(value, str): text = value.strip().strip("()[]") if not text: return [] items = text.replace(",", " ").split() else: return [] if len(items) != 3: return [] try: return [float(item) for item in items] except (TypeError, ValueError): return [] def _json_safe(value: object) -> object: if isinstance(value, Path): return str(value) if isinstance(value, Mapping): return {str(key): _json_safe(item) for key, item in value.items()} if isinstance(value, (list, tuple)): return [_json_safe(item) for item in value] if isinstance(value, (str, int, float, bool)) or value is None: return value return str(value) def _result_from_completed( completed: subprocess.CompletedProcess[str], *, prepared: Mapping[str, object], result_path: Path, error_path: Path, output_step: Path, backend: ScdmBackendInfo, ) -> dict[str, object]: returncode = int(getattr(completed, "returncode", -1)) stdout = str(getattr(completed, "stdout", "") or "") stderr = str(getattr(completed, "stderr", "") or "") if error_path.is_file(): error = _read_json_or_message(error_path) reason = str(error.get("reason") or "scdm-edit-failed") message = str(error.get("message") or stderr or stdout or "SCDM edit failed.") return { **dict(prepared), "ok": False, "reason": reason, "message": message, "returncode": returncode, "stdout": stdout, "stderr": stderr, "error": error, "backend": backend.to_cache(), } if returncode != 0: message = (stderr or stdout or f"SCDM returned {returncode}.").strip() _write_error(error_path, reason="edit-failed", message=message, prepared=prepared, returncode=returncode) return { **dict(prepared), "ok": False, "reason": "edit-failed", "message": message, "returncode": returncode, "stdout": stdout, "stderr": stderr, "backend": backend.to_cache(), } if not result_path.is_file(): message = "SCDM edit finished but did not write result.json." _write_error(error_path, reason="missing-result", message=message, prepared=prepared, returncode=returncode) return _with_prepared(prepared, ok=False, reason="missing-result", message=message, returncode=returncode, backend=backend.to_cache()) result = _read_json_or_message(result_path) if result.get("ok") is not True: reason = str(result.get("reason") or "negative-result") message = str(result.get("message") or "SCDM edit result was not successful.") return _with_prepared(prepared, ok=False, reason=reason, message=message, returncode=returncode, result=result, backend=backend.to_cache()) if not output_step.is_file(): message = f"SCDM edit reported success but output STEP was not written: {output_step}" _write_error(error_path, reason="missing-output-step", message=message, prepared=prepared, returncode=returncode) return { **dict(prepared), "ok": False, "reason": "missing-output-step", "message": message, "returncode": returncode, "result": result, "backend": backend.to_cache(), } try: output_size = output_step.stat().st_size except OSError: output_size = -1 if output_size <= 0: message = f"SCDM edit reported success but output STEP is empty: {output_step}" _write_error(error_path, reason="empty-output-step", message=message, prepared=prepared, returncode=returncode) return { **dict(prepared), "ok": False, "reason": "empty-output-step", "message": message, "returncode": returncode, "result": result, "backend": backend.to_cache(), } return { **dict(prepared), "ok": True, "reason": "ok", "message": str(result.get("message") or "SCDM edit finished."), "returncode": returncode, "stdout": stdout, "stderr": stderr, "result": result, "output_step": str(output_step), "backend": _edit_verified_backend(backend).to_cache(), } def _edit_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 edit completed.", ) def _with_prepared(prepared: Mapping[str, object], **payload: object) -> dict[str, object]: result = dict(prepared) result.update(payload) return result def _write_error(path: Path, *, reason: str, message: str, prepared: Mapping[str, object], **extra: object) -> None: payload = { "ok": False, "reason": reason, "message": message, "createdAt": utc_now(), "jobPath": str(prepared.get("job_path") or ""), "scriptPath": str(prepared.get("script_path") or ""), "outputStep": str(prepared.get("output_step") or ""), } payload.update(extra) write_json(path, payload) def _read_json_or_message(path: Path) -> dict[str, object]: try: return read_json(path) except Exception as exc: return {"ok": False, "reason": "bad-json", "message": str(exc)} _SCDM_EDIT_SCRIPT_TEMPLATE = r''' from __future__ import print_function import json import traceback JOB_PATH = __STEP_EDITOR_SCDM_EDIT_JOB_PATH__ def _write_json(path, payload): handle = open(path, 'w') try: handle.write(json.dumps(payload, indent=2)) finally: handle.close() def _read_job(): handle = open(JOB_PATH, 'r') try: return json.loads(handle.read()) finally: handle.close() def _items(collection): if collection is None: return [] try: return list(collection) except Exception: result = [] try: count = int(collection.Count) for index in range(count): result.append(collection[index]) except Exception: pass return result def _maybe_call(target, name, *args): try: func = getattr(target, name) except Exception: return None try: return func(*args) except Exception: return None def _open_step(path): errors = [] document_open = globals().get('DocumentOpen') if document_open is not None: for args in ((path,),): try: return document_open.Execute(*args) except Exception as exc: errors.append('DocumentOpen.Execute: ' + str(exc)) application = globals().get('Application') if application is not None: for name in ('OpenDocument', 'OpenFile'): try: return getattr(application, name)(path) except Exception as exc: errors.append('Application.' + name + ': ' + str(exc)) raise Exception('open_step_failed: ' + '; '.join(errors)) def _root_part(): get_root_part = globals().get('GetRootPart') if get_root_part is not None: try: return get_root_part() except Exception: pass application = globals().get('Application') if application is not None: for expr in ('ActiveWindow.Document.MainPart', 'ActiveDocument.MainPart', 'Document.MainPart'): value = application ok = True for name in expr.split('.'): try: value = getattr(value, name) except Exception: ok = False break if ok and value is not None: return value raise Exception('root_part_not_found') def _bodies(root): for name in ('Bodies', 'GetAllBodies'): value = _maybe_call(root, name) if value is None: try: value = getattr(root, name) except Exception: value = None items = _items(value) if items: return items return [] def _faces(body): for name in ('Faces', 'GetFaces'): value = _maybe_call(body, name) if value is None: try: value = getattr(body, name) except Exception: value = None items = _items(value) if items: return items return [] def _flat_faces(bodies): result = [] for body in bodies: result.extend(_faces(body)) return result def _int_or_none(value): try: return int(value) except Exception: return None def _append_unique_face(result, face): for existing in result: try: if existing == face: return except Exception: pass try: if existing is face: return except Exception: pass result.append(face) def _locate_one_face(bodies, body_index, face_ordinal, global_face_ordinal): if body_index is not None and face_ordinal is not None and 0 <= body_index < len(bodies): faces = _faces(bodies[body_index]) if 0 <= face_ordinal < len(faces): return faces[face_ordinal] flat_ordinal = global_face_ordinal if global_face_ordinal is not None else face_ordinal if flat_ordinal is not None: flat = _flat_faces(bodies) if 0 <= flat_ordinal < len(flat): return flat[flat_ordinal] return None def _locate_faces(signature): root = _root_part() bodies = _bodies(root) body_index = _int_or_none(signature.get('bodyIndex')) face_ordinal = _int_or_none(signature.get('faceOrdinal')) global_face_ordinal = _int_or_none(signature.get('globalFaceOrdinal')) result = [] locators = signature.get('scdmFaceLocators') if isinstance(locators, list): for locator in locators: if not isinstance(locator, dict): continue face = _locate_one_face( bodies, _int_or_none(locator.get('bodyIndex')), _int_or_none(locator.get('faceOrdinal')), _int_or_none(locator.get('globalFaceOrdinal')), ) if face is not None: _append_unique_face(result, face) face_ordinals = signature.get('faceOrdinals') if isinstance(face_ordinals, list) and body_index is not None: for item in face_ordinals: face = _locate_one_face(bodies, body_index, _int_or_none(item), None) if face is not None: _append_unique_face(result, face) global_face_ordinals = signature.get('globalFaceOrdinals') if isinstance(global_face_ordinals, list): for item in global_face_ordinals: face = _locate_one_face(bodies, None, None, _int_or_none(item)) if face is not None: _append_unique_face(result, face) if result: return result face = _locate_one_face(bodies, body_index, face_ordinal, global_face_ordinal) if face is not None: return [face] raise Exception('object_not_found: geometrySignature does not locate a unique SCDM face') def _selection(items): selection = globals().get('Selection') if selection is not None: try: return selection.Create(items) except Exception: pass if len(items) == 1: try: return selection.Create(items[0]) except Exception: pass return items[0] if len(items) == 1 else items def _import_attr(module_name, attr_name): try: module = __import__(module_name, fromlist=[attr_name]) return getattr(module, attr_name) except Exception: return None def _api_versions(): return ('V23', 'V22', 'V21', 'V20', 'V19', 'V18', 'V17', 'V16') def _command_type(name): value = globals().get(name) if value is not None: return value for version in _api_versions(): value = _import_attr('SpaceClaim.Api.' + version + '.Scripting.Commands', name) if value is not None: return value return None def _command_option_type(name): value = globals().get(name) if value is not None: return value for version in _api_versions(): value = _import_attr('SpaceClaim.Api.' + version + '.Scripting.Commands.CommandOptions', name) if value is not None: return value return None def _selection_type(): value = globals().get('Selection') if value is not None: return value for version in _api_versions(): value = _import_attr('SpaceClaim.Api.' + version + '.Scripting.Selection', 'Selection') if value is not None: return value return None def _geometry_type(name): value = globals().get(name) if value is not None: return value for version in _api_versions(): value = _import_attr('SpaceClaim.Api.' + version + '.Geometry', name) if value is not None: return value return None def _new_options(name): cls = _command_option_type(name) if cls is None: return None try: options = cls() except Exception: return None for attr, value in (('Copy', False), ('DetachFirst', False), ('CreatePatterns', False)): try: if hasattr(options, attr): setattr(options, attr, value) except Exception: pass return options def _empty_selection(): selection = _selection_type() if selection is None: return None for name in ('Empty', 'Create'): try: return getattr(selection, name)() except Exception: pass return None def _fill_mode_three_d(): fill_mode = _command_type('FillMode') if fill_mode is None: return None for name in ('ThreeD', 'ThreeDMode'): try: return getattr(fill_mode, name) except Exception: pass return None def _make_vector(x, y, z): vector_type = _geometry_type('Vector') if vector_type is None: return None for maker in ('Create', 'CreateVector'): try: return getattr(vector_type, maker)(x, y, z) except Exception: pass try: return vector_type(x, y, z) except Exception: return None def _make_direction(x, y, z): direction_type = _geometry_type('Direction') if direction_type is None: return None for maker in ('Create', 'CreateDirection'): try: return getattr(direction_type, maker)(x, y, z) except Exception: pass try: return direction_type(x, y, z) except Exception: return None def _vector_length(values): return (values[0] * values[0] + values[1] * values[1] + values[2] * values[2]) ** 0.5 def _unit_direction_and_distance(values): distance = _vector_length(values) if distance <= 1e-12: return None, 0.0 direction = _make_direction(values[0] / distance, values[1] / distance, values[2] / distance) return direction, distance def _signature_direction(signature): for key in ('normal', 'axis'): value = signature.get(key) if isinstance(value, list) and len(value) == 3: try: direction = _make_direction(float(value[0]), float(value[1]), float(value[2])) if direction is not None: return direction except Exception: pass return None def _call_variants(label, func, variants): errors = [] for args in variants: if args is None: continue try: result = func(*args) return {'ok': True, 'result': result, 'signature': label + '(' + str(len(args)) + ' args)'} except Exception as exc: errors.append(label + '(' + str(len(args)) + ' args): ' + str(exc)) return {'ok': False, 'errors': errors} def _offset_faces(selection, distance, signature): offset_faces = _command_type('OffsetFaces') if offset_faces is None: raise Exception('capability_not_implemented: OffsetFaces command not available') options = _new_options('OffsetFaceOptions') direction = _signature_direction(signature) variants = [] if direction is not None: variants.append((selection, distance, direction, options, None)) variants.append((selection, distance, direction, options)) variants.append((selection, distance, options, None)) variants.append((selection, distance, options)) result = _call_variants('OffsetFaces.Execute', offset_faces.Execute, variants) if result.get('ok'): return {'command': 'OffsetFaces.Execute', 'distance': distance, 'apiSignature': result.get('signature')} raise Exception('capability_not_implemented: OffsetFaces.Execute failed; ' + '; '.join(result.get('errors') or [])) def _translate_selection(selection, delta): move = _command_type('Move') if move is None: raise Exception('capability_not_implemented: Move command not available') options = _new_options('MoveOptions') vector = _make_vector(delta[0], delta[1], delta[2]) variants = [] if vector is not None: variants.append((selection, vector, options, None)) variants.append((selection, vector, options)) direction, distance = _unit_direction_and_distance(delta) if direction is not None: variants.append((selection, direction, distance, options, None)) variants.append((selection, direction, distance, options)) if vector is not None: variants.append((selection, vector)) result = _call_variants('Move.Translate', move.Translate, variants) if result.get('ok'): return {'command': 'Move.Translate', 'delta': delta, 'apiSignature': result.get('signature')} execute = getattr(move, 'Execute', None) if execute is not None and vector is not None: fallback = _call_variants('Move.Execute', execute, ((selection, vector, options), (selection, vector))) if fallback.get('ok'): return {'command': 'Move.Execute', 'delta': delta, 'apiSignature': fallback.get('signature')} result['errors'].extend(fallback.get('errors') or []) raise Exception('capability_not_implemented: Move.Translate failed; ' + '; '.join(result.get('errors') or [])) def _fill_selection(selection): fill = _command_type('Fill') if fill is None: raise Exception('capability_not_implemented: Fill command not available') options = _new_options('FillOptions') for attr, value in (('AutoExtendFillArea', True), ('PatchBlend', True), ('ZipLaminarEdges', True)): try: if options is not None and hasattr(options, attr): setattr(options, attr, value) except Exception: pass secondary = _empty_selection() mode = _fill_mode_three_d() variants = [] if mode is not None: variants.append((selection, secondary, options, mode, None)) variants.append((selection, secondary, options, mode)) variants.append((selection, None, options, mode, None)) variants.append((selection, None, options, mode)) variants.append((selection, secondary, options, None, None)) variants.append((selection, secondary, options, None)) variants.append((selection, options, None)) variants.append((selection, options)) variants.append((selection, None)) variants.append((selection,)) result = _call_variants('Fill.Execute', fill.Execute, variants) if result.get('ok'): return {'command': 'Fill.Execute', 'apiSignature': result.get('signature')} delete = _command_type('Delete') if delete is not None: fallback = _call_variants('Delete.Execute', delete.Execute, ((selection,),)) if fallback.get('ok'): return {'command': 'Delete.Execute', 'apiSignature': fallback.get('signature'), 'fillErrors': result.get('errors') or []} result['errors'].extend(fallback.get('errors') or []) raise Exception('capability_not_implemented: Fill.Execute failed; ' + '; '.join(result.get('errors') or [])) def _float_value(value): try: return float(value) except Exception: raise Exception('target_value_not_numeric: ' + str(value)) def _vector3(value): if not isinstance(value, list) or len(value) != 3: raise Exception('target_value_not_vector3: ' + str(value)) return [float(value[0]), float(value[1]), float(value[2])] def _save_step(path): errors = [] for name in ('DocumentSave', 'DocumentSaveAs'): command = globals().get(name) if command is None: command = _command_type(name) if command is None: continue try: command.Execute(path) return {'command': name + '.Execute'} except Exception as exc: errors.append(name + '.Execute: ' + str(exc)) application = globals().get('Application') if application is not None: for expr in ('ActiveWindow.Document', 'ActiveDocument', 'Document'): document = application ok = True for name in expr.split('.'): try: document = getattr(document, name) except Exception: ok = False break if not ok or document is None: continue for method in ('SaveAs', 'SaveCopyAs', 'Export'): try: getattr(document, method)(path) return {'command': expr + '.' + method} except Exception as exc: errors.append(expr + '.' + method + ': ' + str(exc)) raise Exception('save_step_failed: ' + '; '.join(errors)) def change_hole_diameter(job): target = _float_value(job['target']['value']) if target <= 0: raise Exception('target_value_illegal: diameter must be positive') radius = target / 2.0 signature = job['object'].get('geometrySignature') or {} faces = _locate_faces(signature) selection = _selection(faces) errors = [] standard_holes = _command_type('StandardHoles') if standard_holes is not None: for args in ((selection, radius, None), (selection, radius), (faces, radius, None), (faces, radius)): try: standard_holes.ModifyHoleRadius(*args) return {'command': 'StandardHoles.ModifyHoleRadius', 'targetRadius': radius, 'apiSignature': str(len(args)) + ' args'} except Exception as exc: errors.append('StandardHoles.ModifyHoleRadius: ' + str(exc)) current_radius = None for key in ('radius',): try: current_radius = float(signature.get(key)) break except Exception: pass if current_radius is None: try: current_radius = float(signature.get('diameter')) / 2.0 except Exception: current_radius = None if current_radius is not None: # For internal cylindrical hole walls, SpaceClaim's positive face offset # follows the face normal into the void and reduces the measured radius. radial_delta = current_radius - radius if abs(radial_delta) <= 1e-12: return {'command': 'noop', 'targetRadius': radius, 'reason': 'target already reached'} try: applied = _offset_faces(selection, radial_delta, signature) applied['targetRadius'] = radius applied['radialDelta'] = radial_delta applied['standardHoleErrors'] = errors return applied except Exception as exc: errors.append('OffsetFaces.Execute: ' + str(exc)) raise Exception('capability_not_implemented: hole.diameter adapter failed; ' + '; '.join(errors)) def move_hole_axis(job): target = _vector3(job['target']['value']) signature = job['object'].get('geometrySignature') or {} center = signature.get('center') or [] if not isinstance(center, list) or len(center) != 3: raise Exception('object_signature_missing_center') delta = [float(target[index]) - float(center[index]) for index in range(3)] if _vector_length(delta) <= 1e-12: return {'command': 'noop', 'delta': delta, 'reason': 'target already reached'} faces = _locate_faces(signature) selection = _selection(faces) try: return _translate_selection(selection, delta) except Exception as exc: raise Exception('capability_not_implemented: hole.position adapter failed; ' + str(exc)) def move_slot(job): target = _vector3(job['target']['value']) signature = job['object'].get('geometrySignature') or {} center = signature.get('center') or signature.get('axisCenter') or [] if not isinstance(center, list) or len(center) != 3: raise Exception('object_signature_missing_center') delta = [float(target[index]) - float(center[index]) for index in range(3)] if _vector_length(delta) <= 1e-12: return {'command': 'noop', 'delta': delta, 'reason': 'target already reached'} faces = _locate_faces(signature) selection = _selection(faces) try: applied = _translate_selection(selection, delta) applied['targetCenter'] = target return applied except Exception as exc: raise Exception('capability_not_implemented: slot.position adapter failed; ' + str(exc)) def move_boss(job): target = _vector3(job['target']['value']) signature = job['object'].get('geometrySignature') or {} center = signature.get('center') or signature.get('axisCenter') or [] if not isinstance(center, list) or len(center) != 3: raise Exception('object_signature_missing_center') delta = [float(target[index]) - float(center[index]) for index in range(3)] if _vector_length(delta) <= 1e-12: return {'command': 'noop', 'delta': delta, 'reason': 'target already reached'} faces = _locate_faces(signature) selection = _selection(faces) try: applied = _translate_selection(selection, delta) applied['targetCenter'] = target return applied except Exception as exc: raise Exception('capability_not_implemented: boss.position adapter failed; ' + str(exc)) def pull_face_offset(job): target = _float_value(job['target']['value']) signature = job['object'].get('geometrySignature') or {} current = None try: current = float(signature.get('planeOffset')) except Exception: current = None distance = target - current if current is not None else target if abs(distance) <= 1e-12: return {'command': 'noop', 'distance': distance, 'reason': 'target already reached'} faces = _locate_faces(signature) selection = _selection(faces) try: applied = _offset_faces(selection, distance, signature) applied['targetPlaneOffset'] = target return applied except Exception as exc: raise Exception('capability_not_implemented: face.offset adapter failed; ' + str(exc)) def fill_feature(job): faces = _locate_faces(job['object'].get('geometrySignature') or {}) selection = _selection(faces) try: return _fill_selection(selection) except Exception as exc: raise Exception('capability_not_implemented: feature.fill adapter failed; ' + str(exc)) def _apply_edit(job): operation = job['target'].get('backendOperation') or '' if operation == 'change_hole_diameter': return change_hole_diameter(job) if operation == 'move_hole_axis': return move_hole_axis(job) if operation == 'move_slot': return move_slot(job) if operation == 'move_boss': return move_boss(job) if operation == 'pull_face_offset': return pull_face_offset(job) if operation == 'fill_feature': return fill_feature(job) raise Exception('unsupported_backend_operation: ' + operation) def main(): job = _read_job() outputs = job.get('outputs') or {} error_path = outputs.get('error') or 'error.json' result_path = outputs.get('result') or 'result.json' output_step = outputs.get('outputStep') or 'result.step' try: _open_step(job['model']['sourceStep']) applied = _apply_edit(job) saved = _save_step(output_step) _write_json(result_path, { 'ok': True, 'reason': 'ok', 'message': 'SCDM edit finished.', 'capabilityKey': job['target'].get('capabilityKey'), 'backendOperation': job['target'].get('backendOperation'), 'objectId': job['object'].get('objectId'), 'targetValue': job['target'].get('value'), 'outputStep': output_step, 'applied': applied, 'saved': saved, }) except Exception as exc: _write_json(error_path, { 'ok': False, 'reason': str(exc).split(':', 1)[0].replace('_', '-'), 'message': str(exc), 'traceback': traceback.format_exc(), 'capabilityKey': (job.get('target') or {}).get('capabilityKey') if 'job' in locals() else '', 'backendOperation': (job.get('target') or {}).get('backendOperation') if 'job' in locals() else '', 'objectId': (job.get('object') or {}).get('objectId') if 'job' in locals() else '', 'outputStep': output_step if 'output_step' in locals() else '', }) raise main() ''' __all__ = [ "SCDM_EDIT_ADAPTER", "SCDM_EDIT_SCHEMA_VERSION", "generate_scdm_edit_script", "prepare_scdm_edit_job", "run_prepared_scdm_edit_job", "run_scdm_edit_job", ]