imex_wipe_tower_hull() took the axis-aligned box estimate_wipe_tower_polygon()
returns and compared it to the IMEX collision zones as-is. The real tower is
rotated about its anchor corner before placement -- first_layer_wipe_tower_corners
builds the box in tower-local coordinates, rotates about the local origin, then
translates by wipe_tower_x/y -- so a rotated tower's true footprint fell outside
the hull and the placement check passed on a tower that intrudes into a
carriage's reserved space.
The gap was documented in place and previously harmless, because
wipe_tower_rotation_angle was read from the project config after it had been
moved to the print preset, so the setting did nothing. Upstream repaired that
read ("Fix prime tower rotation angle setting not working"), which makes the
angle reachable and the stale hull wrong.
Rotation is applied to the hull alone. estimate_wipe_tower_polygon() still
returns an unrotated box and still leaves ArrangePolygon::rotation unset, since
the arranger consumes that field separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SceneRaycasterItem keeps the MeshRaycaster it was registered with as a raw
pointer, while PickingModel::reset() destroys it through a unique_ptr. Rebuilding
an icon therefore invalidates any registration still referring to it.
refresh_imex_icon(), reached only from Plater::on_config_change when is_imex or
the bed shape changes, rebuilt the IMEX mode icon without touching the
SceneRaycaster. The stale entry survived, and the next picking pass dereferenced
freed memory inside AABBMesh::intersect_ray.
Swap that one registration in place, matching how calc_vertex_for_plate_name()
handles the name-edit icon. Only the mode icon is registered for picking -- the
warning badge beside it is a plain GLModel -- so a single id is affected and the
blanket remove/re-register reload_scene() performs is not needed here.
Crashes were delayed and looked unrelated to the config change, because bed
raycasters are only tested when the camera looks down (SceneRaycaster::hit). The
reported dump landed on File > New Project, whose render ran a picking pass with
a registration that had gone stale earlier.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up. Removing pem from the ghost cache key also removed its
accidental role as the key's only printer-identity signal: the resolved
active-tools string (roster + primary) and imex_firmware_managed_zones both
shape the baked zone/ghost set but were never keyed directly, so a printer
swap between presets with matching mode names and topology could leave a
stale ghost set. Key all three in build_imex_cache_key, which also hardens
the zone cache against the same pre-existing gap.
Also from review: the tooltip swatch reuses the pem/map its label already
hoisted (one resolution, not two); the bake constructs ghosts with no color
at all, making update_imex_ghost_colors the sole color author; the headless
!m_plater guard is documented as the wxGetApp sentinel it is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the live ghost-color fix, addressing review findings: with the
render loop restamping ghost colors every frame, the bake-time resolution in
calc_imex_ghosts was dead code (its RGB was displayed for zero frames), and
the pem + head-filament-map entries in the ghost cache key had become
color-only inputs that forced a full mesh re-bake — including a visible
hitch on every ghost-picker selection — for what is now a pure recolor.
- calc_imex_ghosts bakes an alpha-only placeholder; IMEX_GHOST_ALPHA is
hoisted to file scope as the single opacity authority (no more reading
alpha back out of the field the restamp overwrites).
- build_imex_ghost_cache_key drops pem and the head-filament map; the
forced invalidations in set/reset_imex_head_filament_map go with them.
Picker selections now recolor live with no rebuild.
- The restamp moves into PartPlate::update_imex_ghost_colors(), beside the
transform refresh, so PartPlate owns its volumes' colors and the canvas
calls one hook. Plate-level inputs (pem, override map) are hoisted once
per frame via a new get_imex_head_filament_color overload that the
single-head form delegates to, keeping tooltip parity by construction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Labels, tooltips, menu items and dialog text mixed "IDEX/IQEX" with "IMEX"
for the same feature. IMEX is now the user-facing name throughout: IDEX and
IQEX are hardware categories, IMEX is the feature spanning them. The
hardware terms remain only where they help a user tell whether the feature
applies to their printer.
Comments and log messages keep IDEX/IQEX, where the specific carriage
topology is the more precise term.
Also corrects the imex_parallel_mode tooltip, which pointed at a
"Printer -> IDEX/IQEX tab" that does not exist; the options live under
Printer -> Multimaterial -> IMEX Configuration.
None of these strings appear in any .po or the .pot, so no translation is
affected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IMEX placement validation only walked model instances. The prime tower is
not a ModelObject, so it could sit in a secondary zone or a carriage
collision strip and slice with no warning -- on mirror mode, a carriage
crash. Span (paired-gantry multicolor) is what made towers reachable in
parallel modes, so this is a gap in that feature, not inherited breakage.
The check now returns a cause instead of a bool so the message can name the
offender, and the tower and per-instance paths share one predicate,
imex_hull_violates_zones(), moved to libslic3r and covered by tests.
Overlap is area-based: a hull flush against a zone boundary is legal, only
a crossing violates. The tests pin that in both directions, since switching
to a touch-based test would silently block placements that work today.
The tower footprint comes from the same estimate the scene draws, so
validation matches what the user sees and drags. Three config reads there
are load-bearing in non-obvious ways and are commented at the point of use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
estimate_wipe_tower_polygon reserved the arrange footprint and clamped the
tower X position with the raw prime_tower_width, under-reserving space
whenever the rib wall squares the tower to a different width.
Catches the fork up from 449a4cf9fc (2026-06-28) to d6cb667b89 (2026-07-24).
Upstream touched 2326 files; 17 of them overlap the 164 this branch touches.
16 of the 17 auto-merged, including all three CMakeLists.txt, the build_all.yml
CI workflow, and every GUI file. The CAD core never conflicts: CadDocument,
SketchEngine, SketchSolver, McpControl and test_caddocument are files this fork
adds, so upstream does not touch them.
The one conflict, tests/libslic3r/test_3mf.cpp, was purely additive in all three
hunks and is resolved as a union: our test pinning that store_bbs_3mf embeds the
CAD recipe as Metadata/SnapOrca_cad.bin, upstream's multi-nozzle plate-metadata
round-trip tests, and both sets of includes. All three were verified present
after resolution rather than assumed.
NOT BUILD-VERIFIED, for a reason that predates this merge and is not caused by
it: this fork cannot be configured on nativedev at all. Its CMakeLists has
required Eigen3 5.0.1 since before the merge (line 592 pre-merge), while the
only deps image on the machine is snaporca-deps, built for snaporca's
find_package(Eigen3 3.3). CMake fails at configure, so nothing compiles.
That means this fork's Catch2 suite has never run. Every "suite green" figure
recorded for M1-M8 was snaporca's suite; the ports were verified by patch-apply
plus the CAD sources being byte-identical to snaporca's. Building an orca_cad
deps image with Eigen 5.0.1 is what would finally close that gap.
Pre-merge state is preserved at branch cad-mainline-pre-upstream-2026-07-25
(30d54f0074).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The big upstream merge replaced the old per-idle unconditional plate-selector toolbar
refresh with a dirty flag (Plater::mark_plate_toolbar_image_dirty), which the
geometry-change sites already set. PartPlate::update_slice_result_valid_state flips a
plate's slice-ready / IMEX blocked-plate ("naughty plate") state without any geometry
change and did not set the flag, so after the merge nothing repaints that plate's
thumbnail or warning badge — the old per-idle refresh used to mask it. Mark the toolbar
image dirty when the state actually changes, matching the established pattern.
Cosmetic; no effect on slicing. Surfaced by the merge review of 58b4a68a10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Follow-up to the cross-gantry mirror axis change, from an adversarial review of it.
GCodeViewer builds its own copy of the zone grid to place the sequential-preview
carriage markers, and it must reproduce PartPlate::calc_imex_zones exactly or the
markers drift away from the ghosts they are meant to track. It did not, in two ways,
because sizing the grid and placing a cell answer different questions:
- Sizing: calc_imex_zones counts every Copy/Mirror tool's OWN column, including the
non-representatives of an aggregated (Span) gantry -- they still donate a column.
GCodeViewer only ever saw the representative, so on an aggregated gantry it could
count fewer columns than the plate and lay its markers out against wider strips.
- Placement: calc_imex_zones PINS an aggregated cell to the primary's column, because
that row-strip spans the full bed and has no column of its own. GCodeViewer used the
representative's own column, which put the marker a strip away from the ghost
whenever the representative was not column-paired with the primary.
Track the two sets separately: grid_tool_ids sizes the grid from own columns, eff_col_of
pins only aggregated tools when placing. Out-of-grid tool indices are deliberately left
unfiltered -- calc_imex_zones drops them while calc_imex_ghosts keeps them, so no policy
here can agree with both, and a comment says so rather than pretending otherwise.
Also:
- The mirror-axis rule lived in three copies (two PartPlate lambdas plus an inline
re-derivation here). Hoist it to imex_mirror_axis_for() so the ghosts and the markers
cannot drift apart, and unit-test it, including degenerate tools_per_gantry.
- Drop imex_head_transform's mirror_axis default. A defaulted axis silently hands a
forgetful caller the X reflection, which is wrong for every cross-gantry tool and
fails silently -- exactly how a stale test kept certifying the old rule.
- Replace that stale test: it asserted a diagonal mirror "flips X only", the rule this
work overturned, and stayed green only because of the default.
- A secondary sharing the primary's gantry now takes the primary's Y box facing. The
box shows the side a tool could be hit from, and two tools on one beam can only be
hit by the same other gantry. No-op on the rear-* layouts, where the hardcoded value
already matched; on front-* layouts it pointed the box away from the only tools that
could reach it.
- Correct two comments that described the aggregated X-frame substitution as a
reflection plane. It is not one: it exists to zero gantry_offset.x, and removing it
would push aggregated ghosts a column off their strip.
Verified: 385/385 tests; CLI slice of the IMEX regression project is byte-identical to
the previous commit's G-code apart from the timestamp, confirming this is
visualization-only and cannot affect sliced output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Mirror tool reflects across the boundary it shares with the primary's zone, and
which boundary that is depends on where the tool sits:
- Same gantry: the tools are side by side along X, so the shared boundary is
vertical and the reflection negates X. This is what single-gantry IDEX does,
and it was the only case the code modelled.
- Different gantry: the zones are stacked along Y (front strip vs rear strip),
so the shared boundary is horizontal and the reflection negates Y. The part
that comes off gantry 1 is a Y-reflection of the tool directly behind it.
imex_head_transform() hardcoded diag(-1, 1, 1) for every mirror, as its own TODO
acknowledged. Lift the axis to a caller-supplied ImexMirrorAxis; PartPlate picks it
from the tool's gantry row. Both reflections keep det = -1, so a mirrored part stays
a true mirror image rather than a 180-degree rotation, which would print the
primary's part merely turned around.
The correct axis removes two workarounds. Both ghost paths special-cased aggregated
mirrors to "drop the X reflection, translate 1:1 and bake the flip into the mesh"
because reflecting X pushed the ghost off-bed as the primary was dragged. With a Y
reflection the X translation is already zero for aggregated tools, so that falls out
for free and the special cases are deleted.
Preview markers follow the same rule, which also fixes two placement bugs:
- Mirrors reflected across a Copy tool's zone edge, falling back to the primary's
column when a row had no Copy. In iq-mirror (0:P,1:C,2:M,3:M) the front row has
no Copy, so t2 and t3 both fell back and computed the identical X — both drawn
on top of each other in t3's zone. A mirror now reflects within its own zone.
- The toolhead footprint box flipped to the far side of the nozzle for any mirror
right of the primary. That only holds for an X-axis mirror, which reverses the
carriage's orientation; a cross-gantry mirror keeps the X orientation of the
tool behind it, so its box stays on the same side.
Tests cover the cross-gantry and diagonal cases, that the axis is caller-supplied
rather than inferred from the offset vector, and that both axes are reflections
(det = -1) rather than rotations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
A print sliced for a nozzle that sits in the hotend rack (but is not
mounted) was blocked by the send dialog's mounted-nozzle diameter check,
even though the printer fetches the required nozzle itself (#14685).
Consolidate the three mounted-nozzle gates (_is_nozzle_data_valid,
is_nozzle_type_match, _is_same_nozzle_diameters) into a single
CheckErrorExtruderNozzleWithSlicing fed by s_get_slicing_extuder_nozzles,
which collects the plate's per-extruder nozzle requirements (hybrid
extruders contribute one entry per used sub-nozzle flow). The rack
extruder validates against its whole inventory (mounted + rack) with
guidance to calibrate the rack, refresh nozzle info, or re-slice, and
blocks when toolhead + rack are full (no free slot to stow a nozzle).
Other extruders keep the validity/flow/diameter checks against the
mounted nozzle.
Also add CheckErrorRackStatus, which holds Send while the printer is
still reading the rack hotend information, and judge material hardness
for the rack extruder per dispatch-mapped nozzle as a non-blocking
caution (mounted nozzles keep the blocking gate).
Deleting an object's instance leaves a stale (obj_id, instance_id) pair in
PartPlate::obj_to_instance_set until it is pruned. object_list_changed() runs
during the delete path and calls has_printable_instances(), which guarded only
obj_id and then indexed object->instances[instance_id] on the now-shorter
vector, dereferencing a garbage ModelInstance* and crashing with SIGSEGV. The
reported fault address (0x12a) matches a member read on that bad pointer.
Reuse the existing valid_instance() helper, which bounds-checks both obj_id and
instance_id, at every obj_to_instance_set scan that was missing the instance-id
check: has_printable_instances(), printable_instance_size(),
is_all_instances_unprintable(), get_extruders_under_cli(),
duplicate_all_instance() and set_pos_and_size() (the last had no bounds check at
all). valid_instance() is made const so the const CLI scan can call it.
Fixes#14159
- FilamentMapDialog's manual page understands volume types: a mixed
(Hybrid) extruder shows separate Standard / High Flow drop zones
with live sub-nozzle counts, a validation timer with an inline
error + "set nozzle count" suggestion, and composes a per-filament
volume map on OK (persisted to the plate or globally)
- switching an extruder's Flow type rewrites the affected plate map
entries; plate maps stay sized across filament add/delete/count
changes (values keep their filament, no index shift)
- CLI: manual mapping on multi-nozzle printers synthesizes the volume
map from extruder flow types when absent; nozzle-manual mode
requires explicit maps; computed maps land on the plate so exported
projects carry filament_volume_maps
- filament_nozzle_map joins the project options (selection seeding +
filament-count resizing)
- pot entries for the new dialog strings
All 19 reference fixtures byte-identical; slicing an exported Hybrid
project reproduces its g-code byte-for-byte with the volume map
round-tripped through model_settings.config.
- 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.
Multi-nozzle sync widget, AMS rack-nozzle mapping popup, calibration rework, send-dialog nozzle mapping and extruder-count UI. Includes the fix to persist the AMS sync badge on filament cards (H2C/A2L and direct-sync printers).
Catches the iXex/IDEX parallel-printing branch up to upstream main
(102 commits). Two content conflicts resolved:
- src/libslic3r/Preset.cpp: s_Preset_printer_options — kept upstream's
new "use_3mf" key and our iMEX printer-capability/mode keys.
- tests/fff_print/test_gcodewriter.cpp: upstream revived the disabled
suite (#14196), dropping the obsolete [.]-tagged lift() test and its
config_lift_unlift.ini; kept their set_speed + z_hop tests and appended
our 10 per-firmware set_pressure_advance/set_temperature scenarios.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching to a printer with fewer filaments (e.g. H2D -> X2D) threw
std::out_of_range in check_filament_printable. Clear stale per-volume
extruder config on count shrink and bound-check filament indices at the
read sites.
124 upstream commits including CrealityPrint integration (added include
in Plater.cpp alongside the existing IMEXHelpers include), profile fixes
and version bumps (#14084, #14085, Polymaker), CI artifact publishing,
test refactors (arachne walls test added; test_3mf/test_config/
test_gcodewriter content moved/removed upstream — IMEX test file
preserved as it is feedback-only), translations.
* Support 'Default' filament option (index 0)
Treat filament index 0 as the new "Default" (use active object/part filament) instead of using 1. Update config defaults and tooltips for wall/sparse/solid infill filament options (min/default -> 0, tooltip explains "Default"). Adjust normalization and propagation logic to respect explicit feature overrides and only apply base extruder when feature values are zero; only copy sparse->solid infill when sparse > 0. Introduce FeatureFilamentOverrideMask and clamp_feature_filament_to_valid to resolve and clamp feature filaments. Update UI lists and selection behavior to expose a "Default" entry and handle zero-based indices in PartPlate and Plater.
* enable_filament_for_features option
Co-Authored-By: LixNix <105106115+lixnix@users.noreply.github.com>
* \n
* Allow wipe_tower_filament to equal nozzle count
Relax the assertion in Print::extruders to permit wipe_tower_filament == config().nozzle_diameter.size(). The configuration value is 1-based and the code subtracts 1 when pushing the extruder index, so equality should be valid and selecting the last nozzle should not trigger an assertion.
* Revert "Allow wipe_tower_filament to equal nozzle count"
This reverts commit 2c976574327a8bcdc74a1b296bf1aaff7752a94e.
* Revert "enable_filament_for_features option"
This reverts commit 01c13baeddb8e26793f752deab788ee4d086975b.
* Migrate legacy feature filament defaults
Add migration logic to convert legacy feature filament selections from 1 to 0 for older 3mf files. Introduces a local migrate_legacy_feature_filament_defaults lambda in src/OrcaSlicer.cpp and src/slic3r/GUI/Plater.cpp that scans keys (wall_filament, sparse_infill_filament, solid_infill_filament, support_filament, support_interface_filament) on configs/objects/volumes, updates values, counts conversions and logs the result. Also adds a Semver check for "2.4.0-dev" in OrcaSlicer to trigger the migration for files older than that version. This preserves expected default filament selections when loading older project files.
* Update OrcaSlicer.cpp
* Extract migration helper to ConfigMigrations
Centralize legacy feature-filament default migration by moving the duplicated lambda into ConfigMigrations::migrate_legacy_feature_filament_defaults (src/libslic3r/Config.cpp) and declaring it in Config.hpp. Update OrcaSlicer.cpp and slic3r/GUI/Plater.cpp to call the new function instead of inline lambdas. The helper converts specific feature filament keys (wall_filament, sparse_infill_filament, solid_infill_filament, support_filament, support_interface_filament) from int 1 to 0 and returns the count of conversions to avoid duplicated migration logic.
* Remove DynamicFilamentList1Based and consolidate lists
Delete the specialized DynamicFilamentList1Based struct and its global instance. Update Choice registrations to use the single dynamic_filament_list for wall, sparse_infill and solid_infill filaments, and remove the extra update call for the removed instance. This consolidates filament choice handling and removes duplicated logic in Plater.cpp.
* move it
* fix objects
* Update Config.hpp
* Update profiles
Adds the `imex_firmware_managed_zones` printer-config key (default off) for
IDEX/IQEX printers whose firmware applies its own copy/mirror offsets in
non-primary modes (e.g. RepRapFirmware IDEX duplication mode, Flashforge
Creator Pro 2/3 Pro). For these printers the slicer needs to emit a single
centered slice at bed origin and let the firmware fan toolheads out from there;
the previous slicer-managed iMEX rendering would draw a print at the primary
zone's world position (off-bed for the firmware-fan-out paradigm).
When the flag is on and the active mode is non-primary, the slicer subtracts
the primary zone's plate-local center from the gcode emission frame. The
writer offset is augmented but the gcode-processor offset stays at plate_origin
so the gcode-preview visualizer renders the centered slice at the bed center
rather than at the prepare-view zone placement. translate_to_print_space is
augmented too so first_layer_print_min/max placeholders (consumed by user
start_gcode like Felix's M118 header) reflect the centered frame.
Slice handoff lives in PartPlate::refresh_imex_slice_offset, called from both
update_slice_context (plate switch) and Plater::priv::update_background_process
(every-slice path — reslice() goes through here with switch_print=false so the
plate-switch hook alone wouldn't fire on mode toggle).
calc_imex_ghosts early-returns in firmware-managed mode: the existing
imex_head_transform math places ghosts at primary_zone_center + gantry_offset
(slicer-managed semantics), which renders off-bed when the toolpath is being
emitted in a centered frame. Proper firmware-managed ghost rendering (showing
where copies/mirrors will actually print after firmware fan-out) is deferred.
When the flag is off, all the new code paths reduce to no-ops byte-identical to
prior behavior. Layer 1 unit tests in test_imex_helpers cover every gating path
of compute_imex_slice_offset; full ctest suite passes (247/247).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`check_outside` (placement check) calls `ensure_imex_zones()` to make sure
zone geometry is current before deciding if an instance is in-bounds. In
the GUI path that's fine, but the CLI / headless 3MF-load path also reaches
this through `PartPlateList::load_from_3mf_structure -> reload_all_objects
-> add_instance -> check_outside`, and CLI mode has no GUI_App initialized.
`build_imex_cache_key` and `calc_imex_zones` both dereference
`wxGetApp().preset_bundle` — without a GUI_App, `wxGetApp()` returns memory
that segfaults on member access, killing the slicer with SIGSEGV before
any G-code is produced.
Latent since 461c69c83e (Apr 9), surfaced now that upstream's main carries
the headless regression-test CI step (#13353) that exercises CLI slicing
on every PR build.
Fix: short-circuit `ensure_imex_zones()` when `m_plater` is null (already
the GUI/CLI marker used by `calc_imex_ghosts`). Also tighten the existing
`build_imex_cache_key` null check to consult `m_plater` first as defence
in depth, so the function stays safe if reached from another headless
caller.
Verified locally against the upstream regression suite — klipper /
p1s_multicolor / toolchanger_4_color all slice cleanly within the
20% baseline tolerance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The aggregated-mirror ghost was X-flipping about the mesh's local origin,
which shifted the ghost sideways for models whose local origin sits at a
corner (calibration cubes, calicat, most STL imports anchored at the
min corner). Visible as a constant left-X offset between the primary's
position and the ghost's position.
Pivot on `mo->raw_mesh_bounding_box().center()` instead, applied through
the instance transform so rotated objects flip about the rotated bbox
center too. Same correction applied to the live-drag update path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce ImexRole::Span as a 5th tile cycle state that declares "this
tool is the multicolor partner of Primary on the same gantry." Encoded
as the `S` role suffix in `imex_mode_active_tools` (e.g. `0:P,1:S,2:M,3:M`).
Disambiguates paired-gantry mc-mirror from 4-independent-copies — both
share the same active_tools shape sans the marker.
Span drives:
- Multicolor block rule: now requires Span on primary's gantry to allow
multi-color slicing in a parallel mode. Replaces the prior "≥2 tools
on primary's gantry" check; pre-existing 4-tool multicolor configs
need T1 flipped to Span.
- Ghost aggregation: one ghost per non-primary gantry when Span is
present, using the column-paired representative. Aggregated-mirror
drag tracks primary 1:1 in X (gantries don't share an X rail) with
X-flip baked into mesh-local frame so geometry still reads as mirrored.
- Zone aggregation: one full-X row strip per non-primary gantry instead
of per-tool quadrants.
- UI: 5th button in IMEXModesCtrl. Cycle Off→P→C→M→S→Off, only offered
on multi-gantry printers and only on tiles sharing primary's gantry row.
Single source of pairing truth: group_imex_active_tools_by_gantry in
IMEXHelpers, consumed by ghost factory and zone calculator.
Also fixes the carriage collision strip's X-boundary check, which lacked
the row constraint its Y-boundary counterpart already had — paired-gantry
mc-mirror was drawing a spurious right-edge strip from T3 sitting
diagonally from primary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The mode-type tag (imex_mode_types config + imex_mode_type_for helper +
Split sentinel) was added in 0bb1cef as scaffolding for the Split rendering
work that landed in 4370cca and then got reverted in d9be71b. With Phase 2-4
gone, this scaffolding is now unused dead code — and the design we settled
on instead is to leave topology entirely implicit (parsed from active_tools_str)
rather than carrying a per-mode type tag the user would otherwise have to
manage explicitly.
The multi-color slicing block + bare T<n> suppression that were the actual
substance of the safeguards work stay in place:
- imex_multicolor_block_reason still allows multi-color exactly when 2+ tools
are active on the primary's gantry — Felix's hypothetical IQEX paired-gantry
case works through this path, no new mode type required.
- Slicer-side: bare T<n> stays suppressed at print-start in IMEX parallel modes;
mid-print T<n> emits naturally for the legitimate IQEX 4-tool-active scenario.
Removed:
- ConfigOptionStrings imex_mode_types (PrintConfig.hpp/cpp + Preset.cpp key list)
- imex_mode_type_for helper + kImexModeType{Primary,Copy,Mirror,Split} sentinels
- mode_type parameter on imex_multicolor_block_reason and the Split short-circuit
- mode_type plumbing in Print::validate and PartPlate::has_imex_multimaterial_conflict
- Three unit tests for imex_mode_type_for + two Split-specific multicolor block tests
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>