# Description
Changing a slicing plugin's configuration had no effect on the sliced
result until you forced a re-slice some other way; it now applies
immediately. Print, printer and filament presets also keep their plugin
configuration separately, so configuring a plugin on one no longer wipes
out what you set on another.
A plugin's custom configuration page gets the same round of improvements
in both the Plugins dialog and the per-preset dialog: it follows the
app's light/dark theme, keeps its state while you edit instead of
resetting under the cursor, and can tell whether it is being edited
globally or for a preset, so "Restore defaults" can be labeled for what
it will actually do. The two bundled examples show this off — Twistify
now ships a custom configuration UI, and Inspector is themed, groups
# Screenshots/Recordings/Graphs
https://github.com/user-attachments/assets/02ca062a-5143-49a3-abe0-a2a040b3a928
## 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)
## Problem
On H2C (carousel) printers, the wipe tower purge volume calculation in
`_make_wipe_tower()` tracks filament state **per-extruder** (2 slots).
Since H2C has up to 7 carousel nozzle slots on a single extruder, all
filaments sharing that extruder are collapsed into one tracking slot.
This causes:
Test:
[5cubes.3mf.zip](https://github.com/user-attachments/files/30092551/5cubes.3mf.zip)
- **Massive redundant AMS flushing** every filament change on the
carousel triggers a full purge against the "previous" filament, even
when the target nozzle slot already has the correct filament loaded
- **60.9g total weight** instead of ~17g (**3.5× material waste**)
- **3h09m print time** instead of ~1h57m (**60% longer**)
## Root Cause
The code uses `nozzle_cur_filament_ids[extruder_id]` (a 2-element array)
to track which filament was last used for each extruder. BambuStudio
uses `NozzleStatusRecorder`, which tracks per `group_id` (physical
carousel slot 0..6).
## Changes
| File | Change |
|---|---|
| `Print.cpp` | Replace `nozzle_cur_filament_ids` with
`NozzleStatusRecorder`. Use `get_nozzle_for_filament()` to resolve the
physical carousel slot per layer. Select `filament_prime_volume_nc` for
nozzle changes, `filament_prime_volume` for filament changes. |
| `PrintConfig.hpp` | Add `ConfigOptionFloats filament_prime_volume`
(per-filament EC prime volume, missing from upstream but present in BBS
and H2C profiles) |
| `PrintConfig.cpp` | Register `filament_prime_volume` with default
45mm³ (matching BBS) |
| `Preset.cpp` | Add `filament_prime_volume` to preset keys |
Also includes `tests/compare_analyzer/` - two standalone Python tools
for G-code slice comparison and temperature timeline analysis (stdlib
only, no dependencies).
## Test Results (5-color H2C Hybrid print, same 3mf project)
| Metric | Upstream (broken) | **Fixed** | BBS (reference) |
|---|---|---|---|
| **Total weight** | 60.90g | **16.20g** ✅ | 17.47g |
| **Print time** | 3h09m | **1h57m** ✅ | 1h51m |
| **Filament changes** | 105 | 105 | 140 |
| **Tool changes** | 35 | 35 | 35 |
| **Critical discrepancies vs BBS** | ⚠️ YES | ✅ None | — |
## Analysis Tools (`tests/compare_analyzer/`)
Two standalone Python tools (stdlib only, no dependencies) for deep
G-code comparison:
- **`compare_slices.py`** - comprehensive .3mf slice comparison:
filament usage, nozzle mapping, tool change sequences, prime tower
analysis, temperature timeline, retract parameters, and automatic
critical discrepancy detection (weight/time anomalies)
- **`show_temp_plot.py`** - interactive HTML temperature timeline
plotter for visualising heater profiles during multi-nozzle prints
(supports single-file and side-by-side comparison)
Usage:
```bash
python3 tests/compare_analyzer/compare_slices.py file1.3mf file2.3mf --labels "Upstream" "Fixed"
python3 tests/compare_analyzer/show_temp_plot.py file1.3mf file2.3mf
```
## Screenshots
### OrcaSlicer Upstream (unfixed) - 60.90g, 3h09m
<img width="1512" height="982" alt="Screenshot 2026-07-16 at 15 22 58"
src="https://github.com/user-attachments/assets/3efb2bff-ff1e-43db-9669-feafa5921b51"
/>
### OrcaSlicer Fixed - 16.20g, 1h57m
<img width="1512" height="982" alt="Screenshot 2026-07-16 at 15 23 08"
src="https://github.com/user-attachments/assets/c1d36dc5-9b01-4691-80ef-7364540e1f4e"
/>
### BambuStudio Reference - 17.47g, 1h51m
<img width="1512" height="982" alt="Screenshot 2026-07-16 at 15 24 52"
src="https://github.com/user-attachments/assets/5c0b11e2-64f4-40e9-8c7c-3f42519786c3"
/>
### Temperature Timeline: Upstream vs Fixed
<img width="1511" height="829" alt="Screenshot 2026-07-16 at 15 23 35"
src="https://github.com/user-attachments/assets/926c2cb5-dfd4-4ce0-bb40-82e6165eb134"
/>
### Temperature Timeline: Fixed vs BBS
<img width="1512" height="825" alt="Screenshot 2026-07-16 at 15 23 51"
src="https://github.com/user-attachments/assets/85c72dda-e779-4aa6-8118-fb17e5f8482d"
/>
## Compatibility
Safe for non-carousel printers: when each extruder has a single nozzle,
`group_id == extruder_id`, so `NozzleStatusRecorder` behaves identically
to the original per-extruder tracking. The `filament_prime_volume`
default (45mm³) matches the existing global `prime_volume` default.
## Reference
BambuStudio `Print.cpp` `_make_wipe_tower()` L3341-3392 -
`NozzleStatusRecorder` pattern.
- WipeTower: use filament_ramming_volumetric_speed(_nc) for ramming, falling back to
max_vol_speed only when nil; gate precool temps on enable_pre_heating
- ToolOrderUtils: disable the inter-layer forecast in the per-nozzle base reorder so
H2D/H2C ordering is unchanged
- PrintConfig: stop stripping filament_prime_volume in handle_legacy; document that
prime_volume drives the Type2 wipe tower and filament_prime_volume the Type1 one
- GCodeProcessor: exclude post-print end-gcode M400 dwells from the M73 estimate and
drop the dead air-filtration state
- Trim verbose BambuStudio source-location comments across the port
Resolved 4 conflicts:
- PrintConfig.hpp / Preset.cpp: upstream restored calib_flowrate_topinfill_special_order
(removed in the prior merge, now re-added and re-registered); kept it alongside our
IMEX options (imex_parallel_mode, imex_head_filament_map).
- Plater.cpp: kept our <sstream> include plus upstream's <optional> and plugin includes.
- tests/fff_print/test_gcodewriter.cpp: both sides appended disjoint scenarios after a
shared base. Reconstructed as upstream's full file (base + its 3 toolchange/H2C
scenarios + the boost/filesystem include its helper needs) followed by our 10
set_pressure_advance / set_temperature scenarios. 22 scenarios total, no duplicates.
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 upstream _make_wipe_tower() tracked purge volumes per-extruder (2 slots),
which collapsed all H2C carousel filaments into one slot and caused massive
redundant AMS flushing (~40g instead of ~0.4g).
Changes:
- Print.cpp: Replace per-extruder nozzle_cur_filament_ids with BBS
NozzleStatusRecorder that tracks per group_id (carousel slot 0..6).
Use get_nozzle_for_filament() to resolve physical slot per layer.
Select filament_prime_volume_nc for nozzle changes, filament_prime_volume
for filament changes (BBS pattern).
- PrintConfig.hpp/cpp: Register filament_prime_volume (per-filament EC prime
volume, default 45mm³) matching BBS PrintConfig.
- Preset.cpp: Add filament_prime_volume to preset keys list.
Safe for non-carousel printers: group_id == extruder_id when each extruder
has one nozzle, so NozzleStatusRecorder behaves identically to the original
per-extruder tracking.
Reference to BBS: BambuStudio/src/libslic3r/Print.cpp _make_wipe_tower() L3341-3392
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).
Resolved conflicts in PrintConfig.hpp and Preset.cpp. Both were adjacent to upstream's
removal of calib_flowrate_topinfill_special_order (superseded by the new
top/bottom_surface_fill_order options); took upstream's removal from the option class
and the print-options list while keeping our new IMEX options (imex_parallel_mode,
imex_head_filament_map) and the IMEX enums (ImexToolLayout, ImexVizTheme) alongside
upstream's new SurfaceFillOrder enum. The option remains in PrintConfig's legacy
ignore-set so old projects still load.
Conflicts were all co-located additions rather than design collisions:
- GCode.cpp: adopt upstream's toolchange(filament_id, nozzle_id) signature and
per-variant set_config_index() while keeping the IMEX bare-T<n> suppression;
rebase the second-layer temperature loop's non-IMEX branch onto upstream's
get_filament_config_index() resolution.
- Preset.cpp / PresetBundle.cpp / PrintConfig.cpp: keep both sides' option-list
and enum-map entries.
- GLCanvas3D.cpp: upstream's printable_heights argument plus the IMEX ghost pass.
- PartPlate.cpp: keep <set> (still used).
- test_gcodewriter.cpp / test_3mf.cpp: keep both sides' test cases.
Resolves two conflicts:
- GLCanvas3D.hpp: keep both the IMEX ghost render declarations and
upstream's _render_wireframe_overlay().
- test_gcodewriter.cpp: both sides appended test cases to the same
region; keep upstream's origin/machine-limit tests alongside the
pressure-advance and temperature scenarios.
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).
* 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::update_filament_maps_to_config takes filament/volume/nozzle
maps, backfills an empty volume map from extruder types, rebuilds
filament_map_2, re-expands the per-filament variant arrays, and
recomputes retract overrides keyed by resolved slots
- grouping writes its result back in every non-sequential mode;
manual multi-nozzle grouping validates the user mapping and raises a
translatable error on deviation; the engine's concrete volume
assignment is deliberately not merged yet (per-filament arrays are
already consumed by filament id, so materializing High Flow now
would change motion before the layer-aware resolvers land)
- Print::apply treats the three map keys as engine outputs in auto
modes (erased from the diff and adopted), compares them against used
filaments in manual mode, and keeps the pre-expansion snapshot in
sync with the late normalization pass so rebuilt headers reflect the
sliced state instead of resurrecting stale values
- volume/nozzle maps and extruder_nozzle_stats join the invalidation
group of filament_map (wipe tower + skirt/brim)
- PresetBundle composes full configs with an optional per-filament
volume map (plate map, else defaults derived from each extruder's
flow type); project config keeps the map sized across filament
count changes
- PartPlate stores per-plate volume/nozzle maps; Plater injects them
at every slice-composition site (incl. g-code reload and wipe-tower
estimation); BackgroundSlicingProcess reads engine results back to
the plate in auto modes
- per-filament map trust guards relaxed to size-match everywhere now
that every producer sizes the map; single-filament explicit flow
assignments are honored
- tests: grouping volume maps stay concrete, merge semantics of
update_used_filament_values, single-filament override honoring
Motion g-code is byte-identical fleet-wide including Hybrid projects
(19-fixture gate + repro determinism double-slice). Header deltas:
the map keys now dump real values, and stale pre-normalization values
(e.g. enable_prime_tower on single-used-filament prints) no longer
leak into the config block.
- Print::get_nozzle_config_index / get_filament_config_indx resolve a
filament's variant slot per layer from the nozzle group result, with
hashed index caches; when no group result is published (sequential
prints), they fall back to the static filament->extruder mapping so
behavior is unchanged
- filament_map_2 caches each filament's resolved print-variant slot;
rebuilt in Print::apply after the filament_map diff handling and in
the filament-map write-back
- filament retract overrides now key by slot indices: apply_override
fallback indexing flips to 0-based, Print::apply passes
filament_map/extruder indices, the write-back passes filament_map_2
(identical resolution while slots equal extruders)
- filament_volume_map/filament_nozzle_map/filament_map_2/
filament_self_index become PrintConfig static members (required for
member access); grouping input guards tightened so their registered
1-element defaults are never mistaken for real per-filament maps
(single-filament manual mode keeps the mix-marker fallback)
- update_filament_self_index_cache refreshed at every full-config
assignment
- tests: 0-based apply_override fallback, get_config_index_base
hit/miss/mixed-type cases
The resolvers are not consumed by the g-code writer yet. Non-Hybrid
g-code is unchanged except the config header, which now serializes the
three new static keys (defaults until the per-filament producer lands);
verified by the 19-fixture byte gate: 3 added header lines per fixture,
zero motion changes.
- get_extruder_nozzle_volume_count derives per-extruder volume-type slot
lists from extruder_nozzle_stats (absent stats = one slot per extruder)
- update_values_to_printer_extruders learns the slot layout: when any
extruder mixes volume types, option arrays keep one slot per
(extruder x volume type), extruder-ascending then volume-ascending;
single-slot resolution takes the filament's volume type on mixed
extruders
- update_values_to_printer_extruders_for_multiple_filaments applies a
per-filament nozzle_volume_type override from filament_volume_map
(when sized to the filament count) and remaps filament_self_index
through the same pipeline as every other filament key
- get_config_index_base + is_auto_filament_map_mode helpers (consumers
land with the per-filament config-index resolvers)
- callers updated: PresetBundle composition paths, PrintApply (counts
hoisted above the extruder_applied guard), Print write-back
- new tests: slot counting, Hybrid slot expansion incl. stride 2,
per-filament override, non-Hybrid degeneracy
Non-Hybrid printers keep their variant layout and values (proven by a
19-fixture byte gate; the only header delta is filament_self_index now
flowing through the same variant pipeline as its sibling filament
keys). Hybrid slices grow the config-block variant arrays to one entry
per sub-nozzle volume type; motion g-code is unchanged until the
g-code writer consumes the new slots.
G-code post-processing is now a step of the slicing-pipeline plugin rather than a
separate capability type. One capability class can transform slices at the geometry
seams AND edit the final G-code, behind a single picker/option.
- Add SlicingPipelineStepPlugin::psGCodePostProcess (bound as
orca.slicing.Step.psGCodePostProcess). Unlike the geometry steps it fires from the
GUI export path in PostProcessor.cpp, not from Print::process(): ctx.print/ctx.object
are None and the plugin edits the file at ctx.gcode_path in place. It may run more
than once per slice (file export and/or upload) and its output is not shown in the
preview.
- Extend SlicingPipelineContext with gcode_path/host/output_name and a C++-only
full_config; config_value() falls back to it when there is no live Print.
- PostProcessor.cpp dispatches SlicingPipelinePluginCapability at psGCodePostProcess,
driven by the existing slicing_pipeline_plugin option.
- The exported G-code lives outside data_dir(), so the plugin audit sandbox would
block the write; the trampoline's audit setup grants ctx.gcode_path's folder as a
scoped allowed root, gated on a non-empty gcode_path so the geometry-step hooks gain
no extra filesystem access.
BREAKING CHANGE: the separate G-code post-processing capability type is removed.
- orca.gcode.GCodePluginCapabilityBase and orca.PluginType.PostProcessing are gone;
post-processing plugins migrate to orca.slicing.SlicingPipelineCapabilityBase +
Step.psGCodePostProcess (and gain ctx.params / ctx.config_value()).
- The post_process_plugin config option is removed; use slicing_pipeline_plugin.
Presets carrying the old key degrade to the standard unknown-key warning.
- Manifest type = "post-processing" now maps to Unknown (advisory only; the loader
dispatches on the C++ get_type()).
Also repairs two latent build breaks the branch carried: stale Step enum value usages
in test_slicing_pipeline_hook.cpp and a reference to the removed
ConfigOptionDef::PluginType::None in Tab::on_value_change (now is_plugin_backed()).
Adds the orca_gcode_stamp sample plugin and a psGCodePostProcess binding test.
An extruder with more than one physical sub-nozzle can hold a mix of
Standard and High Flow nozzles. The Flow dropdown now offers Hybrid for
such extruders (extruder_max_nozzle_count > 1, nil-guarded); grouping
already expands a Hybrid extruder into per-volume nozzle groups from
extruder_nozzle_stats.
- sidebar Flow combo offers Hybrid only for multi-sub-nozzle extruders
- preset lookup treats Hybrid as Standard (presets define no Hybrid
variant); variant strings are never fabricated for it
- printer tab splits a Hybrid extruder into Standard + High Flow rows,
with matching selection-index arithmetic and sync-enable rules
- syncing from a printer whose extruder holds mixed nozzle flows now
selects Hybrid instead of collapsing to the dominant flow type
- send-to-printer flow check: a nozzle-rack extruder validates its
nozzle inventory (mounted + rack) against every needed flow instead
of comparing only the mounted nozzle; mounted-flow lookup is now
per-extruder, fixing an index shift when a nozzle reports no flow
- Hybrid is session-only in app config (stored as Standard), so a
fresh session starts from concrete flow types
Printers whose extruders have a single sub-nozzle (including all
dual-extruder machines without a rack) see no new option and identical
check behavior.
cooling_filter_enabled existed as a config option but was shown nowhere,
and there was no capability flag to gate it. The cooling filter and air
filtration are alternative accessories sharing the same duct, so a
printer declares one or the other.
- new hidden printer capability flag support_cooling_filter
- "Use cooling filter" toggle in the Accessory group, shown only when
the printer supports it; the air-filtration toggle hides in that case
(no vendor restriction: third-party printers keep air filtration)
- explicit defaults (0) in the common machine base
- H2C declares support_cooling_filter=1 instead of support_air_filtration;
its start-gcode already carries the cooling-filter conditional, so the
toggle is functional. On H2C this drops the two exhaust-fan lines that
air filtration emitted for ABS-class filaments, matching the printer's
actual duct accessory; H2C is new on this branch so no existing user
output changes.
Printers without the flag keep exactly the previous accessory UI and
g-code.
New printer option fan_direction (undefine/left/right/both, default
undefine) declares which side the auxiliary part-cooling airflow comes
from. When set and the printer has an auxiliary fan, auto-orient adds a
yaw rotation so the dominant overhang area faces the airflow, and newly
added primitive shapes are pre-oriented the same way (except the Cube,
whose axis-aligned bounding box the pressure-advance pattern calibration
depends on).
- FanDirection enum + fan_direction printer option (Accessory group,
enabled only with auxiliary_fan)
- orient engine: weighted overhang areas per candidate, yaw-direction
search, vertical rotation applied on top of the primary orientation;
the cooling weights are taken from the candidate actually chosen,
including the flat-bottom tie-break
- orient_for_cooling() for primitive placement
- set fan_direction=left on H2C/H2D/H2D Pro/X1/X1E/P1S 0.4 profiles
(X1C/H2S/P2S/X2D/Qidi X-Max 4 already carried the key, which now
takes effect)
With fan_direction unset or no auxiliary fan the vertical rotation stays
identity and auto-orient results are unchanged; slicing and g-code are
never affected.
The engine already implements prime_volume_mode (Default/Saving/Fast)
but nothing in the UI could set it, leaving prime-saving unreachable on
multi-sub-nozzle extruders and fast purge unreachable on printers that
support it.
- new PurgeModeDialog with selectable Standard/Fast or
Standard/Prime Saving cards depending on printer capability
- "Purge mode" sidebar button next to Flushing volumes; opens the
dialog and stores the choice in the project config
- printer preset-load gating: button shown only when the printer has
multiple sub-nozzles per extruder or sets support_fast_purge_mode;
stale project values the printer cannot honor reset to Default
- enable fast purge on A2L 0.4 (support_fast_purge_mode), explicit
default 0 in the common machine base
- new dialog strings added to OrcaSlicer.pot
Printers without these capabilities never show the button and their
projects keep prime_volume_mode at Default, so slicing output is
unchanged.
Filament grouping already consumed per-filament forbidden nozzle volume
types, but every call site passed an empty map, so a variant-restricted
filament (e.g. one limited to "Direct Drive TPU High Flow") could be
auto-grouped onto an incompatible nozzle flow type on multi-variant
printers.
- add convert_to_nvt_type() to parse extruder variant strings
- add Print::get_filament_unprintable_flow(): forbidden volume types =
printer extruder variants minus the filament's declared variants;
filaments declaring no variants stay unrestricted
- feed the map into grouping at the by-object path (Print.cpp) and all
six mapping/planning sites in reorder_extruders_for_minimum_flush_volume
- unit-test the string parser
Non-restricted configurations produce an empty map, so existing
printers' grouping and g-code are unchanged.