# Description
Use Space to trigger a new **speed dial**, which allows users to run
**app actions**. The only app actions implemented currently are python
plugin scripts.
## Notes
- Only toggleable in the Prepare (3D) view. Press Space, type to filter,
Enter or double-click to run.
- Focuses on search bar automatically
- Frecency-sorted (run count + recency) with alphabetical fallback (C++
computes)
- Pin actions as favourites and they will show on the top favourites
bar. Done through star icon on each row.
- Script plugins have no icon art yet, so tiles show a collision-aware
monogram: the capability's initial, escalating only when names collide -
prepend the package initial, then add an ordinal - so same-named actions
from different plugins stay distinguishable.
- E.g., capability Bravo from plugin Alpha normally shows just B. If
another action's name also starts with B, they disambiguate by
prepending the package initial (Alpha -> AB). If two still collide on
both initials (both AB), they become AB1 and AB2.
- "Run X?" confirm with a per-plugin "don't ask again" scope, owned
C++-side; suppression persists.
- Persistence (`speed_dial` AppConfig section): `favourite_actions`
(ordered id list) + per-action `stats` + `ask_suppressed`.
- Example of shape in data_dir:
```json
{
"speed_dial": {
"ask_suppressed": "[\"9b12aa079924bbc4\"]",
"favourite_actions": "[\"ccfdf8b9e492b624\",\"9b12aa079924bbc4\",\"b7abfa67626248e4\"]",
"stats": "{\"31d9d129a616a8b7\":{\"count\":4,\"last\":1783924989},\"53ec17d430634f62\":{\"count\":3,\"last\":1783939329},\"9b12aa079924bbc4\":{\"count\":5,\"last\":1783924981},\"9f2cb0d3ca56a87c\":{\"count\":1,\"last\":1783668370},\"b7abfa67626248e4\":{\"count\":4,\"last\":1783939325},\"f93469da14248128\":{\"count\":3,\"last\":1783939337}}"
},
}
```
# Screenshots/Recordings/Graphs
<img width="688" height="335" alt="image"
src="https://github.com/user-attachments/assets/683efa3b-9401-4977-a347-d70193188165"
/>
<img width="681" height="172" alt="image"
src="https://github.com/user-attachments/assets/fafb4965-054b-4fd5-ad6a-03145264fbe0"
/>
<img width="683" height="335" alt="image"
src="https://github.com/user-attachments/assets/00d5bd49-95c0-4f2f-966b-6c04c14c3cf3"
/>
## Tests
- **Web layer (green):** node-vm logic test `test-speeddial-logic.js`
covers `filterActions`, `visibleFavourites` (incl. the runnable guard),
`selectedActionId`, `resultCountText`, `actionLabel`, `tileCode`,
`nextSel`, and payload seeding - pure helpers, DOM-free.
- **Backend:** `ActionRegistry` FNV-1a id golden-vector Catch2 test
(`test_speed_dial_action_id`) pins the hash; the registry was verified
by fresh-context review including a threading fix (`run()` operates on a
stack copy so a queued refresh can't reallocate the action vector
mid-run).
- **Manual (all passing):**
- Space opens the dial in Prepare only; no regression to existing
Prepare-tab keys or the Plugins dialog.
- Search auto-focuses; typing filters live; a freshly-run action rises
in the frecency order.
- Up jumps to the favourites bar, Down into the list, Alt+1..9 hits
favourites; Enter and double-click run the highlighted action.
- Star pins/unpins an action; favourites persist across an app restart.
- "Run X?" confirm with per-plugin "don't ask again" is respected on
later runs.
- The `?` shortcuts dialog shows the Space row.
- Verified in both light and dark themes.
## Known Issues
When there are no actions, plugins, or scripts, the search bar will show
"Search 0 actions". This is bad UX. One alternative considered was to
show a call to action, for example "Please load plugin scripts so that
they appear here".
However, this is ultimately not implemented, as eventually it is not
expected that actions will be empty. The action registry will not be
expected to be empty because we will include in-app actions such as
opening dialogues or other app actions.
<img width="722" height="117" alt="image"
src="https://github.com/user-attachments/assets/a4b9c0db-b2bf-44bc-9550-398dd8b4c7aa"
/>
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
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.
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).
Drop the orphaned PluginCallbackList (dead
after the PluginManager migration) and hop
run_on_*_callbacks onto the UI thread via
CallAfter, snapshotting under the mutex on the
worker first. Keeps wx subscribers off the
detached load/unload workers.
Only one action source ever existed, so the
IActionSource interface and ScriptActionSource
are gone. ActionRegistry now subscribes to the
plugin loader and enumerates actions directly
in init() - no polymorphism for one impl.
Replace the opaque FNV-hash SpeedDialActionId
with a readable composed id of the form
prefix:title:source_key. Split AppAction's
single source field into source_key (stable
identity, e.g. plugin_key) and source_name
(display), so identity and display no longer
share one field.
* 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
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.
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.
Keep action identity and display metadata
constructor-set so registry keys cannot drift
from the objects they index.
Require sources to publish unique ownership while
the registry retains shared keepalive for runs.
Preserve opaque IDs and persisted state keys.
Lifecycle callbacks can be registered while
another thread dispatches them, risking races
and iterator invalidation.
Snapshot shared callback objects under a mutex,
then invoke them unlocked to preserve reentrancy
and mutable callback state.
Move plugin capability enumeration, loader
subscriptions, and action construction into
ScriptActionSource. Keep ActionRegistry focused
on generic action state, dispatch, and snapshots.
Use source rather than package for generic origin
metadata so future non-plugin providers share the
same interface. Action IDs and persisted
configuration remain unchanged.
Subscribe before initial enumeration so plugin
events cannot be missed between the snapshot and
callback registration. Add a focused test for
source startup.
A dual-nozzle H2C print with support filament hangs at its first nozzle
switch. The emitted file shows the change-filament block's M620 O ordinal
jumping from O1 straight to O230, plus a duplicate "M1020 S<n>" toolchange
command right after every change block. Two causes, fixed together because
they interlock (the ordinal check keys off the same toolchange detection
that suppresses the duplicate):
- append_tcr incremented m_toolchange_count once per prime-tower visit
(roughly once per layer), while the change-filament template only emits
its M620 O{toolchange_count + 1} line on real filament changes. With 229
change-less sparse tower layers below the first support layer, the first
real change reported ordinal 230. The counter now advances only when the
expanded change block really contains a toolchange command, and the
placeholder exposes the upcoming change's ordinal (count + 1). The
set_extruder path already counted per real change and is unchanged.
- toolchange_prefix() returned "M1020 S" for BBL printers, so the
custom_gcode_changes_tool() dedup could never match the stock profiles'
line-leading "T[next_filament_id] ..." commands and the writer's own
toolchange was appended after every change block on dual-extruder
machines. The prefix is now the plain "T" (the manual-filament-change tag
branch stays first), and the M1020 form moved into GCodeWriter::toolchange()
as an explicit branch that also carries the nozzle:
"M1020 S<filament> H<nozzle>". The nozzle parameter is signed on purpose:
the null-safe nozzle lookup legitimately yields -1, matching the stock
templates' own H-1 convention.
The prefix change also lets the CoolingBuffer recognize the change blocks'
T commands as tool boundaries on BBL printers (its per-filament attribution
previously keyed off the duplicate M1020, or nothing at all on
single-extruder models); its existing out-of-range guard ignores
T1000-class machine commands.
Verification: full suites green (libslic3r 48998 assertions / 169 cases;
fff_print 692 / 65 including three new scenarios - writer emission per
printer kind, dedup + ordinal progression on sequential prints, and a
prime-tower regression scenario verified to fail against the old per-visit
counting). Byte gate: 18 of 20 fixtures bit-identical; the sequential repro
differs by exactly its 3 removed duplicate M1020 lines, deterministic
across two runs. Reslicing the field project that exposed the hang yields
M620 O1 followed by a gapless O2..O59 and zero duplicate M1020 lines.
Co-authored-by: songwei.li <songwei.li@bambulab.com>
The time estimator's speed/acceleration limits were indexed by time
mode only, reading slot 0 of the per-(extruder x volume-type) arrays
the multi-extruder profiles already carry (H2C 0.4: 8 entries, H2D
0.4: 10). Every move was therefore modelled with the first machine
slot's limits regardless of which nozzle variant was printing -
estimation fidelity only, since emitted feedrates/accelerations are
decided on the slicing side.
Now the estimator resolves the machine slot of the nozzle currently
mounted in the active extruder: the nozzle grouping context is handed
to the processor BEFORE the streaming replay (new member + setter -
deliberately separate from the post-stream result-field handover that
gates the richer change-time model, whose timing is unchanged), the
occupancy recorder is populated on every filament change (bookkeeping
decoupled from the gated time model; recorder writes have no time
effect), and get_machine_config_idx maps (volume type x extruder type
x extruder) to the slot via the printer's variant layout, newly
carried on the processor result. The feedrate/acceleration getters
gain a slot parameter indexing [slot*2 + mode]; jerk and the
print/travel/retract accelerations stay mode-only. Reloaded sliced
projects re-estimate with the result's saved grouping context;
imported bare g-code degrades to slot 0 - the historical read.
M201/M203 write the parsed value into EVERY slot's mode entry (a
firmware envelope change is global), which keeps per-slot reads in
lockstep with the mode-only reads they replace: the fleet emits
envelope lines before any motion, so estimates - hence the estimated
time header, M73 lines, and every other byte - are unchanged (20/20
pinned-slice byte gate bit-identical, incl. the sequential repro
sliced twice). Fidelity improves where envelope emission is off or a
migrating per-layer plan moves filaments across variants.
Tests: a stub-driven processor case proving the slot follows the
active nozzle through the exact production path (T..H.. commands,
fallback recorder bookkeeping, 4x time ratio on the slow variant),
that emitted M201/M203 reach every slot, and that a missing context
degrades to slot 0. Suites green (libslic3r 48998/169, fff_print
667/62).
When a per-layer nozzle grouping migrates a filament across nozzle
variants, the write-back turns two groups of config arrays from
filament-indexed into column-indexed: the per-variant filament options
(one column per variant a filament uses) and the merged extruder
retract overrides (resized to the column count by apply_override).
Export-path readers that still indexed them with the raw filament id
read a neighbor's column for every filament ordered after a migrating
one: toolchange/standby temperatures (M104/M109), retraction lengths
and feedrates, wipe distance, z-hop types, air-filtration keys, and -
through the Extruder's cached flow term - the extrusion E of every
move.
Now every such read resolves its column through the existing
layer-aware resolver (get_filament_config_index ->
Print::get_filament_config_indx), which returns the raw filament id
whenever no per-layer grouping result is published, so static prints
are byte-inert by construction. The Extruder itself has no layer
knowledge, so it gains an injected config column (set_config_index,
default = filament id) that the generator refreshes at the only two
resolution-changing events - layer change and writer toolchange - and
that re-syncs the cached e_per_mm3 flow term. Old-filament reads
resolve at the current layer, which is safe because the per-layer maps
are gap-filled carry-forward. Whole-array placeholder copies
(toolchange temperature overrides) are rebuilt in filament order,
mirroring the existing per-variant placeholder remap. The resolvers
move to the public section so non-friend helpers (ooze prevention) can
resolve too.
Documented, deliberately unchanged: the wipe tower's per-filament
parameter rows (no layer dimension; tower x per-layer grouping is a
follow-up), travel_slope's physical-extruder read, estimator pre-heat
bookkeeping temps, and index-0 header diagnostics.
Verification: new Extruder column-injection scenario (defaults, column
follow + flow-cache rescale, filament-indexed reads unaffected, reset
semantics) and a migrating write-back case proving the column shift for
filaments ordered after a migrator and the resolver tracking it (11 +
14 assertions); suites green (libslic3r 48998/169, fff_print 655/61);
20/20 pinned-slice byte gate bit-identical (incl. sequential repro x2
deterministic).
When the per-layer filament selector (enable_filament_dynamic_map)
migrates a filament across nozzle variants (e.g. Standard -> High Flow),
the config write-back only stored the derived extruder map; every
per-variant filament value (retraction, nozzle temperature, flow,
flush...) kept the numbers resolved from the pre-slice static mapping.
Now both dynamic write-back sites (the by-layer branch and the
sequential stitch) branch on the result's dynamic support. Migrating
results run a mixed-filament expansion that regathers every
filament_options_with_variant key from the pristine per-variant
superset, giving a migrating filament one config slot per (extruder
type x nozzle volume type) it lands on - filament_self_index,
filament_extruder_variant, and all value arrays grow in lockstep - and
recompute the retract overrides with per-slot machine indices so a nil
slot falls back to its own variant's machine value. Non-migrating
dynamic results take the merged three-map write-back so re-applies
reproduce from the written maps. Unrouted filaments resolve from the
result's own default map, so slot resolution never depends on
filament_map round-tripping through the plate config.
Print::apply reproduces the identical expansion from the persisted
group result (shared dedupe helper, expansion function, and slot
indices on both sides): the expanded keys sit in the psWipeTower /
psGCodeExport invalidate lists, so without the reproduction every
re-apply after a selector slice would diff non-empty and permanently
invalidate. cal_non_support_filaments now resolves the extruder per
layer from the published result for dynamic groupings.
filament_map_2 keeps its apply-time static derivation; nothing on the
dynamic path reads it (the per-slot machine indices key the override
merge), and per-(extruder x volume-type) machine limits in the g-code
processor remain a documented follow-up.
Every change is gated behind is_dynamic_group_reorder() or a persisted
result with dynamic support; no profile sets the flag, so the static
fleet's instruction stream is unchanged (20/20 pinned-slice byte gate
identical, incl. the sequential repro sliced twice, deterministic).
Tests: expansion unit coverage (migrating slots, unrouted fallback via
the default map, mis-sized volume map ignored, nullable retract keys in
lockstep, slot machine index layout), an end-to-end stub-driven
write-back asserting expanded slots, per-layer config-index resolution,
the override merge incl. the nil-slot variant fallback, and re-apply
stability, plus a real selector slice staying valid across re-apply.
Suites green (libslic3r 48987/168, fff_print 633/60).
Sequential (by-object) prints were incoherent with the per-layer filament
selector (enable_filament_dynamic_map): the by-object branch published a
static grouping while each per-object ToolOrdering independently ran the
dynamic planner from an empty nozzle status and wrote its own map to the
config (one write per object, last object wins). The exported toolchange
sequences then disagreed with the published result that drives the
per-layer maps, placeholders, and selector emission.
Now the by-object branch, when the selector is enabled, plans each unique
object once — threading the physical nozzle occupancy and the previous
object's last filament into the next plan — stitches the per-object
per-layer nozzle maps into one print-wide result (gap-filled by the new
normalize_nozzle_map_per_layer so any layer index resolves a filament's
nozzle consistently), publishes it, and writes the derived extruder map
back once. The plans are cached on the Print and g-code export consumes
the cache: the ToolOrdering seed changes the plan input (dontcare
assignment, first-layer reorder), so a fresh export-time construction
could re-plan differently from the published stitch. The per-object
dynamic write-back is gated off for sequential prints.
Every change is gated behind is_dynamic_group_reorder(); no profile sets
the flag, so the static fleet's instruction stream is unchanged (20/20
pinned-slice byte gate identical, incl. the by-object repro sliced twice).
Tests: normalize unit coverage (carry-forward, back-fill, ragged input),
stitched-blocks selector detection, and an end-to-end by-object selector
slice (apply -> process -> export) asserting the published stitched
result, one cached plan per object, the config write-back, and a clean
export. Suites green (libslic3r 48958/165, fff_print 633/60).
Experimental fuzzy on geometry
Mirrors libslic3r's fuzzy_polyline on the slice contours at Step.posSlice,
demonstrating the count-changing mutation idiom (rebuild ring via
Polygon.append, write back via ex.contour / ex.set_holes). C++ analogue
test proves area preservation, cascade, and bounded displacement.
The Print-level LayeredNozzleGroupResult had a single producer, the
by-layer branch of ToolOrdering, which is gated to non-sequential prints.
The by-object branch in Print::process computed a grouping only in auto
map modes and never stored it, so a sequential slice exported with a null
group result: the per-nozzle placeholder tables came up empty and any
start g-code indexing nozzle_diameter_at_nozzle_id[] aborted with
"Indexing an empty vector variable". A prior by-layer slice masked the
bug by leaving its (never cleared) result on the Print.
Now the by-object branch runs get_recommended_filament_maps in every
static map mode (in manual modes the result mirrors the user's
assignment, deviations throw as in by-layer) and publishes it
print-wide. The config write-back stays gated to auto modes: in manual
modes it would only re-store the pre-slice values.
Regression test: a two-object by-object print must publish a non-null
group result and resolve nozzle_diameter_at_nozzle_id[] in start g-code
(both fail without the fix). Suites green (libslic3r 48929/162,
fff_print 633/60); 18-fixture byte gate identical; the by-object repro
project goes from the export error to valid g-code, determinism x2.
* ENH: config: add logic to apply params to object/region config with multi-extruder
JIRA: no-jira
Change-Id: Ieab98cd8d031e5ca82a3aad2d0b89d8ae4a794f1
(cherry picked from commit 3179fd416e68ca8bc2d746f859508d07db18fe5b)
* FIX: X1C switch to H2D lose Highflow parameter
Jira: STUDIO-15272
Change-Id: Id8cf5d93a49d5542ac82f9554974b458e15c1193
(cherry picked from commit 15d9f072ff658a3beb4f916d978dfea12c2d9f16)
* Fix mishandling of `stride` param and add unit test for it
* Fix modified multi-variant per-obj option highlight
* Fix issue that per-obj FloatsOrPercents options are marked as dirty incorrectly when lost focus
---------
Co-authored-by: lane.wei <lane.wei@bambulab.com>
Co-authored-by: weiting.ji <weiting.ji@bambulab.com>
Print::apply rebuilds m_config.filament_map_2 to the real per-filament slot
map on every apply, while the incoming full config only ever carries the
ConfigDef default. The resulting phantom one-key print_diff hit the
invalidator's catch-all branch and killed every print-level step on each
apply, so on multi-extruder printers a fresh slice result was invalidated
the moment the GUI re-applied after slicing completed.
Dropping the key from print_diff loses no information: it is never a user
input, and the rebuild derives it from filament_map, filament_volume_map
and the variant slots, each of which is diffed and invalidation-listed on
its own.
Regression test: re-applying an unchanged config after process() must not
invalidate psSlicingFinished (fails with APPLY_STATUS_INVALIDATED without
the fix). Suites green (libslic3r 48891/154, fff_print 631/59); 19-fixture
byte gate identical incl. the Hybrid repro project, determinism x2.
PluginHostApi.cpp had grown into one TU holding the module entry point plus
three unrelated domains (presets, model/mesh graph, app access), and
PluginHostSlicing.cpp mixed ownable geometry value types with the
non-owning live print graph. Reorganize the orca.host surface into
plugin/host/ with one registrar per domain:
- PluginHost.hpp/.cpp entry point (replaces PluginHostApi)
- PluginHostBindings.hpp internal per-domain registrar declarations
- PluginHostGeometry.cpp BoundingBox, Point, Polygon, ExPolygon + ndarray parsing
- PluginHostMesh.hpp/.cpp TriangleMesh snapshot (own TU ahead of planned
mesh construct/mutate APIs)
- PluginHostPresets.cpp Preset, PresetCollection, PresetBundle
- PluginHostModel.cpp scene graph: Model, ModelObject, ModelInstance, ModelVolume
- PluginHostApp.cpp Plater + plater()/model()/preset_bundle() accessors
- PluginHostSlicing.cpp live print graph only, now with a single lifetime story
- PluginHostUi.hpp/.cpp moved unchanged
PluginBindingUtils.hpp stays at plugin/ root: it is shared with pluginTypes/
and tests, not host/-specific.
No Python-visible change: same submodules, class names and docstrings.
Verified with slic3rutils and fff_print suites.