# Description
This PR introduces persistent, capability-scoped configuration for plugins. Users can configure plugins through the Plugins dialog instead of manually editing the plugin’s Python source.
Configuration exists at two levels:
- **Global** — one configuration per capability, shared by every preset. Edited in the Plugins dialog’s **Config** tab.
- **Per preset** — an optional override stored on a process or printer preset, edited from that preset’s **Plugin Preferences** group. A preset that overrides a capability configures the slices it drives; presets that do not simply use the global configuration.
Plugin authors can use the built-in JSON editor or provide a custom HTML settings interface. Both levels use the same editor and the same stored shape.
## New Plugin Configuration APIs
The following APIs are available to all plugin capability types.
### Python APIs
- `get_config() -> str` — Returns a raw JSON string of the capability’s effective configuration: the active preset’s override if it has one, otherwise the global configuration, otherwise `{}`.
- `save_config(config) -> bool` — Persists JSON-compatible configuration for the capability and returns whether the write succeeded. Always writes the **global** configuration (see [Preset overrides](#preset-overrides)).
- `get_config_version() -> str` — Returns the plugin version that last saved the configuration `get_config()` returned, from that same level, or `""` if it has never been saved.
- `has_config_ui() -> bool` — Override and return `True` to use a custom configuration interface instead of the built-in JSON editor.
- `get_config_ui() -> str` — Returns the HTML used to render the custom configuration interface.
- `get_default_config() -> str` — Returns a raw JSON string of the configuration applied by **Restore defaults** in the Plugins dialog. The default implementation returns `{}`.
### Custom UI JavaScript APIs
Custom configuration interfaces receive a sandboxed `window.orca` bridge:
- `window.orca.getConfig()` — Returns the current capability configuration.
- `window.orca.saveConfig(config)` — Requests that the host persist the supplied configuration.
- `window.orca.onConfig(callback)` — Immediately invokes the callback with the current configuration and invokes it again after successful saves or restores.
`saveConfig()` is asynchronous and does not return a Promise. Custom interfaces should use `onConfig()` to observe the successfully persisted state.
## Plugins Dialog
Every activated capability appears in the Plugins dialog’s **Config** tab, where its global configuration is edited.
The editor shown for a capability is selected as follows:
- If `has_config_ui()` returns `True` and `get_config_ui()` returns valid, non-empty HTML, the dialog renders the custom interface.
- Otherwise, the dialog renders the built-in JSON editor.
- If a custom interface cannot be loaded, the dialog reports the error and falls back to the JSON editor.
- The **Restore defaults** action replaces the stored configuration with the value returned by `get_default_config()`.
Custom interfaces run in a sandboxed iframe and can access configuration only through the provided `window.orca` bridge.
## Preset overrides
Process and printer presets gain a **Plugin Preferences → Capabilities** setting (Advanced mode). Its **Configure** button opens a dialog listing the capabilities that preset actually uses — the ones its `plugins` manifest declares *and* one of its plugin-backed options points at — and edits each one’s configuration for that preset alone. The button shows the number of overrides the preset carries.
That dialog offers two actions:
- **Save** — stores the edited configuration as this preset’s override.
- **Restore defaults** — discards the preset’s override, so the capability falls back to the global configuration. A preset holding no override *is* a preset at its defaults.
### How a running capability reads its configuration
`get_config()` resolves in this order:
1. The active preset’s override for this capability, if it has one.
2. The global configuration in `config.json`.
3. `{}`.
Which preset is consulted follows from the capability’s type. A plugin-backed option declares the capability type it accepts (`ConfigOptionDef::plugin_type`) and belongs to exactly one preset type, so `slicing-pipeline` capabilities are configured by the process preset and `printer-connection` capabilities by the printer preset. Nothing is hardcoded: declaring `plugin_type` on a new option is all it takes to place a new capability type on that map.
`get_config_version()` reports the version stamp from whichever level supplied the configuration, so a plugin migrating a stale config is never handed one level’s data with another level’s version.
`save_config()` from Python always writes the global configuration, never a preset — presets are the user’s to edit, and a plugin saving from a worker thread cannot mark one dirty. A capability whose active preset overrides it will therefore keep reading that override back rather than what it saved.
### Storage
A preset’s overrides live in an ordinary string setting on the preset (`plugin_preference_overrides`), holding a JSON array of entries keyed by plugin and capability. Because it is an ordinary setting, the whole preset lifecycle carries it for free: the dirty marker, the revert arrow, inheritance, project (3MF) round-tripping, and preset sync all behave exactly as they do for every other setting. The dialog is a pure editor over that text — it never writes to the preset itself and never writes to the global config file.
## Configuration Storage
All global plugin configuration is stored in a shared file:
`data_dir()/orca_plugins/config.json`
Configuration entries are isolated by plugin and capability. The host also records the plugin version that last wrote each entry.
The configuration file is intentionally stored outside individual plugin directories. This allows settings to survive:
- Plugin upgrades and reloads
- Local plugin deletion and reinstallation
- Cloud plugin unsubscribe and resubscribe operations
Reinstalling or resubscribing to the same plugin restores access to its previously saved configuration.
## Known limitations
**Filament capabilities cannot be overridden per preset.** There is no single active filament preset — one is selected per extruder — and `get_config()` does not say which extruder the capability is running for, so a filament override could only be applied by guessing. Rather than hand a plugin another extruder's settings, filament capabilities read the global configuration.
Nothing reaches this today: no filament option declares a `plugin_type`, so no capability type maps to the filament preset. Lifting it means pushing the extruder onto the plugin call context the Python trampoline already maintains and resolving the preset from that, with the extruder optional — whole slicing steps (`posSlice`, `psGCodePostProcess`) span every extruder and have no current filament.
# Tests
`tests/slic3rutils` covers the capability config store, the Python config API, the preset override layer, the capability-type → preset-type mapping, and which capabilities a preset counts as in use.
# Screenshots/Recordings/Graphs
Custom UI
<img width="855" height="703" alt="image" src="https://github.com/user-attachments/assets/745ecb7d-9e20-4c39-b857-5aa730a27142" />
Default JSON text editor
<img width="855" height="703" alt="image" src="https://github.com/user-attachments/assets/18b7b89c-6f77-4960-a9b2-964e71f74fc3" />
Process Sidebar
<img width="717" height="360" alt="image" src="https://github.com/user-attachments/assets/8fac3e66-c06a-44e4-ad4b-4cc6c003bb2b" />
Filament dialog
<img width="1090" height="832" alt="image" src="https://github.com/user-attachments/assets/ff6a4cbe-11c0-4d04-9ecb-9a717bdeb3f4" />
Printer settings dialog
<img width="1090" height="832" alt="image" src="https://github.com/user-attachments/assets/c616afb0-4eb2-40c2-92c0-7f5edc50b4e6" />
Dialog opened from preset settings
<img width="860" height="725" alt="image" src="https://github.com/user-attachments/assets/069408a8-e94b-47e0-8e16-a81e0d58b4d4" />
# Example plugin with custom UI used in screenshot
[custom_ui_screenshot_demo.py](https://github.com/user-attachments/files/29995212/custom_ui_screenshot_demo.py)
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The preset option was plugin_preference_overrides while the GUI field type
that renders it was GUIType::plugin_config, for one and the same thing.
Settle on "config": the store, the dialog and the Python hooks all say config
already, so renaming that way touches 4 files instead of the whole plugin
subsystem and the public plugin API.
Keep the _overrides suffix — the option is the preset's override layer over
the base PluginConfig store, a distinction EffectiveCapabilityConfig tracks.
Also wrap the printer tab's group heading in L(); it was the only one of the
three missing it, and was therefore untranslatable.
The merge kept this branch's PluginConfig design, which deletes
PluginDescriptor::settings, get_plugin_settings() and ctx.params, but left
references to them behind: the slic3rutils target did not build, and the
bindings test still asserted the removed ctx.params attribute.
Port the two settings tests onto PluginConfig instead of dropping them. They
guard a field bug where a cloud-metadata refresh wiped a plugin's settings and
it silently ran on its own defaults, so the equivalent properties are still
worth pinning: that a stored config survives the refresh, and that an edited
config reaches the plugin through a real dispatch.
Also defer PluginsConfigDialog's web commands off the webview script-message
callback, as PluginsDialog already does. Its remove_preset_override handler put
a modal wxMessageBox on that stack, which is the GTK crash class fixed in
b779a7bfed/f2ccbfc8b5 for the sibling dialog.
Cloud catalog records never carry [tool.orcaslicer.plugin.settings], so the
metadata merge wiped the locally-parsed settings and plugins silently ran on
their built-in defaults (ctx.params arrived empty).
Update Maschine G-Code according to latest Bambu Studio Version X2D
filament_change gcode: 2026/07/01
X2D layer_change gcode: 2026/07/01
X2D start gcode: 2026/06/05
X2D timelapse gcode: 2026/06/03
# Description
<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
> * What issue does this PR address or fix?
> * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
open_terminal_dialog is reached from the plugins dialog's webview
command, and TerminalDialog hosts a webview of its own — same class as
the plugin-window crash. Defer the window work via CallAfter, guard the
re-front Show() per #13657, and drop the redundant Raise() on creation.
wx 3.3.2 delivers webview script messages synchronously inside the native
callback on GTK and macOS, so script plugins run with the plugins-dialog
webview's signal/delegate frame on the stack. Creating and presenting the
orca.host.ui window from there crashed on Linux at Raise() --
gtk_window_present while GTK's deferred show was still in flight.
Defer the whole window creation to a CallAfter with a pre-bound registry
handle (post/close stay FIFO-safe, teardown races become a no-op), and
drop Raise() plus the show_modeless_dialog wrapper: Show() already
activates and fronts a new window on every platform.
## What this does
Ports the AMS filament drying control feature from BambuStudio. Most
work was done by Claude Code with deepseek-v4-pro. Thanks Bambu & CC &
DeepSeek :P
Allows users to start, monitor, and stop AMS-based filament drying
directly from the OrcaSlicer UI for N3F (AMS 2 Pro) and N3S (AMS HT) AMS
units.
Mostly from
c8f70c6ca7
Part of #12091
## Screenshots
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/24f579cb-c67c-4d6e-bf77-c31e018f2f70"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/74f628aa-6f5f-4150-b2e9-082e4ffc3527"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/d2f26412-a054-4085-9236-5074a030b001"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/7beef770-8fbc-4375-b244-e0de44b8db2f"
/>
## Changes
- **Data model:** Added drying status enums (`DryStatus`,
`DrySubStatus`, `CannotDryReason`, etc.), `DrySettings` struct,
`DevFilamentDryingPreset` struct to `DevAms`/`DevFilaSystem`
- **Promoted `DevAmsType`** to a global enum (`EXT_SPOOL=0, AMS=1,
AMS_LITE=2, N3F=3, N3S=4`), renamed `DUMMY` → `EXT_SPOOL`
- **JSON parsing:** Extended `DevFilaSystemParser` to parse drying
status fields from printer status messages
- **Commands:** Added `CtrlAmsStartDryingHour()` and
`CtrlAmsStopDrying()` sending `"ams_filament_drying"` JSON via MQTT
- **Backend utility:** New `DevUtilBackend` class with
`GetFilamentDryingPreset()` for reading filament drying config keys
- **UI dialog:** New `AMSDryControl` dialog with three pages
(status/control, guide, progress) matching BambuStudio behavior
- **Integration:** Wired AMS humidity indicator click to open the drying
dialog for N3F/N3S AMS types
- **Assets:** 12 new drying-related images from BambuStudio
- **Firmware parsing:** Added `is_support_remote_dry` flag parsed from
`fun2` bit 5
## Constraints
- N3F/N3S only — standard AMS and AMS Lite continue to use the existing
humidity popup
- Backward compatible — existing `command_ams_drying_stop()` preserved
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The eight presets added in 0545750c1b never got ids: the six iQ processes
had none, and K1 SE 0.8 / V-Core 4 0.8 carried ids copied from the presets
they were duplicated from — K1 SE 0.8 still shared K1C 0.8's id. Regenerated
with scripts/assign_vendor_setting_ids.py; none of the eight have shipped in
a release, so no existing id changes meaning.
* fix: impl refactor
* fix: unload/load python module, race conditions, freezes
* remove dead code
* remove extra hook
* remove more dead code
* fix gil run script
reslice() now enforces only the missing-plugin block via
refresh_missing_plugin_block (no second Print::validate); plate
ready-status returns to upstream's plain model_fits, matching
GLCanvas3D::reload_scene. Also restores main's use_bbl_device_tab and
the check_track_enable comment, and drops two unused MainFrame includes.
Merge-resolution cleanup. The #12506 re-select path kept main's preset_bundle
null check, and both select_machine calls now use effective_agent_id rather than
mixing it with the equal-but-differently-named agent_info.id.
Resolve five conflicts, all of which needed both sides rather than a pick:
- BackgroundSlicingProcess: ours was a pure tabs->spaces reformat of base, so
keep main's per-filament volume/nozzle map read-back (its only change here).
- GUI_App: main's #12506 else-if attached to an `if` this branch deleted;
re-expressed onto the same-agent early-return path (the agent factory caches
per id, so pointer equality is the same predicate).
- MainFrame: both sides relocated Sync Presets independently; keep main's
push_notification plus the branch's Plugins menu items.
- Tab: the "TODO: Orca: Support hybrid" blocks were unchanged base, not a branch
decision; take main's enabled Hybrid to match the already auto-merged siblings.
- test_config: union of both sides' cases (6 plugin + 9 multi-nozzle).
The filament-group golden harness landed with H2C/A2L support (#14685). Its
"FilamentGroup golden regression" / stress_66 case fails intermittently on
Windows x64, on main and on unrelated PRs alike. The test depends on how fast the
runner is.
The k-medoids clustering these goldens exercise is an anytime search bounded by a
3 second wall clock. Every restart is seeded from its own index, so nothing about
it is random. What varies is how many restarts fit in the budget, and the best
cost is a minimum over completed restarts, so a slower runner is never better.
Grading a score produced that way measures the machine as much as the code.
Add a ClusteringBudget struct and let the tests set it. The defaults are the
current 3 seconds and 30 restarts, so slicing behavior is unchanged. A
non-positive timeout removes the wall clock and bounds the search by restart
count alone.
The goldens are then graded under a fixed budget of four restarts, where every
one of them reaches the BambuStudio reference within 3%, so the score becomes a
property of the code. This retires the machine-specific 125103 lock on stress_66.
The default wall-clock path keeps its own test, asserting the grouping is valid
and the search does not run away. It makes no score assertion, because under a
wall clock that number is not a property of the code.
The golden test also checks the run fits in ten times the default wall clock.
Slicing quality depends on how many restarts fit in the budget, so a search an
order of magnitude slower would degrade real groupings while a fixed-budget score
gate stayed green.
The 3% tolerance stays as the parity allowance against the goldens. It also
covers a small spread across standard libraries: the k-medoids search seeds each
restart with std::shuffle, whose algorithm the C++ standard leaves unspecified,
so libstdc++, libc++ and the MSVC STL permute the same seed differently, start
from different medoids, and settle on slightly different groupings, about 3e-4
apart and only on the goldens heavy enough to reach the k-medoids search.
# Description
Adds a --slice (-s) mode to the profile validator that slices a
two-colour cube through every shipped printer, expanding all custom
g-code (change_filament_gcode, machine start/end, etc.). This catches
invalid-placeholder / bad-flow / slicing errors that the static JSON
checks and unit tests can't see.
Included:
- Validator: new -s sweep mode; per-profile error attribution in the
log; resolves the synthetic 2nd-filament nozzle-mapping so multi-nozzle
BBL printers (incl. the Direct-Drive+Bowden X2D) validate cleanly.
- CI, two complementary paths:
- check_profiles.yml — runs the sweep on profile-only PRs (nightly
binary).
- build_all.yml — new parallel slice_check_linux job runs it on
engine/src PRs with the PR-built binary (build_all doesn't trigger on
resources/**, so no overlap). Runs off the build's artifact, so it
doesn't lengthen the build leg.
- Profile fixes surfaced by the sweep: Creality, FLSun, Ginger, Qidi,
RatRig, iQ.
- Engine: whitelist BBL firmware T-opcodes (T1001/T65279/T65535) in the
time estimator (log-only, no g-code change); dedupe a
per-filament/per-layer log flood in get_config_index.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The Unit Tests job sparse-checks-out only .github/scripts/tests, so the
baked-in absolute PROFILES_DIR was missing at runtime; the shipped-profile
test then read a non-existent JSON and null-dereferenced in opt_string.
Check out resources/ in the unit-test job, and guard the test helper to
skip when the profile is absent and require the key before dereferencing.