diff --git a/docs/HLSD/preset-plugin-api.md b/docs/HLSD/preset-plugin-api.md new file mode 100644 index 0000000000..aafbfea4ba --- /dev/null +++ b/docs/HLSD/preset-plugin-api.md @@ -0,0 +1,242 @@ +# Preset Plugin API — High Level Design + +## Purpose and scope + +The preset plugin API lets Python plugins update the presets OrcaSlicer is using, +so users do not have to perform every routine setting change by hand. It covers +process, printer, and filament presets. All three use one staged update lifecycle; +the way a target is selected is specific to the preset type. + +The API edits Orca's managed edited preset, not a detached copy or a saved preset +behind the UI's back. It does not save to disk. Saving, discarding, and transferring +unsaved edits remain under Orca's existing preset workflow. + +The API does not create or delete presets or provide off-tab editing. For filament, +choosing a different `preset_name` as the update target also selects that preset for +the requested slot, following the normal UI flow. A standalone slot-assignment or +preset-fork operation is outside this API's contract. + +## Preset and slot model + +Each preset collection maintains a managed edited copy for its selected preset. +Settings tabs apply changes to that copy and own the normal dirty-state, validation, +save/discard/transfer, and downstream refresh behavior. Plugin updates must use the +same path so that the settings tab, dirty indicators, and effective print +configuration observe a consistent state. + +Filament selection adds a slot-to-preset mapping. `PresetBundle::filaments` is the +catalog, while `PresetBundle::filament_presets` maps each slot to a preset name. +Several slots may refer to the same preset. A filament update edits that shared +preset, so all slots referring to it are affected. Slot numbers exposed by the API +are 1-based, matching the UI. + +## API shape + +Each preset API exposes `begin_update()`, which selects and binds one target, and +returns a transaction. The transaction stages setting changes using `set()` and +applies them only on explicit `commit()`. + +```python +# Filament is targeted by its 1-based slot. Omitting preset_name keeps the +# preset currently assigned to the slot; providing it selects that preset for +# the slot before staging updates. +tx = await orca.host.preset.filament.begin_update( + slot_number=2, + preset_name="Generic PLA" +) +tx.set("filament_flow_ratio", "0.98") +tx.set("filament_retract_length", "0.8") +result = tx.commit() + +# Process and printer default to their active presets. A caller can request a +# different preset that is selectable in the corresponding UI dropdown. +tx = await orca.host.preset.process.begin_update( + preset_name="0.20mm Standard" +) +tx.set("layer_height", "0.2") +tx.commit() +``` + +The example expresses the intended API contract. The current Python bridge does not +support this awaitable transaction shape; supporting it requires new coroutine or +continuation handling. Any wait for a UI decision must suspend the plugin operation +without blocking Orca's UI thread. + +### Target resolution + +- **Filament:** `slot_number` identifies the slot. If `preset_name` is omitted, + the transaction targets the preset currently assigned to that slot. If supplied, + it requests that preset for the slot and selects it through the normal UI flow; + the name must be among the presets offered by that slot's dropdown. Selecting a + different preset changes the slot-to-preset mapping. In both cases the transaction + edits the shared preset, not an isolated slot override. +- **Process and printer:** omitting `preset_name` targets the currently active + preset. Supplying `preset_name` requests the preset by the name shown in its + dropdown. It must be one of the choices the dropdown currently offers, rather + than merely any preset present in the collection. This is particularly important + for process presets, whose selectable choices can depend on the current context. +- A transaction targets exactly one preset. Multi-preset transactions are not + supported: applying them would require switching among targets while the + application has only one managed edited copy per collection. + +Targets are resolved through Orca's normal preset-selection model. The API should +not accept raw preset pointers or filesystem paths as target identities. A missing, +unavailable, or ambiguous target fails explicitly instead of silently selecting a +different preset. + +## Selection and user interaction + +`begin_update()` resolves its target, then delegates any required selection to the +shared underlying `switch_preset` operation. It calls `switch_preset` only when the +requested target differs from the active target; otherwise it returns a transaction +without switching. `switch_preset` is responsible for using Orca's normal preset +selection flow, including updating a filament slot assignment when requested. If +the normal selection flow requires user input, it opens the relevant existing +dialog(s), including unsaved-edit choices such as save, discard, or transfer. The +operation is awaitable and remains pending until those interactions finish; it +resolves with the switch result or cancellation, without applying staged settings. +A canceled switch does not change the requested target or filament slot assignment, +but it does not roll back decisions the user already accepted in an earlier dialog. +`switch_preset` is a proposed implementation-level coordinator, not an existing +function or a separate plugin API. It must use the normal type-specific selection +paths and confirm that the requested preset actually became active; a successful +selection request alone is not sufficient. + +If the requested target is already active, `begin_update()` returns without a +selection prompt, apart from any UI-thread dispatch needed by the bridge. If a +different preset is requested, Orca selects it through the normal UI flow. For +filament, this also updates the requested slot's assignment when `preset_name` +differs from its current assignment. When the current preset has unsaved changes, +the plugin operation remains pending until the user chooses among Orca's existing +options, such as transfer, discard, or cancel. + +The API never suppresses that warning, chooses an option for the user, or switches +away from dirty edits silently. Cancel aborts the operation before any staged target +changes are applied. Process and printer updates follow this rule too when the +plugin requests a different `preset_name`; they normally return immediately when +the active preset is the target. + +## Staging and commit semantics + +`set(key, value)` stages a serialized config value and does not mutate the preset. +The transaction is the boundary for a related group of changes, avoiding +intermediate partially-updated states. At commit, the API validates keys and values +against the target preset type and applies the batch through the existing tab and +settings-group update path. Existing setting-specific validation, correction, and +callbacks remain authoritative; plugin updates must not create a parallel setter +with different behavior. + +A successful commit updates dirty state and downstream configuration/Plater state +once for the batch where possible. The edited preset remains unsaved; persistence +continues through Orca's normal save action. A validation or commit failure must +not leave only some staged target settings applied. This all-or-nothing batch +guarantee requires a batch-aware commit path; the current setter applies one option +at a time and cannot provide it by simply looping over existing calls. A filament slot assignment +made as part of target selection is not a staged setting: after the user accepts +the selection flow, it may remain changed even if a later commit fails. The API +does not automatically switch back or reverse the user's selection or any accepted +transfer/discard decision for the previous preset. + +## Outcomes and consistency + +The API distinguishes target resolution failures, invalid setting/value, user +cancellation, stale filament-slot mapping, and commit failure. A result identifies +the target preset, whether preset selection changed, and (for filament) whether the +slot assignment changed and which slots share the edited preset. A canceled +selection applies neither the requested slot assignment nor staged target changes; +a failed commit applies none of the staged target settings. + +Filament slot assignments can change while a plugin operation is pending. The API +must detect that and reject a stale operation instead of silently editing the new +slot assignment. It must not hold raw preset pointers across reloads. Conflicting +duplicate writes to one setting in a transaction are rejected rather than resolved +by incidental call order. + +The existing one-setting filament call, +`orca.host.preset.filament.update(slot_number, key, value)`, can remain as a +compatibility shorthand. It shares the staged API's target resolution, selection, +validation, and commit path. + +## Design principles + +- **Reduce routine interaction, preserve agency.** Clean switches should be + seamless; unsaved user work must still receive the normal warning and decision. +- **One source of truth.** UI and plugins share the managed edited copy and setting + update path, including validation and callbacks. +- **The UI workflow is authoritative.** The API must enter through the same + selection and setting-change workflows as the UI, not reproduce their logic or + write config values directly. This keeps existing and future side effects active: + `on_change` handling for mutually exclusive, invalid, or out-of-range settings; + compatibility and dependent-preset updates; and printer-agent changes when the + selected printer changes. +- **Explicit target and explicit commit.** Plugins know which preset they edit and + when staged changes become visible. +- **No hidden persistence.** Plugin edits remain unsaved until the user saves them + through Orca. +- **Shared means shared.** Editing a filament preset through one slot affects every + slot assigned to it. Choosing another preset for a slot is explicit in + `preset_name`; slot-specific divergence would require a separate fork operation. +- **Consistent lifecycle, type-specific targeting.** Process, printer, and + filament share transaction and error semantics while respecting their existing + selection models. + +## Feasibility against the current codebase + +The design builds on existing preset and tab behavior, but the transaction API and +some target-selection behavior are new. The current plugin surface exposes +per-setting `update()` calls. For filament, `update_filament_slot()` resolves the +preset already assigned to a slot, selects it through `Tab::select_preset()`, and +applies one setting through `Tab::apply_preset_config_value()`. It does not change +the slot assignment. There is no current `begin_update()`, transaction type, or +`switch_preset()` helper. + +`Tab::select_preset()` is the existing basis for switching to process, printer, or +filament presets and for invoking the normal unsaved-edit flow. The flow is +type-specific: a process selection can trigger compatibility handling for +filaments, and a printer selection can involve technology changes and dependent +preset compatibility. It may therefore show more than one dialog. Cancellation of +a later step does not necessarily undo decisions the user already accepted in an +earlier dialog. The plugin operation must preserve those side effects and verify +the resulting edited target. + +The filament `preset_name` behavior is an additional integration, not a capability +of `Tab::select_preset()`: selecting a different name for a slot must also update +the slot-to-preset mapping through the Plater's normal selection behavior. That +path updates more than the vector entry, including dependent UI/configuration state. +The API should reuse or factor that path rather than call +`PresetBundle::set_filament_preset()` alone. It must also define which UI-visible +slots are valid, including whether mixed-color virtual slots are targetable. + +Process and printer dropdown choices are constrained by visibility and +compatibility, and compatibility may depend on the active context. `Tab::select_preset()` +performs its own compatibility and technology checks; merely finding a name in the +collection does not prove it is an allowed dropdown choice. Target validation must +follow the same rules as the UI. + +The current `PluginHostUi::run_on_ui_thread()` path marshals work synchronously and +waits for completion; modal selection dialogs block that operation until answered. +It is not an asyncio/coroutine bridge. Thus the user-facing wait exists today, but +the proposed Python `await` behavior does not. An awaitable `switch_preset()` needs +new plugin scheduling/continuation support and must avoid blocking the UI thread or +deadlocking callers running on threads that the UI itself waits for. + +Finally, `Tab::apply_preset_config_value()` validates and commits one setting at a +time. It backs up one option and invokes callbacks that can adjust related settings. +A transaction that promises all-or-nothing updates and one downstream refresh needs +a batch-aware prepare/commit mechanism that accounts for those callback effects; +repeating the current single-setting method would allow partial updates and repeated +refreshes. Since `begin_update()` performs target selection before the caller stages +values, invalid staged values can still follow a user-visible selection prompt. That +is a consequence of this API shape and should remain explicit in its behavior. + +## Integration boundary + +The plugin API coordinates target resolution, preset selection, staged values, and +the settings commit path. It does not own or duplicate preset dirty-state logic, +selection prompts, setting validation, or persistence. Target changes must flow +through the same UI selection path so dependent behavior—such as selecting the +printer agent—still runs. Setting changes must flow through the same UI change path +so `on_change` and related validation/correction behavior still runs. The +integration must bridge potential UI prompts to the Python operation +asynchronously, reuse the normal Plater slot-selection behavior for explicit +filament targets, then commit a batch without losing existing per-setting behavior.