Brings in upstream/belt-printer (the Sept 14 main merge) plus Hanif Koh's
21 review-fix commits from PR #15685, on top of the MachineKinematics
refactor and the purge-prism / tree-support / first-layer-speed fixes.
Conflict resolution:
- BeltGCodeWriter is gone (kinematics refactor), so Hanif's plate-offset
fix for it is ported into GCodeWriter: the first-layer-plane checks in
travel_to_xy / travel_to_xyz / _travel_to_z now evaluate the plate-local
point, and BeltGCode::init_belt_writer hands the stored plate origin to
the writer it installs.
- init_belt_writer(Print&) takes Hanif's signature; the BBL flag is set on
the surviving writer by GCode::_do_export.
- The shared emit_belt_brim_bands() loop keeps the BeltFloorObjectGuard the
local branch added, so apron bands classify first-layer height against
their own object.
- eager_lift keeps effective_type: it now carries set_force_normal_lift().
- GCodeWriter's initializer list follows Hanif's member order with
m_kinematics in its declared position.
- TreeSupport::detect_overhangs uses Hanif's clamped build_plate_tilt_slope()
for the non-belt path and the belt shear for the belt path.
Fan speeds, multi-tool ramming, the tower interface and flush temperature
fallbacks and the custom G-code placeholders now use the extruder variant a
filament prints with on each layer, instead of reading by filament id.
Upstream moved pressure advance onto the extruder variant: enable_pressure_advance,
pressure_advance and the four adaptive keys joined filament_options_with_variant,
the repeated inline blocks in set_extruder() became a helper, and the lookups
moved from the filament id to get_filament_config_index().
All three conflicts were the same collision, because this branch had modified two
of those same inline blocks to pass a tool qualifier so each carriage is addressed
explicitly in parallel modes. Taking either side whole would have lost something:
upstream's drops the qualifier and leaves parallel carriages with no pressure
advance, ours drops the per-variant indexing and reads the wrong column on a
multi-variant printer. The helper now takes an optional tool, defaulting to -1,
which omits the qualifier. imex_pem_tool_for() already returns -1 off IMEX and in
primary mode, so non-IMEX output is unchanged, and the three call sites that never
passed a tool keep upstream's behavior exactly.
The third conflict was two test cases appended at the same place. Both are kept.
Separately, one defect that merged cleanly and so was not flagged: the loop that
emits pressure advance for secondary carriages at the start of a print still
bounded and indexed those vectors with a raw filament id. They are variant
expanded now, so their length is columns rather than filament slots -- the value
read was the wrong column, and the bound no longer sat in slot space, letting an
out-of-slot filament through. It now bounds on filament_diameter and translates
with get_filament_config_index(), which is what the sibling second-layer
temperature loop already does.
Verified: both changed translation units compile clean under -Werror. The merge
was resolved independently twice and the two resolutions agree on every line of
code. Not yet run: the Release test suite and a parallel-mode slice sweep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pressure advance, adaptive pressure advance and its model can now take a
different value for each extruder variant of a filament, such as Standard and
High Flow nozzles, like the other per-variant filament settings. Projects
saved with one value per filament apply it to every variant of that filament,
and the addnorth BBL filaments in the Orca Filament Library are updated to the
per-variant layout.
The two conflicts are both places where upstream landed on top of IDEX/IQEX
code. In GCode.cpp the relocated file header block meets the IMEX placeholder
block, and the placeholders are set first: file_start_gcode is processed through
the placeholder parser now, which throws on a name it does not know, so a script
naming {imex_mode} would abort the export if the header ran first. In
test_multifilament.cpp both sides appended a case at the end of the file.
Four files overlapped, and each resolution favours upstream where the two sides
had done the same work:
WipeTower's M104/M109 tool qualifier. Both sides bounds-checked the physical
extruder map lookup; upstream omits the T qualifier when the map cannot answer,
where this branch fell back to the logical index and so named a carriage that may
not be the one printing. Upstream's behaviour is what this branch documents
elsewhere, so its version is taken and the local helper is dropped.
get_extruders' mixed-slot switch. Upstream added the same concept to the CLI
overload as expand_mixed_slots, so the GUI overloads' parameter is renamed to
match rather than carrying two names for one idea.
GLCanvas3D's sequential-clearance branch gains upstream's
update_compacted_wipe_tower_clearance for the by-layer case.
The printer_agent re-sync in TabPrinter::reload_config was upstream's and their
preset-undo fix removed it, so it goes; the IMEX modes grid re-sync beside it
stays, since it spans three options and is not a Field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nozzle_temperature is variant-expanded, so its length is columns rather than
filament slots. On the dynamic-nozzle path that makes it longer than the
slot count, and an out-of-slot index reached get_filament_config_index() and
came back as filament 0 - the clamp the bounds-checking was meant to remove.
Bound by the slot count instead; on the ordinary path the two are equal and
nothing changes. The is_extruder_used write gains the matching lower-bound
guard.
IMEXHelpers.hpp now states both halves of the rule its call sites follow.
Bound anything derived from the extruder map against the filament slot count
before using it as a filament id, not against the option about to be read.
And a miss is -1, which is a correct tool qualifier but matches no physical
head, so it cannot serve as a skip-the-primary sentinel: which head prints a
filament is answered by the filament and the map, never by a mode role,
since a primary-mode print may use any or all tools, one at a time.
The consequence is recorded there rather than left implicit. The two skip
sites skip nothing for a slot past the end of the map, so a plate with more
slots than nozzles double-writes the primary's pressure advance. It is
narrow and unreported, and a guard there would be a smaller change than
naming a head.
The header also records that RepRapFirmware sends an unqualified pressure
advance as M572 D0, naming drive 0 absolutely rather than the active tool.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on the preceding commit, plus one defect it should have
caught.
- The IDEX/IQEX pressure-advance loop fed resolve_filament_for_head()'s
result straight into enable_pressure_advance and pressure_advance. That
result is bounded by physical_extruder_map, which holds one entry per
NOZZLE, while both options are indexed per filament SLOT. On a printer
with more nozzles than the project has filaments the two spaces diverge
and get_at() clamped the overflow onto filament 0, emitting its pressure
advance on a secondary carriage. The second-layer temperature loop bounds the
same lookup, but against nozzle_temperature, which is variant-expanded and so
is not the slot count either -- it is not the precedent it looks like.
IMEXHelpers.hpp states the rule
once, and a test pins the contract that makes the bound necessary:
resolve_filament_for_head() answers in nozzle space, so a non-negative
result is not by itself safe to use as a filament id.
- The header claimed every caller renders a -1 tool qualifier as "emit
none". RepRapFirmware substitutes the historical D0 instead, deliberately
and with its own comment in GCodeWriter. Say so, rather than leaving a
contract a future author would code against.
- A cross-reference pointed at a hard-coded line number that the preceding
commit had itself shifted by nine lines. Name the function instead.
- The multi-color rejection reasons reach the user through Print::validate()
as raw English, while the returns on either side of them use L(). Wrap
them and register IMEXHelpers.cpp for extraction. They also still said
"IMEX", the internal name, so they move to IDEX/IQEX with the rest of the
user-facing strings rather than shipping the internal one to translators.
- Trim the preceding commit's comments. One block explained the same
clamping hazard six times; the canonical explanation now lives in
IMEXHelpers.hpp and the call sites point at it. The mode grid carried
twelve lines of commentary and no code, most of it archaeology already in
the commit message, and one claim about the modes editor that was not
true. The ArrangeJob threading note stays: it documents an invariant that
cannot be recovered from the code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on the IDEX/IQEX parallel printing code, all in paths the
feature owns.
- physical_extruder_map lookups used ConfigOptionVector::get_at(), which
clamps an out-of-range index to values.front() rather than reporting a
miss. The map holds one entry per nozzle while filament ids index slots,
and nothing caps the slot count at the nozzle count, so a project authored
with more filaments than the printer has extruders silently addressed the
primary's head: pressure advance pinned to the wrong carriage, and
skip-primary loops suppressing whichever head sat at pem[0]. Bounds-check
at all four sites and treat the miss as "no mapping" (-1). Covered by a new
imex_pem_tool_for test; the header note now warns against get_at here.
- IMEXFilamentPickerPopover leaked a top-level window per ghost click:
wxPopupTransientWindow::Dismiss() only hides, and never reaches OnDismiss().
Destroy from an OnDismiss() override and dismiss the picker through
DismissAndNotify(), which is the path a successful pick takes.
- ArrangeJob read PartPlate's IMEX zone cache from the worker thread, where
a cache miss rebuilds GLModel members with no GL context current while the
GUI thread may be painting them. Snapshot the zones in prepare(), on the
main thread, already converted to plate-local coordinates.
- The mode grid anchored its row window to the Primary's gantry row. A window
as tall as the grid can only start at row 0, so this drew tiles for tools
that do not exist and hid real ones. Render the whole grid instead; a
Primary outside it is a data problem the zone layout already reports.
- Build the mode tooltip from one format string rather than two catalog
fragments concatenated around a runtime value, so translators can move the
mode name within the sentence, and register IMEXModesCtrl.cpp for string
extraction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: bounds-check the toolchange flush-volume and HRC per-filament lookups
GCode::set_extruder's toolchange flush-volume lookup and
GCodeProcessor::update_slice_warnings's HRC check index per-filament and
per-extruder arrays (flush_volumes_matrix, the filament map, the nozzle list)
by filament/extruder id. When a config leaves one of those arrays shorter than
the filament count (partial or legacy multi-extruder projects, minimal
configs), the reads run off the end: silent on a normal STL, a hard abort under
_GLIBCXX_ASSERTIONS.
Route both reads through bounds checks: the flush lookup falls back to no flush,
matching the existing unknown-old-filament branch beside it, and the HRC check
skips an unmapped filament, mirroring the required_nozzle_HRC guard on the line
above. When the arrays are sized to the filament count the values are unchanged,
so correctly-specified configs are unaffected.
* ci: retrigger checks
* fix: guard H2C per-filament array reads against short config arrays
The H2C tool-ordering, wipe-tower, and g-code export paths index per-filament
config arrays by filament/tool id. A config with fewer entries than the filament
count (partial or legacy projects, minimal test configs) makes these reads run
past the end of the vector: silent under a normal STL, but UB that aborts under
the flatpak build's bounds-checked STL (_GLIBCXX_ASSERTIONS).
Route the reads through the existing clamping accessors (get_at,
get_filament_category, is_in_same_extruder) and add a small clamp helper for
filament_change_length. The guards are no-ops when the arrays are sized to the
filament count, so correctly specified configs are unaffected.
* fix: size the grouping context's filament_info to the filament count
build_filament_group_context built model_info.filament_info by walking
filament_type, so a config whose filament_type is shorter than the filament
count produced a short vector. FilamentGroup indexes filament_info by filament
id, so clamping the individual reads only moved the out-of-bounds access
downstream. Loop to filament_nums and read all three fields through get_at,
and drop filament_ids entries past the filament count, since the grouping code
pairs filament_ids and filament_info by position.
Adds a regression test with four filaments and one-entry filament_type /
filament_is_support. Without the fix it throws bad_alloc from copying a garbage
std::string read past the end.
* fix: guard the carousel nozzle-change length reads too
The carousel branch added in b90ac13d86/b0dddb4648 reads
m_filaments_change_length by tool id without a bounds check, the same
pattern this branch already routed through filament_change_length_at
a few lines above in both plan_toolchange and plan_tower_new.
* fix: guard WipeTower per-filament array reads against short config arrays
The BambuStudio WipeTower sync reintroduced raw per-filament array
indexing that reads out of bounds when a config leaves an array shorter
than the filament count: m_physical_extruder_map in format_line_M104/M109
(indexed even when empty), and m_filament_categories in get_wall_skip_points
and get_wall_filament_for_all_layer. Silent on a normal STL, a hard abort
under the bounds-checked STL the Flatpak build uses.
Bounds-check the physical extruder map before indexing (omitting the T
token, as the existing -1 path already does), and route the two raw
m_filament_categories reads through the clamping get_filament_category()
accessor the surrounding code already uses. No change for correctly-sized
configs.
BeltGCode is only created for belt printers, so its hooks no longer re-check belt_printer, and the BBL-machine flag is set once on whichever writer survives init_belt_writer instead of on one about to be discarded.
Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange.
The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged.
Merge origin/main (00429da739) into belt-printer.
Conflicts resolved:
- src/CMakeLists.txt: keep both wxInspector workarounds.
- GCodeProcessor.cpp: keep the belt compare_pos / z_for_height lines.
- PrintObjectSlice.cpp: the belt bbox-Z guard also covers main's
printable_region_ids bookkeeping.
- TreeSupport.cpp: the belt-floor check runs before main's PendingNode
queueing.
- Tab.hpp: keep the belt fields, drop the removed upload description
fields.
- tests/libslic3r/CMakeLists.txt: keep both test files.
Also included:
- eSUN PLA belt presets declare their own filament_id (OFkrxQC4) and
scripts/filament_id_snapshot.json is regenerated, as main's filament_id
check requires.
- Custom.json version bumped to 02.04.00.05 so the belt entries reach
existing installs.
- Fix the ambiguous WithinRel call in the belt apron width test, which
otherwise breaks the fff_print build.
Fixes the report in #12998 (comment 5465250754): first-layer speed and the
slow_down_layers ramp were ignored on a belt printer. The report reads as a
per-object problem, but neither applied to *any* object -- the reporter's first
part slowed down because slow_down_for_layer_cooling was on, which is
CoolingBuffer's time-per-layer mechanism, not initial_layer_speed.
FirstLayerPlane decides first-layer-ness by perpendicular distance to a plane it
derives by composing gcode_remap_* with compute_machine_z_affine(). The plane is
therefore a function of how G-code is *addressed*, not of where the belt is:
change the output axis convention and the plane moves. On MCBELT-TYPE2 the
first layer measured 86.2 mm from the plane and got effective index 431, far
past any slow_down_layers ramp.
on_first_layer(point) and effective_layer_index_for_point() now measure height
above the belt surface, using the belt description already carried in
SlicingParameters -- belt_floor_shear_factor / belt_floor_from_axis /
belt_floor_z_shift -- the same description the support generator uses. That is a
property of how the object was sliced, so no remap or back-transform can perturb
it.
Deliberately not via BeltFloorContext: its init() folds in
belt_support_floor_offset, a support-generator diagnostic, and letting that
option steer the model's first-layer speed band would be a surprising coupling
(a negative value would switch the slowdown off outright).
Preserving the existing first-layer-plane settings:
* first_layer_plane XY/YZ/XZ keeps the FirstLayerPlane evaluator, as those are
explicit opt-outs.
* A non-zero first_layer_plane_offset also keeps it. The offset is a machine-Z
shift that FirstLayerPlane converts into a perpendicular distance in the
slicing frame; this evaluator measures along slicing Z, so there is no
faithful translation. Deferring to the evaluator that implements the setting
beats silently ignoring it.
* The two thresholds stay separate, exactly as FirstLayerPlane keeps them:
the first-layer boolean tests initial_layer_print_height, while the
effective layer index counts bands of first_layer_plane_thickness.
Brim and coincident apron bands are emitted before m_layer is switched to their
object -- for an apron band there is no Layer at all -- so both paths publish the
belt-floor owner explicitly. Without that a brim's classification would borrow
whichever object was visited previously, making it depend on plate order.
Note that first-layer-ness drives more than speed: extrusion acceleration, jerk,
the first-layer flow ratio and eligibility for overhang speed/fan analysis all
read it, so all of them are corrected on belt printers by this change.
Classification still samples only each path's first point, as it did before.
Non-belt is unaffected by construction: belt_height_above_floor() returns false
when the belt floor is inactive and both call sites fall back to the previous
path. FirstLayerPlane stays in place for its other modes and for CoolingBuffer,
whose machine-coordinate probe is a separate outstanding bug.
Measured, MCTEST4 on MCBELT-TYPE2 (initial_layer_speed=5, slow_down_layers=40):
15 distinct feedrates with no gradient and F300 absent, becomes 70 including the
full ramp 300(5) 382(6) 465(8) 630(10) 795(13) ... Two bare cubes on a belt:
0 slow extrusions becomes 2378 across Z 32.36..95.18. The same two cubes on a
Cartesian printer keep their slow extrusions confined to Z 0.20..2.00.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
BeltGCodeWriter subclassed GCodeWriter and overrode seven methods, five of them
by copying the base body and changing the transform. The base writer already
carried an axis remap and already branched at each of its seven
coordinate-emission decisions; the subclass did the same branching with a
different transform, and the two copies had begun to drift.
Replace the inheritance with a strategy object owned by GCodeWriter:
CartesianKinematics to_machine = the existing apply_axis_remap; today's base
behaviour, moved rather than changed.
BeltKinematics to_machine = MachineFrameTransform o axis_remap o
BeltBackTransform, plus a world_coordinates variant for
the PA calibration generators.
New: src/libslic3r/GCode/MachineKinematics.{hpp,cpp}, GCode/BeltKinematics.{hpp,cpp}
Deleted: src/libslic3r/BeltGCodeWriter.{hpp,cpp} (341 lines)
Points worth a reviewer's attention:
* The predicate is must_emit_all_axes(), not couples_axes(). The base returns
true for any non-identity remap, including pure permutations that do not
physically couple axes, so the question is "must every axis word be
emitted", not a statement about kinematics.
* Every per-site word-omission branch is preserved. The base deliberately
emits X/Y only, or Z only, or drops Z when its quantised value is unchanged.
The strategy changes which transform applies, never whether words are
omitted.
* set_kinematics() replays the configured remap and build volume onto a newly
installed strategy, because BeltGCode::init_belt_writer runs before
GCode.cpp calls set_axis_remap/set_build_volume_max.
* uses_pointwise_travel_speed() preserves a pre-existing divergence rather
than introducing one: the base travel_to_xyz emits the raw configured travel
speed in its final branch, ignoring the first-layer value computed at the
top, whereas the belt path used the first-layer-aware value throughout. Both
are kept. Unifying them changes feedrates and belongs in its own change.
* The [BELT-DEBUG] block is deleted; it rate-limited itself with a
function-local static thread_local in the hot emission path, and this is the
commit that would otherwise have moved it into shared code.
This commit is intended to preserve existing export output. That is reviewed by
construction -- each emission site keeps its own omission branch and each policy
divergence is preserved -- and is NOT verified against a G-code diff corpus.
Building that corpus is the outstanding work here.
Two API-equivalence exceptions, neither reachable by any caller today:
* Belt kinematics with no plane pointer installed, m_is_first_layer true,
initial and normal travel speeds differing, travel_to_xyz() reaching its
final branch: the old belt writer selected the initial-layer speed, the new
writer selects the normal travel speed. The pending-lift and XY-only
branches keep their previous selection.
* Belt kinematics installed without set_force_normal_lift(true) and a
non-normal lift requested: the old belt writer forced a normal lift, the new
writer can take the slope branch.
The PA-pattern generator reaches the writer through explicit travel_to_z() /
travel_to_xy(), not travel_to_xyz() or the lazy/eager lift paths, and normal
belt export installs both the plane and the forced-normal-lift policy, so
neither exception changes output produced today. They are recorded because a
future caller could reach them.
tests/fff_print/test_gcodewriter.cpp was also not compiling before this branch:
it called writer.to_machine_coords(), a method that existed only on
BeltGCodeWriter. It never surfaced because the build targets OrcaSlicer, not
all, and BUILD_TESTS defaults to OFF, so that translation unit was outside every
compile path. Fixed here; the existing 30-degree coordinate assertions are kept
verbatim as the best available regression net.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
Five sites carried a hardcoded fallback for imex_tools_per_gantry that had
to match the value registered in print_config_def, with nothing enforcing
the agreement, and several explanatory comments miscounted the sites they
described or cited stale line numbers.
Add imex_cfg_int/_float/_bool/_enum<T> to IMEXHelpers, which return the
value registered for the key when it is absent from the config, so the
registration is the single source and there is nothing left to keep in
sync. Route every read of the IMEX geometry keys through them: 32 call
sites across IMEXZones, PartPlate, GCodeViewer and Tab. The only direct
lookup left is the bail in PartPlate::imex_multicolor_block_reason, which
must not default because it reports a routing conflict and a defaulted
grid would produce a false warning.
imex_cfg_enum uses dynamic_cast on both halves rather than the type()
comparison the others use: every ConfigOptionEnum<T> reports coEnum, so a
type() check cannot tell one enum type from another and would cast a
ConfigOptionEnum<OtherEnum> to the requested T. The ConfigOptionPercent :
ConfigOptionFloat inheritance that rules dynamic_cast out for the float
accessor has no analogue for enums.
Correct the comments that prompted this: the cache-key input list in
PartPlate named five inputs for a nine-part key, the ImexMarkerKey note
in GCodeViewer called imex_tool_layout an input only the preview reads
when the plate keys it too, and four file:line citations pointed at the
wrong lines. Values are unchanged at every converted site; cache key
strings keep their existing representation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No IMEX code upstream, so nothing in this merge touches the feature. All 21 overlapping
files auto-resolved; verified every upstream addition is present in the merged tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes review comment 10.
`461c69c83e` settled this in April — IMEX internally, IDEX/IQEX as the user-facing label — but the
UI strings were never converted. Every translated string naming the feature now reads IDEX/IQEX:
41 occurrences across the printer and process option labels and tooltips, the modes editor, the
plate mode indicator, the pre-slice warnings, the placement refusals and the slicing errors. The
reviewer listed eight; the rest were in the same class.
Nothing else moves. The config keys keep the `imex_` spelling — `is_imex`, `imex_mode_names`,
`imex_parallel_mode` and the rest are on-disk format in existing printer presets and 3MF projects,
so renaming them would break every profile and project already saved. C++ identifiers, filenames,
comments and test names keep IMEX as well: it stays the internal name of the subsystem, which is
what covers the topology space (one gantry with 2-4 tools, 2x1 and 2x2 grids) that neither acronym
names on its own. Where a tooltip quotes a key, the key spelling is preserved and only the feature
word around it changed.
The `is_imex` tooltip is reworded rather than substituted: it already named the hardware families
parenthetically, so a literal replacement would have said IDEX/IQEX twice in one sentence.
No translation impact — no IMEX string had reached OrcaSlicer.pot or any catalogue, so there is
nothing to migrate. One test asserted on the old error text and now matches the new one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes review comment 2, part of 18, and one non-IMEX regression the review did
not cover.
An imex_parallel_mode naming no entry in imex_mode_names still entered the
parallel branches. get_imex_active_tools() returned empty and the else was
skipped, so no head received its 1st-to-2nd layer temperature transition, and
imex_suppresses_bare_toolchange() still dropped the initial T<n> on the
expectation that a mode script would select the tool. validate() did not catch it
because its guard is declared_primary >= 0 and an unresolved mode yields -1. You
reach it by renaming a mode after a plate is set to it, or by opening a 3MF whose
printer preset names its modes differently. m_imex_parallel_mode is no longer
assigned before the lookup; a non-Primary name that matches no row now warns and
re-resolves against the Primary row, which is the fallback the PR description
already claimed. A mode that resolves to an empty tool roster takes the same path,
since the emitted G-code is wrong in the same way.
Warned rather than blocked: opening someone else's 3MF is a legitimate way to get
here and the Primary reading prints correctly, so refusing to slice would turn a
recoverable situation into a dead end. Silent was not an option either, because
the plate keeps showing the stale mode name while drawing no zones.
Separately, the 14 imex config keys all register non-nil defaults, so
append_full_config was emitting "; imex_* = <default>" into every exported
G-code, including on single-nozzle printers with nothing to do with the feature.
They are banned from the dump, matching the treatment already given to the
fast-purge, extruder-change and timelapse keys, so the config block is
byte-identical to the pre-IMEX baseline for the whole shipping fleet. Nothing
reads them back: GCodeProcessor has no imex reference, and the two per-plate keys
round-trip through the 3MF's model_settings.config on an independent path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
append_tcr2 scanned the tower's G-code for a "preheat T<n>" comment and rewrote
its S value to the interface temperature. Nothing it could match was ever there.
That comment has exactly one producer, GCodeProcessor's backtrace injector, and
that runs inside run_post_process() -- a pass over the finished, exported file.
append_tcr2 runs while the file is still being generated, so the text it looked
for did not exist yet and could not.
The loop therefore walked every line, matched none, and swapped the string for
an identical copy. Delete it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
M104/M109 address a heater, but every caller of the instance
GCodeWriter::set_temperature overload addresses filaments by logical id, so on a
printer whose physical_extruder_map is not the identity the emitted T named the
wrong head -- or, where the logical id exceeds the head count, no head at all.
With a map of 0,0,0,0,1,2,3 a toolchange to filament 5 emitted "M109 S265 T4"
and "M104 S190 T4 ;cooldown" while the head it meant was T1.
Upstream already treats these commands as physical: the preheat it injects in
GCodeProcessor maps through the same map before emitting, and BBS's own wipe
tower does likewise. Emitting logical is the half that never got the memo.
That mismatch also disabled the cooldown suppression beside the preheat, which
compares the line's T against pem[tool_number] and so never matched a logical
one -- 171 cooldowns survived in a two-head print where none should have. Worse,
it could match the wrong line: a cooldown for filament 1 emitted T1, and a
toolchange to filament 5 gives pem[4] == 1, so a legitimate cooldown for head 0
was deleted because the incoming head happened to be numbered 1.
Translate once, in the instance overload every logical-space caller passes
through. The static overload is already physical-in and is left alone.
Gated on is_imex. physical_extruder_map carries two readings in this tree: the
BBS paths index it by extruder id, the IMEX paths by filament id, and the two
coincide only when the filament and nozzle counts match. Mapping unconditionally
would impose the IMEX reading on profiles that mean the other one --
fdm_bbl_3dp_002_common ships a non-identity [1,0], spared today only because
single_extruder_multi_material suppresses the T qualifier entirely.
The wipe tower's interface-temperature pass has to move with it. It strips the
M109 that post_toolchange emits by searching for that filament's tool index, so
it now searches for the mapped one; left alone it would have stopped matching,
and the surviving blocking M109 would have silently defeated the interface
temperature. Its sibling pass reads WipeTower2 output, which emits no T at all,
and is deliberately unchanged.
The bare T<n> toolchange stays logical -- it selects an AFC lane, not a heater.
Test slices two objects across a head boundary, the only case that reaches this
emission: the same-physical short-circuit in set_extruder suppresses the
cooldown entirely for lane swaps within one head. It scans every M104/M109
rather than matching fixed strings, so it catches any unmapped emission and not
just the two sites changed here. With the mapping neutered it reports 101
offending lines; with it in place, none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>