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

1146 lines
39 KiB
Python
Raw Normal View History

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",
]