mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-06 18:40:57 +00:00
feat: Python Plugins
This commit is contained in:
475
docs/plugins/examples/host_ui_panel.py
Normal file
475
docs/plugins/examples/host_ui_panel.py
Normal file
@@ -0,0 +1,475 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = ["numpy"]
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Host Inspector Panel"
|
||||
# description = "A non-modal interactive panel that browses the whole orca.host read-only API."
|
||||
# author = "OrcaSlicer"
|
||||
# version = "1.0.0"
|
||||
# ///
|
||||
"""Host Inspector — a worked sample for the interactive `orca.host.ui` API that
|
||||
also walks the entire `orca.host` read-only surface.
|
||||
|
||||
Run it from the Plugins dialog. It opens a NON-MODAL window (so OrcaSlicer stays
|
||||
usable) with three expandable rows — Plater, Presets, Model. Each row shows a one
|
||||
line summary; click its triangle to reveal the full detail for that section. The
|
||||
page talks to the plugin through the injected `window.orca` bridge:
|
||||
|
||||
page --orca.postMessage({command:'refresh'})--> plugin.on_message()
|
||||
page --orca.postMessage({command:'detail',section})--> plugin.on_message()
|
||||
plugin --win.post({command:'summary'|'detail', ...})--> page (orca.onMessage)
|
||||
|
||||
Detail is fetched lazily — the heavy Model walk (mesh geometry + numpy arrays)
|
||||
runs only when you expand the Model row. Click "Refresh" after changing the plate
|
||||
and the summaries update; any section left open re-fetches its detail to stay live.
|
||||
|
||||
numpy is declared as a dependency so the zero-copy mesh arrays and 4x4 matrices
|
||||
are available; the code still degrades gracefully if it is missing.
|
||||
"""
|
||||
|
||||
import orca
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
except Exception:
|
||||
np = None
|
||||
|
||||
|
||||
# Soft caps so a heavy scene cannot produce an unusably long detail block. When a
|
||||
# cap trims output we say so explicitly rather than truncating silently.
|
||||
MAX_OBJECTS = 25
|
||||
MAX_VOLUMES = 10
|
||||
MAX_INSTANCES = 10
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# small formatting helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
def fmt_vec(v):
|
||||
"""Format an (x, y, z) tuple / 3-element sequence with mm precision."""
|
||||
return "(" + ", ".join(f"{float(c):.3f}" for c in v) + ")"
|
||||
|
||||
|
||||
def fmt_bbox(bb):
|
||||
"""Format a host.BoundingBox as min / max / size (mm)."""
|
||||
if not bb.defined:
|
||||
return "<undefined>"
|
||||
return f"min{fmt_vec(bb.min)} max{fmt_vec(bb.max)} size{fmt_vec(bb.size)}"
|
||||
|
||||
|
||||
def vol_type_name(volume):
|
||||
"""Readable name of a ModelVolumeType enum value."""
|
||||
t = volume.type()
|
||||
return getattr(t, "name", str(t))
|
||||
|
||||
|
||||
class Report:
|
||||
"""Accumulates indented key/value lines into one block of text."""
|
||||
|
||||
def __init__(self):
|
||||
self._lines = []
|
||||
|
||||
def line(self, text=""):
|
||||
self._lines.append(text)
|
||||
|
||||
def kv(self, indent, key, value, width=15):
|
||||
pad = " " * indent
|
||||
self._lines.append(f"{pad}{(key + ':'):<{width}} {value}")
|
||||
|
||||
def text(self):
|
||||
return "\n".join(self._lines)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# section builders — each guards itself so one failure doesn't sink the report
|
||||
# --------------------------------------------------------------------------- #
|
||||
def report_plater(r, plater):
|
||||
r.line("[Plater]")
|
||||
try:
|
||||
r.kv(2, "project dirty", plater.is_project_dirty())
|
||||
r.kv(2, "presets dirty", plater.is_presets_dirty())
|
||||
r.kv(2, "in snapshot", plater.inside_snapshot_capture())
|
||||
except Exception as exc:
|
||||
r.kv(2, "error", exc)
|
||||
r.line()
|
||||
|
||||
|
||||
def report_presets(r, bundle):
|
||||
r.line("[Presets]")
|
||||
try:
|
||||
pp = bundle.current_print_preset()
|
||||
r.kv(2, "process", f"{pp.name} (system={pp.is_system} dirty={pp.is_dirty})")
|
||||
|
||||
printer = bundle.current_printer_preset()
|
||||
r.kv(2, "printer", f"{printer.name} (system={printer.is_system})")
|
||||
|
||||
r.kv(2, "filaments", bundle.current_filament_preset_names())
|
||||
|
||||
# PresetCollection access: sizes + currently selected names.
|
||||
r.kv(2, "collections",
|
||||
f"prints={bundle.prints.size()} "
|
||||
f"printers={bundle.printers.size()} "
|
||||
f"filaments={bundle.filaments.size()}")
|
||||
|
||||
# full_config_value(): a few representative keys, only if present.
|
||||
keys = ("layer_height", "nozzle_diameter", "filament_type",
|
||||
"printer_model", "sparse_infill_density")
|
||||
samples = []
|
||||
for key in keys:
|
||||
value = bundle.full_config_value(key)
|
||||
if value is not None:
|
||||
samples.append(f"{key}={value}")
|
||||
if samples:
|
||||
r.kv(2, "config sample", " | ".join(samples))
|
||||
r.kv(2, "config keys", f"{len(bundle.full_config_keys())} total")
|
||||
except Exception as exc:
|
||||
r.kv(2, "error", exc)
|
||||
r.line()
|
||||
|
||||
|
||||
def report_mesh(r, indent, mesh, instance, volume):
|
||||
"""Detail a host.TriangleMesh: counts, samples, and (numpy) arrays."""
|
||||
r.kv(indent, "vertices", mesh.vertex_count())
|
||||
r.kv(indent, "triangles", mesh.triangle_count())
|
||||
r.kv(indent, "empty", mesh.is_empty())
|
||||
r.kv(indent, "manifold", mesh.is_manifold())
|
||||
r.kv(indent, "volume", f"{mesh.volume():.3f} mm^3")
|
||||
r.kv(indent, "bbox", fmt_bbox(mesh.bounding_box()))
|
||||
if not mesh.is_empty():
|
||||
# numpy-free element access (always available, bounds-checked).
|
||||
r.kv(indent, "vertex[0]", fmt_vec(mesh.vertex(0)))
|
||||
r.kv(indent, "triangle[0]", tuple(mesh.triangle(0)))
|
||||
|
||||
if np is None:
|
||||
r.kv(indent, "numpy", 'not installed (add dependencies=["numpy"])')
|
||||
return
|
||||
|
||||
try:
|
||||
V = np.asarray(mesh.vertices()) # (N, 3) float32, read-only, zero-copy
|
||||
T = np.asarray(mesh.triangles()) # (M, 3) int32, read-only, zero-copy
|
||||
N = np.asarray(mesh.face_normals()) # (M, 3) float32, computed copy
|
||||
r.kv(indent, "np vertices",
|
||||
f"shape={V.shape} dtype={V.dtype} "
|
||||
f"writeable={V.flags.writeable} zero_copy={V.base is not None}")
|
||||
r.kv(indent, "np triangles", f"shape={T.shape} dtype={T.dtype}")
|
||||
r.kv(indent, "np normals", f"shape={N.shape} dtype={N.dtype}")
|
||||
|
||||
# World-space bounding box for this volume under `instance`, using the
|
||||
# row-vector convention world = [V 1] @ (instance @ volume).T
|
||||
if instance is not None and V.size:
|
||||
M = instance.matrix() @ volume.matrix() # 4x4 float64
|
||||
homog = np.c_[V.astype(np.float64), np.ones(len(V))]
|
||||
world = (homog @ M.T)[:, :3]
|
||||
r.kv(indent, "world bbox",
|
||||
f"min{fmt_vec(world.min(0))} max{fmt_vec(world.max(0))}")
|
||||
except Exception as exc:
|
||||
r.kv(indent, "numpy", f"<error: {exc}>")
|
||||
|
||||
|
||||
def report_volume(r, index, volume, instance):
|
||||
r.line(f" Volume[{index}] '{volume.name}' type={vol_type_name(volume)}")
|
||||
try:
|
||||
r.kv(6, "roles",
|
||||
f"part={volume.is_model_part()} modifier={volume.is_modifier()} "
|
||||
f"negative={volume.is_negative_volume()} "
|
||||
f"support_enf={volume.is_support_enforcer()} "
|
||||
f"support_blk={volume.is_support_blocker()}")
|
||||
r.kv(6, "extruder_id", volume.extruder_id())
|
||||
r.kv(6, "offset", fmt_vec(volume.offset()))
|
||||
r.kv(6, "rotation", fmt_vec(volume.rotation()))
|
||||
r.kv(6, "scale", fmt_vec(volume.scaling_factor()))
|
||||
r.kv(6, "mirror", fmt_vec(volume.mirror()))
|
||||
r.kv(6, "facets", volume.facets_count())
|
||||
r.kv(6, "manifold", volume.is_manifold())
|
||||
r.kv(6, "mesh errors", volume.mesh_errors_count())
|
||||
r.kv(6, "painted",
|
||||
f"support={volume.is_fdm_support_painted()} "
|
||||
f"seam={volume.is_seam_painted()} "
|
||||
f"mm={volume.is_mm_painted()} "
|
||||
f"fuzzy={volume.is_fuzzy_skin_painted()}")
|
||||
r.kv(6, "config keys", len(volume.config_keys()))
|
||||
r.line(" mesh:")
|
||||
report_mesh(r, 8, volume.mesh(), instance, volume)
|
||||
except Exception as exc:
|
||||
r.kv(6, "error", exc)
|
||||
|
||||
|
||||
def report_instance(r, index, instance):
|
||||
r.line(f" Instance[{index}]")
|
||||
try:
|
||||
r.kv(6, "printable", instance.printable)
|
||||
r.kv(6, "is_printable", instance.is_printable())
|
||||
r.kv(6, "offset", fmt_vec(instance.offset()))
|
||||
r.kv(6, "rotation", fmt_vec(instance.rotation()))
|
||||
r.kv(6, "scale", fmt_vec(instance.scaling_factor()))
|
||||
r.kv(6, "mirror", fmt_vec(instance.mirror()))
|
||||
r.kv(6, "left_handed", instance.is_left_handed())
|
||||
r.kv(6, "world bbox", fmt_bbox(instance.bounding_box()))
|
||||
except Exception as exc:
|
||||
r.kv(6, "error", exc)
|
||||
|
||||
|
||||
def report_object(r, index, obj):
|
||||
r.line(f" Object[{index}] '{obj.name}'")
|
||||
try:
|
||||
r.kv(4, "input_file", obj.input_file or "<none>")
|
||||
r.kv(4, "module_name", obj.module_name or "<none>")
|
||||
r.kv(4, "printable", obj.printable)
|
||||
r.kv(4, "volumes", obj.volume_count())
|
||||
r.kv(4, "instances", obj.instance_count())
|
||||
r.kv(4, "facets", obj.facets_count())
|
||||
r.kv(4, "parts", obj.parts_count())
|
||||
r.kv(4, "materials", obj.materials_count())
|
||||
r.kv(4, "mesh errors", obj.mesh_errors_count())
|
||||
r.kv(4, "flags",
|
||||
f"multiparts={obj.is_multiparts()} cut={obj.is_cut()} "
|
||||
f"custom_layering={obj.has_custom_layering()}")
|
||||
r.kv(4, "painted",
|
||||
f"support={obj.is_fdm_support_painted()} "
|
||||
f"seam={obj.is_seam_painted()} "
|
||||
f"mm={obj.is_mm_painted()} "
|
||||
f"fuzzy={obj.is_fuzzy_skin_painted()}")
|
||||
r.kv(4, "z range", f"[{obj.min_z():.3f}, {obj.max_z():.3f}]")
|
||||
r.kv(4, "bbox", fmt_bbox(obj.bounding_box()))
|
||||
r.kv(4, "raw bbox", fmt_bbox(obj.raw_mesh_bounding_box()))
|
||||
|
||||
# The first instance is used as the frame for world-space mesh maths.
|
||||
instance0 = obj.instance(0) if obj.instance_count() else None
|
||||
|
||||
shown = min(obj.volume_count(), MAX_VOLUMES)
|
||||
for vi in range(shown):
|
||||
report_volume(r, vi, obj.volume(vi), instance0)
|
||||
if obj.volume_count() > shown:
|
||||
r.line(f" ... and {obj.volume_count() - shown} more volume(s)")
|
||||
|
||||
shown = min(obj.instance_count(), MAX_INSTANCES)
|
||||
for ii in range(shown):
|
||||
report_instance(r, ii, obj.instance(ii))
|
||||
if obj.instance_count() > shown:
|
||||
r.line(f" ... and {obj.instance_count() - shown} more instance(s)")
|
||||
except Exception as exc:
|
||||
r.kv(4, "error", exc)
|
||||
r.line()
|
||||
|
||||
|
||||
def report_model(r, model):
|
||||
r.line(f"[Model] id={model.id()}")
|
||||
try:
|
||||
r.kv(2, "objects", model.object_count())
|
||||
r.kv(2, "materials", model.material_count())
|
||||
r.kv(2, "current plate", model.current_plate_index())
|
||||
r.kv(2, "max_z", f"{model.max_z():.3f} mm")
|
||||
r.kv(2, "painted",
|
||||
f"support={model.is_fdm_support_painted()} "
|
||||
f"seam={model.is_seam_painted()} "
|
||||
f"mm={model.is_mm_painted()} "
|
||||
f"fuzzy={model.is_fuzzy_skin_painted()}")
|
||||
r.kv(2, "designer", repr(model.designer()))
|
||||
r.kv(2, "design_id", repr(model.design_id()))
|
||||
r.kv(2, "bbox exact", fmt_bbox(model.bounding_box()))
|
||||
r.kv(2, "bbox approx", fmt_bbox(model.bounding_box_approx()))
|
||||
except Exception as exc:
|
||||
r.kv(2, "error", exc)
|
||||
r.line()
|
||||
|
||||
count = model.object_count()
|
||||
if count == 0:
|
||||
r.line(" (no objects on the plate)")
|
||||
r.line()
|
||||
return
|
||||
|
||||
shown = min(count, MAX_OBJECTS)
|
||||
for oi in range(shown):
|
||||
report_object(r, oi, model.object(oi))
|
||||
if count > shown:
|
||||
r.line(f" ... and {count - shown} more object(s)")
|
||||
r.line()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# one-line summaries (collapsed row text) — pure formatters
|
||||
# --------------------------------------------------------------------------- #
|
||||
def summary_plater(plater):
|
||||
return f"project dirty: {'yes' if plater.is_project_dirty() else 'no'}"
|
||||
|
||||
|
||||
def summary_presets(bundle):
|
||||
return f"printer: {bundle.current_printer_preset().name}"
|
||||
|
||||
|
||||
def summary_model(model):
|
||||
return f"{model.object_count()} object(s) · max_z {model.max_z():.1f} mm"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# the page — three expandable section rows, themed, self-contained
|
||||
# --------------------------------------------------------------------------- #
|
||||
#
|
||||
# No hardcoded colors: the host injects a theme matching OrcaSlicer's current
|
||||
# light/dark mode and exposes it as CSS variables (--orca-bg, --orca-fg,
|
||||
# --orca-muted, --orca-accent, --orca-border). The page only adds layout and
|
||||
# reuses those variables, so it follows the active theme automatically.
|
||||
PAGE = r"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
header { display:flex; gap:8px; align-items:center; padding:10px 14px;
|
||||
border-bottom:1px solid var(--orca-border); }
|
||||
header h1 { font-size:14px; margin:0; flex:1; }
|
||||
button.secondary { background:transparent; color:var(--orca-fg);
|
||||
border-color:var(--orca-border); }
|
||||
.section { border-bottom:1px solid var(--orca-border); }
|
||||
.row { display:flex; align-items:center; gap:8px; padding:8px 14px;
|
||||
cursor:pointer; user-select:none; }
|
||||
.row:hover { background:var(--orca-border); }
|
||||
.tri { width:1em; color:var(--orca-muted); }
|
||||
.label { font-weight:600; }
|
||||
.sum { color:var(--orca-muted); }
|
||||
pre.detail { margin:0; padding:8px 14px 14px 32px;
|
||||
font:12px ui-monospace,SFMono-Regular,Menlo,monospace;
|
||||
white-space:pre-wrap; word-break:break-word; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Host Inspector</h1>
|
||||
<button onclick="refresh()">Refresh</button>
|
||||
<button class="secondary" onclick="orca.close()">Close</button>
|
||||
</header>
|
||||
|
||||
<div class="section">
|
||||
<div class="row" onclick="toggle('plater')">
|
||||
<span class="tri" id="tri-plater">▶</span><span class="label">Plater</span>
|
||||
<span class="sum" id="sum-plater">…</span>
|
||||
</div>
|
||||
<pre class="detail" id="det-plater" style="display:none"></pre>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="row" onclick="toggle('presets')">
|
||||
<span class="tri" id="tri-presets">▶</span><span class="label">Presets</span>
|
||||
<span class="sum" id="sum-presets">…</span>
|
||||
</div>
|
||||
<pre class="detail" id="det-presets" style="display:none"></pre>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="row" onclick="toggle('model')">
|
||||
<span class="tri" id="tri-model">▶</span><span class="label">Model</span>
|
||||
<span class="sum" id="sum-model">…</span>
|
||||
</div>
|
||||
<pre class="detail" id="det-model" style="display:none"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var KEYS = ['plater', 'presets', 'model'];
|
||||
var expanded = {}, loaded = {};
|
||||
|
||||
function toggle(key) {
|
||||
expanded[key] = !expanded[key];
|
||||
document.getElementById('tri-' + key).innerHTML = expanded[key] ? '▼' : '▶';
|
||||
var det = document.getElementById('det-' + key);
|
||||
det.style.display = expanded[key] ? 'block' : 'none';
|
||||
if (expanded[key] && !loaded[key]) requestDetail(key);
|
||||
}
|
||||
|
||||
function requestDetail(key) {
|
||||
document.getElementById('det-' + key).textContent = 'Loading…';
|
||||
orca.postMessage({ command: 'detail', section: key });
|
||||
}
|
||||
|
||||
function refresh() { orca.postMessage({ command: 'refresh' }); }
|
||||
|
||||
orca.onMessage(function (msg) {
|
||||
if (!msg) return;
|
||||
if (msg.command === 'summary') {
|
||||
var d = msg.data || {};
|
||||
KEYS.forEach(function (key) {
|
||||
document.getElementById('sum-' + key).textContent = d[key] || '';
|
||||
if (expanded[key]) { loaded[key] = false; requestDetail(key); } // keep open rows live
|
||||
});
|
||||
} else if (msg.command === 'detail') {
|
||||
var det = document.getElementById('det-' + msg.section);
|
||||
if (det) det.textContent = msg.text;
|
||||
loaded[msg.section] = true;
|
||||
}
|
||||
});
|
||||
|
||||
refresh(); // initial summaries; rows start collapsed
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# the plugin
|
||||
# --------------------------------------------------------------------------- #
|
||||
class HostInspectorPanel(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Host Inspector"
|
||||
|
||||
def execute(self):
|
||||
# Non-modal: returns immediately. The window is host-owned and lives on
|
||||
# after execute() returns; on_message keeps firing when the page posts.
|
||||
self.win = orca.host.ui.create_window(
|
||||
title="Host Inspector",
|
||||
html=PAGE,
|
||||
width=760,
|
||||
height=560,
|
||||
on_message=self.on_message,
|
||||
on_close=self.on_close,
|
||||
)
|
||||
return orca.ExecutionResult.success("Host Inspector opened.")
|
||||
|
||||
# Called on the UI thread when the page posts a message.
|
||||
def on_message(self, msg):
|
||||
msg = msg or {}
|
||||
command = msg.get("command")
|
||||
if command == "refresh":
|
||||
self.win.post({"command": "summary", "data": self.summaries()})
|
||||
elif command == "detail":
|
||||
section = msg.get("section", "")
|
||||
self.win.post({"command": "detail", "section": section,
|
||||
"text": self.detail(section)})
|
||||
|
||||
def on_close(self):
|
||||
print("Host Inspector closed")
|
||||
|
||||
@staticmethod
|
||||
def summaries():
|
||||
"""One-line summary per section; each guarded independently so one
|
||||
failure (or a not-ready host) shows only that row's error."""
|
||||
def safe(fn):
|
||||
try:
|
||||
return fn()
|
||||
except Exception as exc:
|
||||
return f"<error: {exc}>"
|
||||
return {
|
||||
"plater": safe(lambda: summary_plater(orca.host.plater())),
|
||||
"presets": safe(lambda: summary_presets(orca.host.preset_bundle())),
|
||||
"model": safe(lambda: summary_model(orca.host.model())),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def detail(section):
|
||||
"""Full monospace report for one section, built on demand."""
|
||||
try:
|
||||
r = Report()
|
||||
if section == "plater":
|
||||
report_plater(r, orca.host.plater())
|
||||
elif section == "presets":
|
||||
report_presets(r, orca.host.preset_bundle())
|
||||
elif section == "model":
|
||||
report_model(r, orca.host.model())
|
||||
else:
|
||||
return f"<unknown section: {section}>"
|
||||
return r.text()
|
||||
except Exception as exc:
|
||||
return f"<error: {exc}>"
|
||||
|
||||
|
||||
@orca.plugin
|
||||
class HostInspectorPlugin(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(HostInspectorPanel)
|
||||
141
docs/plugins/examples/multi_capability_skeleton.py
Normal file
141
docs/plugins/examples/multi_capability_skeleton.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Capability Skeleton"
|
||||
# description = "Starter template: one plugin package that registers several different capabilities."
|
||||
# author = "Your Name"
|
||||
# version = "1.0.0"
|
||||
# ///
|
||||
"""Multi-capability plugin skeleton.
|
||||
|
||||
Copy this file into its own folder under ``data_dir()/orca_plugins/<your-plugin>/``
|
||||
and adapt it. It shows the full shape of a plugin that offers more than one
|
||||
capability:
|
||||
|
||||
* ExampleScript - a ``script`` capability (runs from the Plugins dialog)
|
||||
* ExamplePostProcess - a ``post-processing`` capability (edits exported G-code)
|
||||
* ExamplePrinterAgent - a ``printer-connection`` capability (a network printer agent)
|
||||
|
||||
A plugin is a *package* (the ``@orca.plugin`` class at the bottom) that registers one
|
||||
or more *capabilities*. Each capability is an independent class with its own name and
|
||||
type. To make your own plugin: delete the capabilities you do not need, fill in the
|
||||
ones you keep, and register only those in ``register_capabilities``.
|
||||
|
||||
See ``plugin_development.md`` for the full reference, and ``host_ui_panel.py`` for a
|
||||
richer worked example built on the ``orca.host`` read-only API.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import orca
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Capability 1 - a script capability
|
||||
# Runs on the main/UI thread via the Plugins dialog "Run" action. Keep
|
||||
# execute() fast: a slow call freezes the UI. Offload heavy work to your own
|
||||
# threading.Thread (which must not touch the model) and surface results through
|
||||
# an orca.host.ui window (see host_ui_panel.py).
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ExampleScript(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
# Display name; unique within this plugin; must not contain ';'.
|
||||
return "Example Script"
|
||||
|
||||
def on_load(self):
|
||||
# Optional. Runs once when the capability is loaded. Default: no-op.
|
||||
pass
|
||||
|
||||
def on_unload(self):
|
||||
# Optional. Runs once when the capability is unloaded. Default: no-op.
|
||||
pass
|
||||
|
||||
def execute(self):
|
||||
# TODO: your logic here.
|
||||
return orca.ExecutionResult.success("Example Script ran")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Capability 2 - a post-processing (G-code) capability
|
||||
# Runs on a background slicing thread during G-code export. Receives a context
|
||||
# pointing at the temporary G-code file, which you may rewrite in place.
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ExamplePostProcess(orca.gcode.GCodePluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Example Post-process"
|
||||
|
||||
def execute(self, ctx):
|
||||
# ctx.gcode_path - absolute path to the temp G-code being post-processed
|
||||
# ctx.output_name - the output file name
|
||||
# ctx.host - target host when exporting to a network printer
|
||||
# ctx.orca_version - OrcaSlicer version string
|
||||
# Writing into the folder of ctx.gcode_path is permitted by the audit hook;
|
||||
# writing elsewhere outside data_dir() is blocked.
|
||||
try:
|
||||
with open(ctx.gcode_path, "a", encoding="utf-8") as f:
|
||||
f.write(f"\n; processed by Example Post-process for {ctx.output_name}\n")
|
||||
except Exception as exc:
|
||||
return orca.ExecutionResult.failure(
|
||||
orca.PluginResult.RecoverableError,
|
||||
f"post-process failed: {exc}")
|
||||
return orca.ExecutionResult.success("G-code annotated")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Capability 3 - a printer-connection (agent) capability
|
||||
# Registers a network printer agent on load. The host serialises each native
|
||||
# agent call into a single JSON request envelope
|
||||
# ({command, request_id, dev_id, payload}); you dispatch on request["command"]
|
||||
# and return a JSON response envelope string. Delete this whole class if your
|
||||
# plugin is not a printer agent.
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ExamplePrinterAgent(orca.printer_agent.PrinterAgentBase):
|
||||
def get_name(self):
|
||||
return "Example Printer Agent"
|
||||
|
||||
def get_agent_info(self):
|
||||
return orca.printer_agent.AgentInfo(
|
||||
id="example-agent",
|
||||
name="Example Printer Agent",
|
||||
version="1.0.0",
|
||||
description="Skeleton printer agent.",
|
||||
)
|
||||
|
||||
def send_command(self, request_json):
|
||||
# Parse the request envelope and dispatch on its "command".
|
||||
try:
|
||||
request = json.loads(request_json or "{}")
|
||||
except json.JSONDecodeError:
|
||||
request = {}
|
||||
command = request.get("command", "")
|
||||
request_id = request.get("request_id", "")
|
||||
|
||||
# TODO: handle the commands your device supports and build a real response.
|
||||
return json.dumps({
|
||||
"request_id": request_id,
|
||||
"status": "error",
|
||||
"message": f"unhandled command: {command}",
|
||||
})
|
||||
|
||||
def send_command_with_progress(self, request_json, update_fn, cancel_fn):
|
||||
# Optional. Override only for long-running commands (uploads, prints).
|
||||
# update_fn(stage, percent, message) - push progress to the host UI.
|
||||
# cancel_fn() -> bool - poll it; abort if it returns True.
|
||||
# Default behaviour is to run as a plain send_command.
|
||||
return self.send_command(request_json)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# The package - exactly one @orca.plugin class per file. register_capabilities()
|
||||
# declares which capabilities this plugin exposes. A capability you do not pass to
|
||||
# register_capability() is invisible to OrcaSlicer, even if its class is defined
|
||||
# above.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@orca.plugin
|
||||
class CapabilitySkeleton(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(ExampleScript)
|
||||
orca.register_capability(ExamplePostProcess)
|
||||
orca.register_capability(ExamplePrinterAgent)
|
||||
Reference in New Issue
Block a user