fix: update demo plugins

This commit is contained in:
peachismomo committed 2026-10-17 15:09:21 +08:00
1 parent 0b478a1d0b
commit b7f9888516
2 files changed
+219 -1189

No files matched your search

+62 -263
View File
@@ -4,294 +4,93 @@
# [tool.orcaslicer.plugin]
# id = "preset-transaction-demo"
# name = "Preset Transaction Demo"
# description = "Manual test for the staged begin_update()/set()/commit() preset API."
# description = "Stages several settings on one preset with begin_update()/set()/commit() (demo)."
# author = "OrcaSlicer contributors"
# version = "0.1.0"
# version = "0.2.0"
# type = "script"
# ///
"""Preset Transaction Demo — a manual test harness for the staged preset API.
"""Preset transaction demo: change several settings with one commit.
Install this file in the local ``orca_plugins`` directory, enable the plugin, then run
"Preset Transaction Demo" from the Plugins dialog. The window drives
``orca.host.preset.<category>.begin_update(...)`` and shows the returned
``PresetUpdateResult``.
Enable the plugin, then run "Preset Transaction Demo" from the Plugins dialog.
Edit CATEGORY, PRESET_NAME, FILAMENT_SLOT and UPDATES below to try other presets
and settings.
This is an asynchronous Script capability: ``execute`` is ``async def`` and runs on the
background Script worker's own asyncio event loop. ``begin_update()`` is awaitable — it
suspends the coroutine until the normal preset-selection / unsaved-edit dialog is answered,
without blocking Orca's UI thread. ``set()`` and ``commit()`` remain synchronous and are
only valid on that worker.
``begin_update()`` resolves the target through Orca's normal preset-selection
flow, so it may open Orca's usual "unsaved changes" dialog. The call is
synchronous: the background Script worker waits until the user answers, while
the UI thread stays responsive. ``set()`` only stages values; ``commit()``
validates and applies the whole batch. The edited preset stays unsaved (dirty)
until the user saves it through the normal preset workflow; nothing here writes
to disk.
The window's own messages arrive on the UI thread, so ``on_message`` cannot await directly.
Instead the handler enqueues each message onto the worker loop (``call_soon_threadsafe``) and
``execute`` consumes them, one coroutine at a time, so every command can ``await
begin_update``. Closing the window enqueues a sentinel that ends ``execute``.
Categories
* process / printer: target is the active preset, or the name typed into the target
field, resolved through the normal preset-selection flow;
* filament: target is the 1-based slot number, with an optional preset name that
selects (and assigns) that preset for the slot.
Buttons exercise the documented outcomes:
* "Commit" stages every key/value row and commits the batch;
* "Duplicate key" stages the same key twice and shows the rejected conflict;
* "Invalid key" stages a key that belongs to no preset and shows the commit error.
Nothing here saves to disk: an applied change leaves the edited preset dirty until the
user saves it through Orca's normal preset workflow. Values use Orca's serialized config
syntax (numbers plain, percentages with ``%``, booleans ``1``/``0``, lists ``;``-separated).
Values are Orca's serialized config strings: numbers plain, percentages with
"%", booleans "1"/"0", choices by their internal key, lists ";"-separated.
"""
import asyncio
import orca
CATEGORIES = ("process", "printer", "filament")
# ---------------------------------------------------------------------------
# Edit these. CATEGORY and UPDATES belong together: each presets category only
# accepts its own setting keys.
# ---------------------------------------------------------------------------
PAGE = """<!doctype html>
<html><head><meta charset="utf-8">
<style>
body { font: 13px/1.4 system-ui, sans-serif; margin: 16px; color: #222; }
fieldset { border: 1px solid #ccc; border-radius: 6px; margin: 0 0 12px; padding: 10px 12px; }
legend { font-weight: 600; padding: 0 4px; }
label { display: inline-block; min-width: 90px; }
input, select { font: inherit; padding: 3px 5px; margin: 2px 0; }
input[type=text] { width: 260px; }
.row { margin: 3px 0; }
button { font: inherit; padding: 5px 10px; margin: 2px 6px 2px 0; cursor: pointer; }
#out { white-space: pre-wrap; background: #f6f6f6; border: 1px solid #ddd; border-radius: 6px;
padding: 10px; min-height: 70px; }
.err { color: #b00020; }
.ok { color: #0a7a2f; }
</style></head>
<body>
<fieldset><legend>Target</legend>
<div class="row">
<label>Category</label>
<select id="category">
<option value="process">process</option>
<option value="printer">printer</option>
<option value="filament">filament</option>
</select>
</div>
<div class="row" id="slotRow" style="display:none">
<label>Slot (1-based)</label><input id="slot" type="text" value="1" style="width:80px">
</div>
<div class="row">
<label>Preset name</label>
<input id="preset" type="text" placeholder="(blank = current / slot assignment)">
</div>
<div class="row" style="color:#666">Edited now: <span id="current">?</span></div>
</fieldset>
# Preset category: "process", "printer", or "filament".
CATEGORY = "process"
<fieldset><legend>Staged settings (serialized values)</legend>
<div class="row"><input id="k1" type="text" placeholder="key, e.g. layer_height"> =
<input id="v1" type="text" placeholder="value, e.g. 0.25"></div>
<div class="row"><input id="k2" type="text" placeholder="key (optional)"> =
<input id="v2" type="text" placeholder="value"></div>
</fieldset>
# Target preset name for process/printer. "" keeps the currently active preset.
# For filament this optionally selects and assigns the preset for FILAMENT_SLOT;
# "" keeps the preset the slot already has.
PRESET_NAME = ""
<div>
<button onclick="send('commit')">Commit</button>
<button onclick="send('duplicate')">Duplicate key</button>
<button onclick="send('invalid')">Invalid key</button>
<button onclick="send('refresh')">Refresh</button>
</div>
<p id="out">Ready.</p>
# 1-based filament slot. Only used when CATEGORY is "filament".
FILAMENT_SLOT = 1
<script>
const $ = (id) => document.getElementById(id);
function pairs() {
const out = [];
for (const n of [1, 2]) {
const k = $('k' + n).value.trim();
if (k) out.push({ key: k, value: $('v' + n).value });
}
return out;
}
function send(command) {
const category = $('category').value;
$('slotRow').style.display = category === 'filament' ? '' : 'none';
const message = {
command,
category,
preset_name: $('preset').value.trim(),
slot: parseInt($('slot').value, 10) || 1,
pairs: pairs(),
};
orca.postMessage(message);
}
function show(payload) {
if (payload.error) { $('out').className = 'err'; $('out').textContent = payload.error; return; }
if (payload.message) { $('out').className = 'ok'; $('out').textContent = payload.message; }
if (payload.current !== undefined) $('current').textContent = payload.current;
}
orca.onMessage(show);
send('refresh');
</script>
</body></html>"""
# Settings staged in order, then committed together.
UPDATES = (
("layer_height", "0.25"),
("wall_loops", "3"),
)
async def _begin(category, preset_name, slot):
"""Create a transaction for one category, following the two begin_update arities.
begin_update() is awaitable: it suspends until any selection / unsaved-edit dialog is
answered, without blocking Orca's UI thread.
"""
editor = getattr(orca.host.preset, category)
# The binding takes a plain string, so an empty field means "" (keep the current / slot target).
if category == "filament":
return await editor.begin_update(slot, preset_name)
return await editor.begin_update(preset_name)
def _result(payload, category):
return {
"category": category,
"target_name": payload.target_name,
"selection_changed": payload.selection_changed,
"slot_number": payload.slot_number,
"slot_assignment_changed": payload.slot_assignment_changed,
"shared_slots": list(payload.shared_slots),
"keys": list(payload.keys),
"dirty": payload.dirty,
}
def _current(bundle, category, slot):
if category == "filament":
presets = list(bundle.current_filament_presets())
if 1 <= slot <= len(presets) and presets[slot - 1] is not None:
return "%s (slot %d)" % (presets[slot - 1].name, slot)
return "<slot %d has no preset>" % slot
if category == "printer":
return bundle.current_printer_preset().name
return bundle.current_process_preset().name
def _begin_update():
"""Bind CATEGORY's target and return a transaction to stage changes on."""
if CATEGORY not in ("process", "printer", "filament"):
raise ValueError('CATEGORY must be "process", "printer", or "filament"')
if CATEGORY == "filament":
# Filament updates are addressed by their 1-based slot number.
return orca.host.preset.filament.begin_update(FILAMENT_SLOT, PRESET_NAME)
return getattr(orca.host.preset, CATEGORY).begin_update(PRESET_NAME)
class PresetTransactionDemo(orca.script.ScriptPluginCapabilityBase):
window = None
category = "process"
slot = 1
loop = None
def get_name(self):
return "Preset transaction demo"
async def execute(self):
"""Open the window and drive its messages, awaiting begin_update per command.
Runs on the background Script worker's asyncio loop. The window's on_message fires on
the UI thread, so it only enqueues; execute consumes the queue here, where awaits are
legal, and keeps the loop alive until the window closes.
"""
def execute(self):
# A plain def runs on the background Script worker, so begin_update() may
# block here through Orca's selection / unsaved-edit prompts.
try:
self.loop = asyncio.get_running_loop()
self._messages = asyncio.Queue()
self.category = "process"
self.slot = 1
self.window = orca.host.ui.create_window(
title="Preset transaction demo", html=PAGE, width=760, height=520,
on_message=self._enqueue, on_close=self._on_close,
transaction = _begin_update()
for key, value in UPDATES:
transaction.set(key, value)
result = transaction.commit()
except Exception as error:
return orca.ExecutionResult.failure(
orca.PluginResult.RecoverableError,
"Preset transaction demo failed: %s" % error,
)
try:
while True:
message = await self._messages.get()
if message is None: # sentinel: the window closed
break
await self._handle(message)
finally:
self.window = None
return orca.ExecutionResult.success("Preset transaction demo closed.")
except Exception as error:
text = "Preset transaction demo failed: %s" % error
orca.host.ui.message(text, title="Preset transaction demo", buttons="ok", icon="error")
return orca.ExecutionResult.failure(orca.PluginResult.RecoverableError, text)
# --- UI thread -> worker loop hand-off ------------------------------------------------
def _enqueue(self, message):
"""on_message: runs on the UI thread. Hand the message to the worker loop."""
try:
self.loop.call_soon_threadsafe(self._messages.put_nowait, message)
except Exception:
# The loop is already gone (plugin unloading); drop the message.
pass
def _on_close(self):
"""on_close: runs on the UI thread. Wake execute so it returns and the loop closes."""
try:
self.loop.call_soon_threadsafe(self._messages.put_nowait, None)
except Exception:
pass
# --- worker loop ----------------------------------------------------------------------
async def _handle(self, message):
message = message or {}
category = message.get("category", "process")
if category not in CATEGORIES:
self._post(error="Unknown category %r" % (category,))
return
self.category = category
self.slot = message.get("slot", 1) if isinstance(message.get("slot"), int) else 1
preset_name = message.get("preset_name") or ""
try:
command = message.get("command")
if command == "refresh":
self._post()
return
if command == "duplicate":
transaction = await _begin(category, preset_name, self.slot)
transaction.set("layer_height", "0.2")
transaction.set("layer_height", "0.25")
self._post(error="Duplicate key was not rejected (unexpected)")
return
if command == "invalid":
transaction = await _begin(category, preset_name, self.slot)
transaction.set("this_is_not_a_preset_setting", "1")
transaction.commit()
self._post(error="Invalid key was not rejected (unexpected)")
return
if command == "commit":
pairs = message.get("pairs") or []
if not pairs:
self._post(error="Stage at least one key/value row.")
return
transaction = await _begin(category, preset_name, self.slot)
for entry in pairs:
key = entry.get("key")
if not isinstance(key, str) or not key:
raise RuntimeError("A setting key is required in every row")
value = entry.get("value")
transaction.set(key, value if isinstance(value, str) else str(value))
result = transaction.commit()
self._post(
payload=_result(result, category),
message="Committed %d setting(s) to %s \"%s\"."
% (len(result.keys), category, result.target_name),
)
return
self._post(error="Unknown command %r" % (command,))
except Exception as error:
self._post(error="%s: %s" % (type(error).__name__, error))
def _post(self, payload=None, message="", error=""):
payload = dict(payload or {})
if message:
payload["message"] = message
if error:
payload["error"] = error
try:
payload["current"] = _current(orca.host.preset_bundle(), self.category, self.slot)
except Exception as state_error:
payload["current"] = "unavailable: %s" % state_error
if self.window is not None and self.window.is_open():
self.window.post(payload)
return orca.ExecutionResult.success(
'Committed %d setting(s) to the %s preset "%s"; %s.'
% (
len(result.keys),
result.category,
result.target_name,
"it stays unsaved until you save it"
if result.dirty
else "it already matches its saved state",
)
)
@orca.plugin
File diff suppressed because it is too large. Load diff