Adapt the speed dial's ActionRegistry to the collapsed
get_plugin_capability(PluginCapabilityId) overload, and restore the
script success/skipped status message the dialog lost when its
PluginScriptRunner refactor was superseded by ActionRegistry.
# 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.
Replace hand-rolled nozzle type comparison + Hybrid hack with
BBS-style NozzleGroupInfo comparison in check_ams_status_impl.
Previous approach: direct nozzle_volume_type == printer_flow_type
with a Hybrid tolerance lambda. This either suppressed the dialog
entirely (Hybrid always matched) or showed it on every Preview switch.
New approach (matching BBS):
- Build preset NozzleGroupInfo from extruder_nozzle_stats config
- For Hybrid presets: expand into per-type counts (Std#N, HF#M)
- nozzle_count==0 (never synced): dialog appears for first sync
- nozzle_count>0 (after sync): compare with printer GetNozzleGroups()
- Counts match → dialog suppressed on Prepare↔Preview switches
Safe for all multi-extruder printers (H2C, H2D): non-Hybrid presets
use single NozzleGroupInfo per extruder. Single-extruder printers
exit at is_multi_extruders() guard before reaching this code.
Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp
is_extruder_stat_synced() line 16642
There is no arm64 self-hosted build server, so when \`vars.SELF_HOSTED\`
is set the arm64 Linux and Windows legs previously fell back to
GitHub-hosted runners. Drop those legs entirely instead, along with the
unit test jobs that consume their artifacts.
**Changes:**
- **Linux / Windows builds:** matrices switch from a static \`include:\`
list to \`fromJSON(vars.SELF_HOSTED && ... || ...)\`, so self-hosted
runs build x86_64/x64 only. The Windows job's per-arch runner
conditional is gone — the runner is now baked into each matrix entry.
- **Unit tests:** \`unit_tests_linux_aarch64\` and
\`unit_tests_windows_arm64\` are gated on \`!vars.SELF_HOSTED\`; their
\`needs\` still succeed, so \`success()\` alone would not skip them.
- **Slice check:** the profile validator artifact is now named per-arch
and uploaded from the aarch64 leg normally, or x86_64 on self-hosted.
The job's runner and download name follow the same switch, keeping the
gate alive rather than failing on a missing artifact.
- **Comments:** trimmed across the touched blocks.
No change when \`SELF_HOSTED\` is unset — an unset variable is falsy, so
GitHub-hosted runs keep both arches, the same runners, and the aarch64
slice check.
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.
Enable use_forcast in reorder_filaments_for_minimum_flush_volume_base
to match the multi-extruder path behavior (line 1227).
The forecast solver (solve_extruder_order_with_forcast) considers the
next layer's filament set when choosing ordering for the current layer,
minimizing inter-layer transition flush cost.
Previously disabled (hardcoded false) in the single-nozzle/base path,
causing suboptimal inter-layer transitions. The multi-extruder path
already had this enabled.
Measured on 5cubes (5 filaments, 35 layers, H2C):
- Print time: -12 min (-10%)
- Waste filament: -5g (-28%)
- WT extrusion: -44%
Limited to ≤5 filaments per nozzle per layer (O(N!×M!) complexity).
Clamp ramming speed during extruder changes so the departing nozzle
has enough time to reach precool_target_temp before carousel rotation.
Only applies to extruder changes (not carousel nozzle changes).
Reference to BBS: BambuStudio/src/libslic3r/GCode/WipeTower.cpp
ramming() L3449-3462
Physical dist/speed ignores acceleration/deceleration, giving
underestimated total time (29 min vs real 48 min). M73 from the
trapezoid planner accounts for accel/decel and is closer to reality.
Now we keep physical DISTRIBUTION (proportions per-filament) but
scale the X-axis so total time matches M73 trapezoid estimate.
Replace M73-based timeline interpolation with physical time calculation
from G1 feedrates and M400 delays. M73 has 1-minute resolution and
non-uniform granularity which distorts the time axis (e.g. P83→P100
jump makes last filament appear much longer than it actually is).
Physical timeline computes cumulative time per gcode line from actual
move distances and feedrates, giving accurate filament duration on plot.
Falls back to M73 interpolation when raw gcode lines are not available.
End gcode contains firmware-conditional M400 waits for air purification,
timelapse capture, and sound notification that are post-print operations.
These were incorrectly included in M73 total time, inflating the estimate.
The fix detects MACHINE_END_GCODE_START tag during the streaming parse
(process_tags) and sets m_skip_end_gcode_delays=true. process_M400 then
skips timed delays (S/P params) in the end gcode scope.
BBS achieves the same effect by dropping leftover in calculate_time
(is_final=true). We skip at the source instead, which is more surgical
and leaves calculate_time behavior unchanged for all printers.
Affects all BBL printers with MACHINE_END_GCODE_START tag.
Non-BBL printers are unaffected (no tag = no skip).
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).
The M620→M621 firmware toolchange block weight (500x) was only applied
to sparse track sample lines. Hundreds of G1 moves between samples
inside the M620 block got weight=1, causing firmware toolchange to
appear compressed on the timeline plot.
Build continuous M620→M621 line ranges from track samples and apply
weight=500 to ALL lines within those ranges. This makes toolchange
and wipe tower zones proportionally accurate on the timeline.
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)