* Toolchange Cyclic Order
* Apply cyclic order to first layer
* Unit test
* Copilot fixes
---------
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
The preview brim, the placement margin and the pre-generation validation
warning each decided on their own whether the tower has a Type2 cone
base, reading the wall type and cone angle three different ways. The
preview's read cast the preset's enum to ConfigOptionEnum<T>, which a
preset-shaped config never holds, so the cone base was never previewed.
estimate_wipe_tower_first_layer_outline now answers that question once,
beside the footprint estimate, from the config and the resolved planner;
all three sites take the outline from it. The libslic3r case reads the
outline off a preset-shaped config, where the old cast came back empty.
The clamps and validation work from estimates. Once the tower is
generated, _make_wipe_tower re-tests the exact first-layer footprint,
brim and cone base included, against the printable area and the
exclusion zone, so an off-plate tower fails with a clear error instead
of exporting unprintable G-code. The rectangle-wall mesh footprint
learns the Type2 cone base so that check and the post-generation
validation see the real outline.
Pre-generation, validation hard-checks the body plus an explicit brim
and warns on the estimated auto brim and cone base with the existing
"may collide" strings, so the user hears about a marginal position on
the first slice rather than only at generation time.
Two fff_print fixtures that print a tower at the default position move
it onto the 200 mm test bed, as the multifilament fixtures already do:
the shipped default y of 220 is off that bed, and the backstop now says
so instead of exporting the tower.
The shared estimate reserved every tower with one volume-per-purge rule
and the stability floor. Both planners do more: WipeTower (Type1) wipes
each filament's own prime volume in whole lines, one block per
adhesiveness category sized by its worst layer, rams the leaving
filament at every nozzle change, and squares a rib tower from the
planned depth; WipeTower2 (Type2) spaces its lines by
wipe_tower_extra_spacing, not the Type1-only infill gap, and its extra
flow cancels out of the depth. Both extend the ribs rather than the body
below the stability minimum, size every layer including a thinner first
one, and lay the brim in whole loops, WipeTower reporting half a spacing
of line width on top.
All of that now lives in estimate_wipe_tower_footprint, fed the planner
(resolve_wipe_tower_type mirrors Print::wipe_tower_type and the CLI's
Bambu Lab detection) and the filament ids rather than a count. Print
passes its own tool set; the PartPlate adapter derives the plate's ids
from the passed config and treats an explicit count as a floor, so the
CLI's count-only callers size per filament too. The placement clamp also
reserves a Type2 cone's base bulge, which the body box does not cover.
The planner-mirroring helpers sit beside the planners in WipeTower and
WipeTower2 so the two stay in sync; the libslic3r cases pin them to
footprints measured from generated G-code.
Validation grows the estimated body by the brim before the tower is
generated, so a tower whose brim leaves the bed is rejected up front
instead of at export. The scene reload re-clamps the stored position,
since set_default_wipe_tower_pos_for_plate does not rerun when painting
changes the filament count. The rectangle-wall footprint polygon gets its
two missing brim corners (it was a skewed quad), so the post-generation
check covers the whole brim.
A raft is not a reason to reserve a tower. Print::apply runs
normalize_fdm_2, which clears enable_prime_tower for a plate that purges
one filament unless smooth timelapse or wrapping detection is on, so a
single-filament plate with a raft prints no tower at all and the estimate
was reserving bed area for one. Drop the input; need_wipe_tower is now
exactly the two exceptions normalize_fdm_2 honours, named there so the
next reason added has to be checked against it.
The GUI preview and the validation containment check each re-derived
"is a tower printed here" from the filament count instead of reading the
estimate, so both missed the towers printed with no tool change to purge
for. They now take the answer from the footprint, which is the drift this
shared estimate exists to remove. A tower that is not printed estimates to
zero, so its hull is degenerate and every check on it passes trivially -
the containment check needs no gate of its own.
WipeTowerData::width was written only by the pre-generation estimate and
left at zero for the whole post-generation life of the Print, while its
neighbour depth held the real value. Set it from the generator in both
branches.
The plate's height scan transformed every model part's full mesh per
instance on each scene reload, discarding all but the z extent. The
cached convex hull has the same z extent.
A plate loaded from a sliced .gcode.3mf holds no objects and its filaments
live in slice_filaments_info; the config-taking get_extruders overload
returned an empty list for it, which sized the tower for a placeholder two
filaments. It now answers the way the wx overload does, without reaching
the plater.
Also drop estimate_wipe_tower_size, which has no callers.
* build: enable /Zc:lambda for MSVC
MSVC keeps its legacy lambda processor under /std:c++17, which rejects
reading a constexpr constant inside a lambda that does not capture it
(C3493). No other compiler requires that capture, and clang reports it as
an unused one, so the two cannot both be satisfied without the flag.
/Zc:lambda selects the conforming lambda parser that clang and GCC
already use. It is implied by /std:c++20 and /permissive-, so it is only
needed while we are on C++17. clang-cl is conforming already and does not
take the flag.
It requires VS2019 16.8, so build_release_vs.bat now says 16.8+.
* build: clear 237 unused lambda capture warnings
236 captures across 81 files, 142 of them `this`. Removing an unused
capture changes no behavior; clang does not report a capture whose type
has a non-trivial destructor, so nothing held only to extend an object's
lifetime is in this set.
Nine of them are the second half of the warning, "is not required to be
captured for this use", where the capture is a const or constexpr value
the body does read. Those depend on the /Zc:lambda change in the previous
commit. One of them, in FillRectilinear.cpp, had been worked around with
an #ifndef __APPLE__ guard around the capture list, which is now gone.
GUI_ObjectTableSettings.cpp captured its reset button only to read it
inside #ifdef __WXOSX_MAC__. That branch now takes the button from the
event it is already handling.
* build: fail configure on MSVC older than 19.28 instead of dropping /Zc:lambda
cl.exe answers an unrecognized /Zc: sub-option with warning D9002 and keeps
going, so on VS2019 before 16.8 the flag is silently ignored and the build
instead dies with C3493 in FillRectilinear.cpp, nowhere near the cause.
* fix: delete three locals that are now unused
Their only remaining use was the lambda capture this branch removed. The
Clang builds set -Wno-unused-variable, so the build never flagged them.
---------
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
* build: remove std::move that blocks copy elision
std::move wrapped around a temporary, or around a local being returned,
stops the compiler constructing it in place. Each edit is the fix clang
suggests, which is to delete the std::move call and keep its argument.
Three of the 39 sites save a move, the two return std::move(local) in
Print.cpp and TreeSupport.cpp:2749. The rest are equivalent either way
and match how the codebase already writes this elsewhere.
Clears 39 -Wpessimizing-move warnings.
* build: drop null checks on references and this
A reference cannot be bound to null and this cannot be null, so the
compiler folds these conditions to true and drops the guard. Seven are
if (&bitmap && bitmap.IsOk()), where IsOk() already does the work; two
test this directly. The guarded code runs either way, so removing the
dead operand changes nothing.
Clears 11 -Wundefined-bool-conversion warnings.
Prime towers reserved depth from the prime volume alone, ignoring the flush
matrix: rib-wall towers in both the engine and the preview, and rectangle and
cone towers in the preview, which never carried the flush-aware estimate the
engine already used. The preview also read the print preset, which does not
carry the printer- and filament-scope keys the estimate needs and so silently
fell back to defaults. On multi-nozzle printers the flush matrix, which holds
one block per nozzle, was additionally read as a single block. The tower could
come out too small for the purge it has to hold.
The flush-based estimate also skipped the height-based minimum depth that the
prime-volume one applies, so low-flush prints could estimate a tower shallower
than the one that actually gets built.
The placement clamps and the tower-approach router both stood in the bed's
bounding box for the bed itself, so on a delta or hexagonal bed the prime tower
could be parked in a corner that does not exist and the nozzle could be routed
across it. Both now test the real printable outline, slicing reports a tower
that does not fit instead of printing it off the bed, and a tower parked near an
edge is routed along the clamped side rather than falling back to a straight
line across the tower.
Also fixes the placement validation rotating the tower hull by degrees read as
radians about the plate origin, and never rotating the generated tower footprint
at all.
Adds a printer option that picks up the new tool without a blocking temperature
wait, travels to the wipe tower, and waits there right before purging, parked
beside the tower so the ooze from the heat-up lands next to it rather than on the
model. The incoming filament's target is raised ahead of the tool change, so the
heat-up overlaps both the change itself and the travel to the tower.
Off by default, and only offered for multi-extruder printers using a Type 2 wipe
tower; the generic toolchanger profile enables it.
The rib tower is now always square (prime_tower_width is ignored, as the
GUI already implies), carries the rib origin offset like the BBL tower so
the rib tips sit inside the configured position, clamps the rib length to
the tower diagonal, and extends the ribs for short towers.
# 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
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).
# 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 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).