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>
Completes the Split-type IMEX mode plumbing started in 0bb1cef. Phase 1
landed the config option, helper, validator, and plater badge updates;
this commit lands the visualization side and the editor surface so users
can actually create and use Split modes.
Zone aggregation (PartPlate::calc_imex_zones)
- When mode_type == "split" and there's row separation between primary
and secondaries, collapse copy_cells and mirror_cells per-gantry
(each cell gets primary's column) and force has_col_sep = false.
make_boxes then takes its full-X-row branch and renders one zone
covering each non-primary gantry's full Y band, instead of per-tool
quadrants.
Ghost aggregation (PartPlate::calc_imex_ghosts)
- Pre-scan the active mode's tool list to pick a canonical head per
non-primary gantry (the tool whose physical column matches primary's;
fallback to first-seen). For Split modes, the per-head emission loop
skips non-canonical tools so each non-primary gantry gets exactly one
aggregate ghost rendered at the canonical's mirrored position.
- update_imex_ghost_transforms unaffected — it iterates the already-
filtered ghost set.
Modes editor Type column (IMEXModesCtrl in Tab.cpp)
- Each non-primary mode row gains a wxChoice dropdown selecting Copy /
Mirror / Split. Primary row gets a static "Primary" label.
- Header row picks up a "Type" column header with sized spacer that
aligns with the dropdown.
- get_mode_data() now returns a fourth tuple element (types vector).
load_from_config() reads imex_mode_types via imex_mode_type_for so
legacy presets without the new option still infer types from mode
names. on_change writes imex_mode_types back to config.
- matches_config(), snapshot_row(), row_differs_from_parent(), and
reset_row_to_parent() all extended to track type alongside name/
tools/gcode — per-row reset arrows reflect type-only changes, and
reset restores the parent's type via imex_mode_type_for fallback.
- Type-choice dropdown fires notify() on change so the dirty/save flow
catches it like any other row edit.
End-to-end: switching a row to Split type in the modes editor, then
selecting that mode on a plate, allows multicolor slicing (the gantry-
pair check is bypassed in imex_multicolor_block_reason) and the bed
visualization shows a single aggregate zone + one ghost per non-primary
gantry instead of per-tool clutter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Felix14-v2 PR review feedback (https://github.com/OrcaSlicer/OrcaSlicer/pull/13086#issuecomment-4323696312)
plus the slice-time validation work that follows from his bug list.
UI polish:
- Capitalize "Primary" in display (right-click mode menu, plate tooltip).
Sentinel stays lowercase for wire compatibility.
- Pre-slice-warnings checkbox now uses Orca's ::CheckBox so it matches the
green toggle style of the rest of the IDEX/IQEX configuration page.
- DPI-scaled the IMEXModesCtrl (modes editor) sub-panels, button grid,
text wraps, gcode textarea, and the ghost-tooltip swatch (imgui.scaled).
Legend swatches sized to body-text height for visual balance.
- Primary mode tool buttons in the modes editor are now disabled (read-
only): cycling roles on the IMEX-off mode is a no-op and confusing.
- Modes editor sub-panels now explicitly inherit the app's window-default
dark colour so chromeless ScalableButtons don't render with a visible
light box around their icons on GTK dark themes.
- Per-mode-line reset arrows in the modes editor: each row gets a small
reset bitmap that snaps that row's name+tools+gcode triplet back to the
saved preset's value. matches_config() guard on the page-level reload
prevents the textbox-being-typed-into from being destroyed mid-keystroke.
- New View menu item "Show IDEX/IQEX Toolhead" — toggles the per-carriage
footprint boxes during G-code preview playback. Gated to Preview tab +
IMEX printer; backed by app_config so it persists.
Coordinated config migrations:
- imex_tool_layout and imex_viz_theme migrate from coString to coEnum
(ImexToolLayout / ImexVizTheme). Existing wire format preserved so
saved presets and 3MFs deserialize unchanged. Side-benefit: both now
pick up standard Field rendering and so finally show reset arrows.
Slice-time safeguards (the validation half):
- imex_suppresses_bare_toolchange(parallel_mode, count): suppresses the
slicer's bare T<n> at print-start in any IMEX parallel mode (the user's
imex_mode_gcode + machine_start_gcode owns tool activation there). Mid-
print T<n> emits normally — Print::validate blocks the configurations
where mid-print T<n> wouldn't make sense. Applied to both code paths
inside GCode::set_extruder (the long multi-extruder path AND the
single-extruder path that fires when multiple_extruders=false).
- imex_multicolor_block_reason(): hard-stop validator returning a user-
facing reason string when the active IMEX configuration can't physically
support multi-color. Catches IDEX (1 tool/gantry), 2-tool-active IQEX
(no within-gantry swap topology), and any MMU/AFC lane sharing among
used filaments. Wired into Print::validate as a slice blocker, and into
PartPlate::has_imex_multimaterial_conflict so the plater badge agrees
with the slice block (no more false positives where the badge warns but
the slice goes through).
- New imex_mode_types config option (parallel array to imex_mode_names)
and imex_mode_type_for() helper. Mode-type tag drives behaviour: zones,
ghosts, and validation interpret modes differently per type. Initial
types: "primary", "copy", "mirror", "split". Split modes are explicitly
designed for paired-gantry IQEX multi-color and bypass the gantry-pair
check in imex_multicolor_block_reason (MMU sharing still blocks them).
Test coverage:
- New unit tests cover imex_suppresses_bare_toolchange (4 cases),
imex_multicolor_block_reason (8 cases including IDEX, IQEX 2-/4-tool-
active, MMU sharing, missing primary, Split type), and imex_mode_type_for
(3 cases including legacy fallback). Total: 186 IMEX assertions across
71 test cases, all passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The IMEX zone cache key drives ghost rebuild and zone-overlay
invalidation. Adding a printer config option that affects zone geometry,
ghost transforms, or collision strips without extending the key produces
a silent staleness bug: the cache thinks the zones are still valid and
ghost meshes / overlays don't refresh after the new option changes.
Document what currently feeds the key and pin the precision convention
(*10 scale on float values for 0.1 mm resolution) so future contributors
know the contract and where to extend it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Across 6 files the literal "primary" string was the sentinel for "no IMEX
parallel mode active" — used for short-circuiting in serialization, ghost
visualization, zone calc, popup-menu list construction, the IMEXModesCtrl
non-deletable first row, and several layer-of-export checks. A typo in
any one would silently bypass the guard.
Define kImexPrimaryMode in IMEXHelpers.hpp with a docstring describing
what equality with it means semantically, and route every call site
through it. No behavior change.
Touched: bbs_3mf.cpp, GCode.cpp, PartPlate.cpp, Plater.cpp, Tab.cpp.
The bbs_3mf and Tab files now include IMEXHelpers.hpp; the other three
already did.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four quick-fix items surfaced by pre-PR self-review.
GCodeViewer.cpp:1607
Null-check get_curr_plate() before dereferencing. Other call sites in
the file already guard; this was the only unguarded one in the IMEX
layer-preview path. In practice m_plate_list always has a plate, but
the inconsistency is easy to fix and removes the only ungated deref.
PartPlate.cpp:build_imex_cache_key
Cache key for IMEX zone geometry truncated nozzle_clearance_x/y to int
before stringifying — a config change from 30.0 to 30.5 would not
invalidate the cache. Match the *10 precision pattern already used for
imex_carriage_margin so 0.1 mm steps invalidate correctly.
Plater.cpp:select_plate_by_hover_id (right-click popup)
Two issues:
1. Lambda captured `modes` by reference. PopupMenu() is synchronous
today so the reference outlived the menu's event handling, but the
pattern is fragile — anyone refactoring to async Popup() would
silently dangle. Capture by value.
2. Used wxID_HIGHEST + i for menu item IDs — standard wx anti-pattern
because it can collide with other handlers listening in that range.
Allocate per-item IDs via wxNewId() and look up the chosen mode by
finding the event ID in a parallel vector. The lookup becomes O(N)
instead of O(1) but N is small (mode count) and this is clicker
latency, not a hot path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace imex_head_transform's fifth argument (Vec3d primary_origin) with a
Vec2d primary_zone_center and rewrite the Mirror branch as a true reflection
about the plane x = primary_zone_center.x + gantry_offset.x/2. Previous math
flipped about the primary's current origin, which:
* let the ghost drift out of the target zone as the primary moved, and
* made mirrored drag motion track 1:1 with the primary instead of reflecting.
The new transform places the ghost at the mirrored position within the target
zone (matching where the mirror tool actually prints) and reflects drag so
primary +X → ghost -X while Y tracks 1:1 — i.e. the ghost stays a true
mirror while the user drags. Off-row Mirror targets (e.g. T3 on a 2x2) still
reflect across the same X-plane as on-row peers.
PartPlate::calc_imex_ghosts and update_imex_ghost_transforms now feed primary_off
(the primary head's zone center) instead of an instance-space Vec3d.
Mirror tests rewritten against the new geometric contract: ghost origin at the
reflected position, primary drag deltas reflected across the zone-boundary plane.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Centralize the project→printer→printer_extruder_id fallback for the physical
extruder map. PrintApply, PartPlate (ghost color + cache key), Plater (tooltip
+ click gate) all previously open-coded the three-step lookup, and each handled
the "pem unset, derive from pei" case slightly differently — an IDEX printer
without an explicit pem could paint an UNPRINTABLE_COLOR ghost even though the
slicer would have derived a valid mapping.
- IMEXHelpers: add effective_physical_extruder_map(explicit_pem, pei) and a
PresetBundle overload that wraps the project→printer precedence.
- PrintApply: use the helper in place of the inline pei→pem normalization.
- PartPlate / Plater: call the PresetBundle overload at every ghost-color,
ghost-cache-key, tooltip, and click-gate site.
- Plater::format_imex_ghost_tooltip: when no filament resolves to a head,
surface an actionable message directing the user to extend the extruder
count in the Machine tab, instead of the generic "(no filament routed)".
- IMEXFilamentPickerPopover: hold m_pem by value so callers can pass a
stack-local derived pem without lifetime worries.
- Tests: 5 new cases covering explicit-wins, default-pem fallback, null
inputs, and the IDEX ghost-color regression that motivated this.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PartPlate::calc_imex_zones, GCodeViewer::render, and Plater::collect_imex_warnings
each hand-rolled their own "phys:P/C/M" tokenizer with subtly different error
handling. Replace the three inline loops with parse_imex_active_tools +
imex_primary_tool_for_mode so the Primary/Copy/Mirror classification agrees
across zones, the G-code viewer legend, and slice warnings.
No behavior change: the shared helpers preserve the 1=Primary / 2=Copy /
3=Mirror encoding already consumed downstream and continue to accept the
legacy bare-index form as Primary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the plater-icon popover with colored transparent ghost copies
of primary-head instances on the plate, one per secondary active head
under its Copy/Mirror role transform. Left-click on a ghost opens a
compact filament picker popover for the ghost's head (MMU lane override).
Ghosts track the primary through drag/rotate/scale/mirror and invalidate
on mode or pem changes.
Key pieces:
IMEXHelpers -- imex_head_transform (Primary/Copy/Mirror), shared role
parser, per-head filament resolution with X-axis Mirror anchor.
PartPlate -- ghost state, volume rebuild on mode/map/object mutation,
primary_origin plumbed for Mirror reflection across the primary-row
gantry plane.
GLCanvas3D -- ghost rendering with per-head filament color and
translucent blending; picking routed via volume composite id.
Plater -- ghost click + tooltip; plater icon left-click always cycles.
IMEXFilamentPickerPopover -- BitmapComboBox row for one secondary head,
writes imex_head_filament_map on selection.
bbs_3mf -- round-trip the per-plate imex_head_filament_map option.
PrintConfig -- add imex_head_filament_map as a plate option.
MMU/AFC routing for parallel modes relies on the printer profile's
physical_extruder_map (see prior commit for authoring format). Primary-
row heads and their per-plate filament overrides are resolved through
that map, so PA and temperature emission address the correct physical
extruder when multiple logical slots share one carriage.
Tests: IMEXHelpers coverage for Primary/Copy/Mirror transforms
including a 2x2 off-row regression guard for the X-axis reflection fix.
Temperature emission (GCode.cpp, PrintConfig.cpp, ConfigManipulation.cpp):
- Set temperatures for all active tools in layer_change_gcode for iMEX
parallel modes (primary + secondary carriages)
- Fix filament temperature commands so Layer 1 temperatures only emit on
the first layer; subsequent layers use normal layer-change temperatures
- Remove stray temperature commands that fired outside the intended context
- Consolidate iMEX temperature handling into the second-layer transition;
clean up emission logic and naming throughout
Pre-slice warning dialogs (PartPlate.cpp/hpp, Plater.cpp):
- Collect per-plate IMEX warnings before slicing: multi-material conflict,
bed temperature mismatch between carriages (>5 °C delta), and filament
type incompatibility
- Show a dismissible Yes/No dialog from both "Slice Plate" and "Slice All"
actions; No returns to 3D view, Yes proceeds to slice
- Refresh plate thumbnails after the panel switch so previously-generated
thumbnails are not left black
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add runtime display backend detection for Wayland support
Add LinuxDisplayBackend utility to detect X11 vs Wayland at runtime
using GDK_IS_X11_DISPLAY / GDK_IS_WAYLAND_DISPLAY macros. This is
the foundation for removing the forced GDK_BACKEND=x11 and enabling
native Wayland support.
- New files: LinuxDisplayBackend.hpp/.cpp with get_linux_display_backend(),
is_running_on_wayland(), and is_running_on_x11()
- Propagate wxHAVE_GDK_X11 / wxHAVE_GDK_WAYLAND from FindGTK3.cmake
as compile definitions to libslic3r_gui
- No-op on non-Linux platforms (returns Unknown / false)
* Fix Phase 1 code quality: pragma once, source ordering, static cache
* Make X11 initialization conditional for Wayland support
Remove the unconditional GDK_BACKEND=x11 force that blocked native
Wayland. Replace with conditional logic:
- EGL safety fallback: re-force X11 only when wxUSE_GLCANVAS_EGL is
off and WAYLAND_DISPLAY is set, with a warning log
- XInitThreads() only called when DISPLAY is set (X11 in use)
- __GLX_VENDOR_LIBRARY_NAME only set when DISPLAY is present (GLX-specific)
- WEBKIT_DISABLE_COMPOSITING_MODE only set under XWayland (both
DISPLAY and WAYLAND_DISPLAY present)
- Guard X11/Xlib.h include with __has_include for robustness
- Restore display validation to accept either DISPLAY or WAYLAND_DISPLAY
This is Phase 2 of the Wayland support plan.
* Fix Phase 2: safer EGL macro check, add clarifying comments
* Add GLAD2 library and replace GLEW linkage in build system
Set up GLAD2 as a static library to replace GLEW for OpenGL loading.
GLAD2 supports both GLX and EGL, which is required for Wayland support.
- Create src/glad/ with pre-generated GLAD2 sources (GL 4.6 compat)
- Add src/glad/CMakeLists.txt building glad as a static library
- Wire glad into src/CMakeLists.txt before libvgcode
- Modify libvgcode to use shared glad for GL path (keeps local copy
only for GLES2/Emscripten) to avoid duplicate symbol conflicts
- Replace GLEW::GLEW with glad in libslic3r_gui link libraries
Note: GLEW is kept in deps for OpenCSG. Code migration from GL/glew.h
to glad/gl.h headers will follow in Phase 3B+3C.
* Fix Phase 3A+3D: libvgcode GLAD include, dead files, dlopen dep, OpenGL link var
* Migrate from GLEW to GLAD: replace headers and API calls across codebase
Replace all #include <GL/glew.h> with <glad/gl.h> across 49 source files.
Migrate GLEW API calls to GLAD equivalents:
- glewInit/glewExperimental -> gladLoaderLoadGL()
- GLEW_EXT_* / GLEW_ARB_* extension checks -> GLAD_GL_EXT_* / GLAD_GL_ARB_*
- Remove GLEW-specific EGL/GLX mismatch #error guards (not needed with GLAD)
- Replace unavailable EXT symbols with core GL equivalents in
GLCanvas3D.cpp (GL_MAX_SAMPLES, glRenderbufferStorageMultisample,
glBlitFramebuffer, GL_READ/DRAW_FRAMEBUFFER)
- Update log messages from glewInit to gladLoadGL
* Fix Phase 3B+3C: remove GLEW find, clean EXT symbols, update attribution
- Remove find_package(GLEW) block from root CMakeLists.txt since GLEW
is no longer linked by any main application code
- Remove "glew" from SLIC3R_STATIC option description
- Replace all remaining EXT framebuffer symbols with core equivalents
in render_thumbnail_framebuffer_ext and _rectangular_selection_picking_pass
- Update AboutDialog credits from GLEW to GLAD
* Enable EGL in wxWidgets and add runtime GLX/EGL selection for Wayland
- Set wxUSE_GLCANVAS_EGL=ON in wxWidgets build and Flatpak manifest
- Add PreferGLX() call on X11 sessions for driver compatibility
- Remove Phase 2 safety fallback (EGL is now always compiled in)
- Guard SwapBuffers against hidden canvases to prevent Wayland stalls
* Fix Phase 4: move PreferGLX to app startup, fix FPS counter guard
Move wxGLCanvas::PreferGLX() from OpenGLManager::create_wxglcanvas()
(static initializer) to GUI_App::on_init_inner() before any wxGLCanvas
is constructed. This prevents a race where SkipPartCanvas could trigger
wxGLBackend::Init() before the GLX preference is set. The new location
also adds explicit is_running_on_wayland() detection with a warning for
unknown backends.
Move increment_fps_counter() inside the IsShownOnScreen() guard so FPS
is only counted when a frame is actually swapped.
* Update GLFW from 3.3.7 to 3.4 for runtime Wayland/X11 backend selection
Replace the compile-time GLFW_USE_WAYLAND flag (which locked to a single
backend) with GLFW 3.4's GLFW_BUILD_WAYLAND + GLFW_BUILD_X11 flags that
build both backends and auto-select at runtime based on the available
display server. This enables the CLI thumbnail renderer to work on both
Wayland and X11 sessions without separate builds.
* wayland: Fix UI call sites that rely on global screen coordinates
On Wayland, wxGetMousePosition() returns (0,0) and SetPosition() is a
no-op for top-level windows. Fix the highest-impact call sites:
- GLCanvas3D: Use cached m_mouse.position from event handlers instead
of wxGetMousePosition() + ScreenToClient() in get_local_mouse_position()
- Plater: Use event-relative coords via ClientToScreen(e.GetPosition())
instead of wxGetMousePosition() in 3 leave-window handlers
- BBLTopbar: Use event.GetPosition() and FindToolByPosition() directly
in mouse handlers instead of wxGetMousePosition()/FindToolByCurrentPosition()
- Search: Use focus-based dismiss logic on Wayland instead of
wxGetMousePosition()-based rect checks in SearchDialog and
SearchObjectDialog
- GUI_App: Skip SetPosition() in window_pos_restore() on Wayland where
it is a no-op; still restore size and maximize state
- Button: Position tooltip relative to button widget via ClientToScreen
instead of wxGetMousePosition()
* Fix SearchDialog Wayland dismiss: guard against search_line focus
* flatpak: Add Wayland socket permission for native Wayland support
* spec
* Fix crash on Wayland when wxWidgets lacks EGL support
Restore the safety fallback that forces GDK_BACKEND=x11 when wxWidgets
was not built with wxUSE_GLCANVAS_EGL=ON. Without this, the GLX backend
tries to access a non-existent X11 display on native Wayland, crashing
in wxGLCanvas::IsDisplaySupported() with SIGSEGV at offset 0xe4.
Also add a defense-in-depth guard in detect_multisample() that skips
the IsDisplaySupported call entirely on Wayland without EGL.
Root cause: deps/wxWidgets must be rebuilt after enabling EGL. The
compile-time check in OrcaSlicer.cpp detects the mismatch and falls
back safely.
* Fix EGL detection: use wxHAS_EGL instead of wxUSE_GLCANVAS_EGL
wxUSE_GLCANVAS_EGL is a CMake build option, NOT a C++ preprocessor
macro. The actual macro defined in wxWidgets setup.h is wxHAS_EGL.
All compile-time EGL checks were using the wrong macro, causing
the safety fallback to always trigger even with a properly built
EGL-enabled wxWidgets.
* Fix GL function pointers invalidated on Wayland/EGL
gladLoaderLoadGL() dlopen's libGL.so.1 to resolve GL function pointers
via dlsym, then immediately dlclose's the handle. On X11/GLX this is
fine because the GLX context keeps libGL.so mapped. On Wayland/EGL,
nothing else holds libGL.so open, so dlclose unmaps it and all function
pointers become dangling — causing SIGSEGV on the first GL call.
Fix: on Wayland, use gladLoadGL(eglGetProcAddress) which resolves
function pointers through the EGL loader without opening/closing
libGL.so.
* fix crash on start and various rendering issues
* fix crash on close
* small refactor
* move GPU selection to desktop file
* clean up a bit
* clean up more
* fix appimage error
- Add warning badge (obj_warning.svg overlay) to the iMEX plate icon when
a parallel mode is active alongside multi-material objects on the same plate
- Add has_imex_multimaterial_conflict() using get_extruders(true) so only
filaments actually used on the plate are checked
- Move multi-material caution dialog from reslice() into on_action_slice_plate /
on_action_slice_all so it fires exactly once per user action and does not
disrupt GL thumbnail generation during Slice All
- Fix is_imex missing from p->config init key list so on_config_change()
diff detection correctly triggers refresh_imex_icons()
- Defer imex_changed handling until after set_bed_shape() so m_shape is current
- Replace plain remove button with ScalableButton (imex_remove.svg) in mode rows
- Add EditGCodeDialog launch button per mode row for placeholder browsing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IMEXModesCtrl:
- Primary mode is now a non-deletable first row stored as sentinel
"primary" in imex_mode_names; older configs load cleanly
- New rows default T0 → Primary when no tool assignment is stored
- Filter "primary" from plater popup/cycle list to prevent double entry
- imex_tools_per_gantry cap raised 2 → 4
Zone sizing:
- Zone width/height now based on active tool count only; inactive tools
donate their bed share to active neighbors (fixes 4-tool layout)
- Active col/row maps (col_to_zone/row_to_zone) applied consistently
across zone fills, collision strips, and primary zone box
GCodeViewer animation:
- Mirror position formula fixed: left-of-copy reflects across copy
zone's left edge; right-of-copy reflects across right edge
(T3 was rendering on top of T1)
- strip_width/row_strip_height use active counts, matching PartPlate
Stability:
- Early return in calc_imex_zones() when tool_states is empty; prevents
OOB crash on new printer with stale process-preset mode name
- is_imex toggle in on_config_change calls refresh_imex_icons() so the
plate mode icon appears without requiring a new project
GCode:
- Remove "primary" guard so Primary mode gcode field is emitted at
start of print
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>