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>
* fix(libnest2d): skip the excluded-region alignment pass when there are none
NfpPlacer::finalAlign(), run from clearItems() and the destructor, always
ran the "find a best position inside the NFP of fixed items" pass even when
no items are fixed. With nothing to avoid, calcnfp() computes the inner-fit
NFP of the pile and can feed clipper a coordinate outside its allowed range.
On Linux/clang the value stays in range so it went unnoticed; on MSVC the
clipper "Coordinate outside allowed range" exception escapes the noexcept
destructor and aborts the process (exit 0xC0000409).
Build the excluded set up front and only run the pass when it is non-empty.
The block exists solely to keep the pile clear of fixed items (excluded
regions / wipe tower), so it is a no-op when there are none and the
wipe-tower behaviour is unchanged.
* test(libnest2d): remove dead nesting tests and split the suite by feature
Seven of the suite's hidden [.] test cases drove code paths Orca abandoned
at the BambuStudio fork: BottomLeftPlacer (used nowhere in src/) and the
stock default NfpPlacer backend, which returns zero bins in Orca. They have
been red since the fork and are never registered with ctest. Remove them.
Split the 1,000-line libnest2d_tests_main.cpp into per-feature files, per the
repo convention, sharing a header for the no-fit-polygon backend setup that
every translation unit must agree on (ODR):
libnest2d_tests.cpp Item and nest() basics
test_geometry.cpp geometry primitives
test_nfp.cpp no-fit-polygon machinery
libnest2d_test_utils.hpp shared includes and the NFP backend specialisation
Along the way: drop a debug exportSVG() helper that only wrote a file on test
failure (so the suite never leaves stray assets), convert the deprecated
Catch::Approx to WithinRel/WithinAbs matchers, and give the tests descriptive
names.
* test(libnest2d): add NfpPlacer unit tests
NfpPlacer is the placement engine the arranger drives, but the suite only
covered the geometry primitives. Add a fixture and five tests that exercise
pack()/accept() directly: a single item lands in the bin, an oversized item
is rejected, the first item is seeded for every starting point, many items
pack without overlap, and the rotation candidates are searched. This lifts
nfpplacer.hpp line coverage from 42% to 87% in the libnest2d suite.
* test(libslic3r): add arrangement::arrange() integration coverage
The libnest2d suite cannot reach Orca's real nesting entry point because it
does not link libslic3r. Add test_arrange.cpp driving arrangement::arrange():
items land on the bed and within bounds, do not overlap, are spaced by their
inflation, an oversized item stays unplaced, overflow spills onto virtual beds,
an empty input is a no-op, and the DONT_ALIGN and USER_DEFINED final-alignment
paths are exercised. A self-test guards the overlap check the other cases use.
The testing guide stated OrcaSlicer uses Catch2 v2 and advised the v2
`<catch2/catch.hpp>` include, but the vendored framework is v3.11.0
(tests/catch2/) and every test file includes `<catch2/catch_all.hpp>`.
The wrong version drove several incorrect claims: that SKIP() is
unavailable (it is, v3.3.0+), that the string matcher is "Contains"
rather than "ContainsSubstring", and that thread-safe assertions,
multiple reporters, STATIC_CHECK and built-in sharding do not exist.
Correct all version statements, the example include, and the
former "Version-Specific Limitations" section to reflect v3.11.0.
Re-enable [OrcaCloudServiceAgent] tests now that the headless crash is fixed
The two OrcaCloudServiceAgent display-name tests were tagged [NotWorking]
in #14175 because the agent constructor dereferenced a null wxTheApp when
run headless (no wxApp is created in the unit-test binary), crashing before
any assertion ran. That null dereference was fixed in 14d2dfdd4c, which
guards wxTheApp in compute_fallback_path() and skips file persistence when
no fallback path is available.
With the fix in place both tests build and pass headless, so drop the
[NotWorking] tag and the stale explanatory comments. Verified on Linux
clang-18 (the CI compiler), headless: 20 assertions in 2 test cases pass.
Closes#14193
* Disable fff_print tests that fail only in CI
Skirt height is honored, Scenario: Skirt and brim generation, and
Scenario: PrintGCode basic functionality slice geometry that makes clipper's
coordinate range check throw "Coordinate outside allowed range" in the Linux
CI environment, while the same tests pass in local builds. Tag them
[NotWorking] so the Unit Tests job (ctest -LE NotWorking) excludes them until
the underlying slicing issue is fixed in a follow-up PR.
* Trigger Build all workflow on tests/** changes
The push and pull_request path filters did not include tests/**, so a
test-only change never started the build and the Unit Tests job never ran.
Add tests/** to both filters so changes to the test suite are built and
exercised by CI.
Fix Unit Tests CI job silently running zero tests
scripts/run_unit_tests.sh selected tests with `ctest -L "Http|PlaceholderParser"`,
but catch_discover_tests() was called without ADD_TAGS_AS_LABELS, so Catch2 tags
were never registered as CTest labels. The -L filter matched nothing and the job
passed green while running no tests ("No tests were found!!!"). Tests have not run
in CI since PR #11485 added that -L line (2025-12-23).
Register tags as labels via a shared orcaslicer_discover_tests() wrapper in
tests/CMakeLists.txt (passing ADD_TAGS_AS_LABELS), routed through all five test
suites. Restore full-suite execution by replacing the narrow -L selection with a
`-LE NotWorking` exclusion, so all reliable tests gate PRs again (the suite ran in
full before #11485).
Tag the two OrcaCloudServiceAgent display-name tests [NotWorking]: their
constructor reaches wxStandardPaths::Get().GetUserDataDir(), which dereferences
the null wxTheApp in the headless test binary and segfaults on every platform.
Excluded until the agent can be constructed without the wx app context.
CI now runs 151 tests (was 0) and passes.
* Fix null-deref and arranger bugs that gate headless slicing tests
export_gcode dereferenced a null result out-param, enum serialization
dereferenced a null keys_map, and get_arrange_polys left bed_idx unseeded so
the arranger dropped items. All only affect the headless test/CLI path.
* Fix the headless test harness and add G-code test helpers
Use the real arranger, fix temp-file handling with an RAII guard, and add
layers_with_role / max_z for inspecting sliced G-code.
* Re-enable the Model construction test
* Re-enable SupportMaterial tests and add an enforced-support test
* Re-enable and extend PrintObject layer-height and perimeter tests
* Re-enable Print skirt, brim, and solid-surface tests
* Re-enable and extend PrintGCode tests
Un-hide the basic scenario (dead-key fixes, reframes, trimmed trivia) and add
initial-layer-height, sequential-order, and null-result export tests.
* Re-enable and reframe the skirt/brim tests
Detect skirt/brim by G-code role comment instead of a sentinel speed, and
resolve the previously-unfinished skirt-enclosure test.
* Replace the stale lift()/unlift() test with a z_hop test
* Delete the stub and broken Flow tests
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.
* Add test for Arachne duplicate wall segment detection
Add test cases that reproduce an issue where Arachne generates
duplicate/coinciding extrusion segments at certain min_bead_width settings.
Test configuration:
- Profile: 0.28mm Extra Draft @BBL X1C (0.4mm nozzle, 0.28mm layer)
- outer_wall_line_width: 0.42mm, inner_wall_line_width: 0.45mm
- wall_loops: 2, precise_outer_wall: enabled
- Test polygon: outer rectangle (0,0)-(20,20) with inner cutout (0.5,0.5)-(19.5,19.5)
This creates a 0.5mm wide frame around the perimeter.
Results:
- 50% min_bead_width (0.20mm): FAILS - detects 4 duplicate segments (all 4 sides)
- 60% min_bead_width (0.24mm): PASSES - no duplicates
At 50%, Arachne generates two separate closed loops that share all 4 edges
of the inner square. At 60%, Arachne generates a single closed loop.
SVG output is exported to /tmp/opencode/ for visual debugging.
* Fix Arachne duplicate extrusion caused by bead count mismatch
WideningBeadingStrategy::compute() used optimal_width (inner wall width)
to determine if a thin wall should produce a single bead. However,
getOptimalBeadCount() uses optimal_width_outer (outer wall width) via
RedistributeBeadingStrategy to decide the bead count.
This inconsistency caused situations where getOptimalBeadCount() returned
2 beads, but compute() produced only 1 bead at full thickness. The single
bead was then generated for both inner and outer contours, resulting in
duplicate extrusion paths.
Fix: Use getTransitionThickness(1) instead of optimal_width. This method
returns the exact threshold for the 1-to-2 bead transition, ensuring
consistency between bead count calculation and bead generation.
Reproduces with: 50% min_bead_width, 0.42mm outer wall, 0.45mm inner wall,
0.5mm polygon inset creating ~0.38mm wall thickness.
Fixes#13917
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
Includes nozzle-diameter guards for printers without nozzle info (#13255,
now landed upstream) and manual-calibration nozzle mismatch fix (#13882).
# Conflicts:
# src/slic3r/GUI/GCodeViewer.cpp
# src/slic3r/GUI/GLCanvas3D.hpp
# src/slic3r/GUI/Tab.cpp
* Add get_json_string_field helper
Introduce get_json_string_field in OrcaCloudServiceAgent.cpp to safely extract string fields from JSON objects.
* Add resolve_display_name helper
Introduce resolve_display_name to normalize provider metadata labels for the UI. The helper returns the first non-empty value from display_name, nickname, full_name, name, falling back to username, resolving human-facing label across varying provider payloads.
* Replace safe_str anonymous function
Replace get_json_string_field for better readability.
* Replace resolution flow for nickname with function
Consolidate both flows into one function for easier maintenance and more consistency.
* Update OrcaCloudServiceAgent.hpp
* Add OrcaCloudServiceAgent display name tests
Add unit tests verifying OrcaCloudServiceAgent resolves a user's display name from various session JSON shapes.
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>
* Update eigen from v3.3.7 to v5.0.1.
This updates eigen from v3.3.7 released on December 11, 2018-12-11 to v5.0.1
released on 2025-11-11. There have be a large number of bug-fixes,
optimizations, and improvements between these releases. See the details at;
https://gitlab.com/libeigen/eigen/-/releases
It retains the previous custom minimal `CMakeLists.txt`, and adds a
README-OrcaSlicer.md that explains what version and parts of the upstream
eigen release have been included, and where the full release can be found.
* Update libigl from v2.0.0 (or older) to v2.6.0.
This updates libigl from what was probably v2.0.0 released on 2018-10-16 to
v2.6.0 released on 2025-05-15. It's possible the old version was even older
than that but there is no version indicators in the code and I ran out of
patience identifying missing changes and only went back as far as v2.0.0.
There have been a large number of bug-fixes, optimizations, and improvements
between these versions. See the following for details;
https://github.com/libigl/libigl/releases
I retained the minimal custom `CMakeLists.txt`, added `README.md` from the
libigl distribution which identifies the version, and added a
README-OrcaSlicer.md that details the version and parts that have been
included.
* Update libslic3r for libigl v2.6.0 changes.
This updates libslic3r for all changes moving to eigen v5.0.1 and libigl
v2.6.0. Despite the large number of updates to both dependencies, no changes
were required for the eigen update, and only one change was required for the
libigl update.
For libigl, `igl::Hit` was changed to a template taking the Scalar type to
use. Previously it was hard-coded to `float`, so to minimize possible impact
I've updated all places it is used from `igl::Hit` to `igl::Hit<float>`.
* Add compiler option `-DNOMINMAX` for libigl with MSVC.
MSVC by default defines `min(()` and `max()` macros that break
`std::numeric_limits<>::max()`. The upstream cmake that we don't include
adds `-DNOMINMAX` for the libigl module when compiling with MSVC, so we need
to add the same thing here.
* Fix src/libslic3r/TriangleMeshDeal.cpp for the unmodified upstream libigl.
This fixes `TriangleMeshDeal.cpp` to work with the unmodified upstream
libigl v2.6.0. loop.{h,cpp} implementation.
This file and feature was added in PR "BBS Port: Mesh Subdivision" (#12150)
which included changes to `loop.{h,cpp}` in the old version of libigl. This PR
avoids modifying the included dependencies, and uses the updated upstream
versions of those files without any modifications, which requires fixing
TriangleMeshDeal.cpp to work with them.
In particular, the modifications made to `loop.{h,cpp}` included changing the
return type from void to bool, adding additional validation checking of the
input meshes, and returning false if they failed validation. These added
checks looked unnecessary and would only have caught problems if the input
mesh was very corrupt.
To make `TriangleMeshDeal.cpp` work without this built-in checking
functionality, I removed checking/handling of any `false` return value.
There was also a hell of a lot of redundant copying and casting back and forth
between float and double, so I cleaned that up. The input and output meshs use
floats for the vertexes, and there would be no accuracy benefits from casting
to and from doubles for the simple weighted average operations done by
igl::loop(). So this just uses `Eigen:Map` to use the original input mesh
vertex data directly without requiring any copy or casting.
* Move eigen from included `deps_src` to externaly fetched `deps`.
This copys what PrusaSlicer did and moved it from an included dependency under
`deps_src` to an externaly fetched dependency under `deps`. This requires
updating some `CMakeList.txt` configs and removing the old and obsolete
`cmake/modules/FindEigen3.cmake`. The details of when this was done in
PrusaSlicer and the followup fixes are at;
* https://github.com/prusa3d/PrusaSlicer/commit/21116995d78b74dee69ce42874b643012a7c14e5
* https://github.com/prusa3d/PrusaSlicer/issues/13608
* https://github.com/prusa3d/PrusaSlicer/pull/13609
* https://github.com/prusa3d/PrusaSlicer/commit/e3c277b9eea0ca25e90e2378bcda198d56c4d304
For some reason I don't fully understand this also required fixing
`src/slic3r/GUI/GUI_App.cpp` by adding `#include <boost/nowide/cstdio.hpp>` to
fix an `error: ‘remove’ is not a member of ‘boost::nowide'`. The main thing I
don't understand is how it worked before. Note that this include is in the
PrusaSlicer version of this file, but it also significantly deviates from what
is currently in OrcaSlicer in many other ways.
* Whups... I missed adding the deps/Eigen/Eigen.cmake file...
* Tidy some whitespace indenting in CMakeLists.txt.
* Ugh... tabs indenting needing fixes.
* Change the include order of deps/Eigen.
It turns out that although Boost includes some references to Eigen, Eigen also
includes some references to Boost for supporting some of it's additional
numeric types.
I don't think it matters much since we are not using these features, but I
think technically its more correct to say Eigen depends on Boost than the
other way around, so I've re-ordered them.
* Add source for Eigen 5.0.1 download to flatpak yml config.
* Add explicit `DEPENDS dep_Boost to deps/Eigen.
I missed this before. This ensures we don't rely on include orders to make
sure Boost is installed before we configure Eigen.
* Add `DEPENDS dep_Boost dep_GMP dep_MPFR` to deps/Eigen.
It turns out Eigen can also use GMP and MPFR for multi-precision and
multi-precision-rounded numeric types if they are available.
Again, I don't think we are using these so it doesn't really matter, but it is
technically correct and ensures they are there if we ever do need them.
* Fix deps DEPENDENCY ordering for GMP, MPFR, Eigen, and CGAL.
I think this is finally correct. Apparently CGAL also optionally depends on
Eigen, so the correct dependency order from lowest to highest is GMP, MPFR, Eigen, and CGAL.
---------
Co-authored-by: Donovan Baarda <dbaarda@google.com>
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
* Add OrcaCloud sync platform and preset bundle sharing system
Introduce OrcaCloud, a cloud sync platform for user presets, alongside
a preset bundle system that enables sharing printer/filament/process
profiles as local exportable bundles or subscribed cloud bundles.
OrcaCloud platform:
- Auth to Orca Cloud
- Encrypted token storage (file-based or system keychain)
- User preset sync with
- Profile migration from default/bambu folders on first login
- Homepage integration with entrance to cloud.orcaslicer.com
Preset bundles:
- Local bundle import/export with bundle_structure.json metadata
- Subscribed cloud bundles with version-based update checking
- Thread-safe concurrent bundle access with read-write mutex
- Canonical bundle preset naming (_local/<id>/... and _subscribed/<id>/...)
- Bundle presets are read-only; grouped under subheaders in combo boxes
- PresetBundleDialog with auto-sync toggle, refresh, update notifications
- Hyperlinked bundle names to cloud bundle pages
Co-authored-by: Sabriel Koh <sabrielkcr@gmail.com>
Co-authored-by: Derrick <derrick992110@gmail.com>
Co-authored-by: Mykola Nahirnyi <mnahirnyi@amcbridge.com>
Co-authored-by: Ian Chua <iancrb00@gmail.com>
Co-authored-by: Draginraptor <draginraptor@gmail.com>
Co-authored-by: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Co-authored-by: Ocraftyone <Ocraftyone@users.noreply.github.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
Co-authored-by: peterm-m <101202951+peterm-m@users.noreply.github.com>
* Fixed an issue on Windows it failed to login Orca Cloud with Google account
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>
Catches the case where a non-primary IMEX mode's active tools all sit on
one gantry — e.g. mode "0:P,1:C" on a 2x2 IQEX where T0 and T1 share
gantry 0. The "Copy"/"Mirror" label is decorative there: nothing actually
parallel-prints, but the user's mode_gcode still fires and emits firmware
setup that doesn't apply, while the slicer treats it as a multi-color
parallel print. Conceptually it's just a regular multi-tool single-gantry
print and belongs in Primary mode.
Changes:
- imex_multicolor_block_reason now collects the set of distinct gantries
spanned by the active tools and short-circuits with a clear "single
gantry — not a parallel-print scenario" message before falling through
to the existing within-gantry-swap check.
- Drops the redundant pre-slice "Multi-material objects detected" soft
warning from collect_imex_warnings — the slice-time block surfaces a
more specific message at the right moment, and the soft warning was
vague handwaving in front of it. Bed-temp + filament-type checks stay.
- New unit test covering the single-gantry block.
Behavior matrix on a 2x2 IQEX with multi-color:
"0:P,1:C" single gantry -> BLOCK (new)
"0:P,2:C" dual gantry, 1 each -> BLOCK (existing within-gantry-swap)
"0:P,1:C,2:M,3:M" dual gantry, 2 each -> ALLOW
multiple filaments to same physical via pem -> BLOCK (existing MMU sharing)
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>
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>
Three of the four IMEX physical-vs-logical fixes landed earlier on this
branch (fbc58d2a1d, fa048babeb, a38b95bf45, e11e7d46df) used inline
lambdas / direct loops to translate physical extruder indices to logical
filament slots. No test coverage existed for the specific composition,
even though the underlying primitives (resolve_filament_for_head,
first_filament_for_physical_head) were tested.
Pull two patterns out of Plater.cpp and GCode.cpp into IMEXHelpers as
named helpers, then unit-test them:
imex_primary_logical_from_objects(used_slots_1b, pem, primary_physical)
Walks the plate's used filament slots (1-based) and returns the
first one whose pem entry maps to the primary's physical extruder.
This is what the warning's `logical_for_primary` now delegates to —
moves the "look at object assignments, not pem first-routed default"
behavior introduced in fa048babeb out of the lambda and into a
separately-testable function.
imex_secondary_logical_slots(active_physicals, primary_physical,
plate_head_filament_map, pem)
Iterates IMEX active physicals, skips the one matching primary,
resolves each remainder via resolve_filament_for_head (per-plate
override + first-routed fallback), deduplicates, drops -1 entries.
Replaces the inline loop in GCode::_do_export's is_extruder_used
marking (a38b95bf45 + e11e7d46df).
10 new test cases in test_imex_helpers.cpp cover the cases that
correspond directly to bugs hit:
imex_primary_logical_from_objects:
- AFC primary picks the object's slot (the user's specific bug)
- Multi-color AFC primary returns first input-order match
- Direct extruder primary unambiguous (no MMU)
- No object routed to primary returns -1
- Empty inputs (no objects, empty pem)
imex_secondary_logical_slots:
- Copy mode skips primary, falls back to first-routed
- IQEX 4-mode enumerates all three secondaries
- Per-plate override wins over first-routed
- Drops unrouted physicals + deduplicates
- Only-primary-active returns empty
All [IMEX] + [Variant] + [3mf] regression: 166 assertions / 61 cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings in the IMEX test coverage (PA per-firmware, Temperature per-firmware,
[Variant] expansion, 3MF round-trip, imex_pem_tool_for helper + tests, and
the cherry-picked variant-truncation regression test).
Resolution notes:
- Two GCode.cpp call sites for set_pressure_advance had divergent edits:
* tests/imex-coverage rewrote them to use the new imex_pem_tool_for
helper (commit c2492ccc47), eliminating the inline parallel-mode
check entirely.
* feedback replaced the literal "primary" with kImexPrimaryMode in the
same lines (commit 085f5ccec8).
Resolution: keep the helper-call form. The kImex change is moot on lines
the helper replaces, and imex_pem_tool_for in IMEXHelpers.cpp is also
updated to use kImexPrimaryMode for consistency with the rest of the
codebase.
- Test test_3mf.cpp updated for upstream's load_bbs_3mf signature change
(PR adds is_orca_3mf out-parameter between is_bbl_3mf and file_version).
All three call sites in the new IMEX 3MF round-trip tests pass &is_orca
in addition to &is_bbl.
Full regression post-merge:
libslic3r: 143 cases / 48,553 assertions (+10 cases from new tests)
fff_print: 24 cases / 245 assertions (+10 cases from new tests)
sla_print: 21 cases / 14,100 assertions
libnest2d: 14 cases / 488 assertions
slic3rutils: 3 cases / 3 assertions
All tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-review found two weaknesses in the preceding test commits:
1) The equal-size [Variant] scenario claimed to distinguish the truncation
guard's `cur > target` predicate from a regression to `cur >= target`,
but both paths yield identical child values in practice: when
extruder_variant names match, set_with_restore's variant_index is fully
populated (no -1 slots) and the merge path restores every position from
backup — producing the same {1.5, 2.5} output as the skip path. The
test passes in both guard states.
Rewritten to use mismatched variant names between child and parent.
variant_index then has -1 slots, and set_with_restore overwrites those
positions with parent values. Now the merge path yields {0.8, 0.8} and
the skip path yields {1.5, 2.5} — observably different. Verified:
- `cur > target` (correct): 4 scenarios pass, 15 assertions
- `cur >= target` (regressed): equal-size scenario fails with
"1.5 is within 0.000000001 of 0.80000000000000004"
- Guard removed entirely: child>parent + stride=2 both fail with
truncation ("1 == 2" / "2 == 4")
2) The [3mf][IMEX] round-trip only covered a single plate. A plate-
indexing regression (IMEX metadata landing on the wrong plate, or
bleeding across plates on reload) would not have been caught.
Added a multi-plate scenario: two plates with distinct mode and
head-filament-map values. Asserts both land on their respective
destination plates after reload. Load-bearing verified:
- With IMEX serialization intact: 3 scenarios pass, 45 assertions
- With IMEX serialization disabled: positive + multi-plate fail
(both "nullptr != nullptr"); primary-mode passes (expects nullptr)
- With primary-mode short-circuit removed: primary-mode scenario
fails ("0x... == nullptr") because primary modes now serialize
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four scenarios cover the temperature emission surface that IMEX layer-change
handling routes through (Tier 1 of the deferred Target B test plan —
pure-function-only, no fixture).
- Per-flavor command routing: Marlin (M104), RRF (G10 — M104 is deprecated
on RRF), Mach3/Machinekit (P-prefix for value instead of S).
- Wait handling: Marlin emits M109, MakerWare/Sailfish silently drop wait
requests (the firmware doesn't support blocking waits), Teacup and RRF
both emit a separate M116 poll.
- Per-tool qualifier for IMEX secondary carriages: Marlin and Klipper
emit T<N>, RRF uses P<N> (same P override as its wait poll). This is
exactly the path that lets IMEX set secondary-tool layer temperatures
without a tool-change.
- Instance overload's multi-extruder gating: a tool index passed to a
single-extruder GCodeWriter is discarded (no spurious T0 on
single-tool printers), but a multiple_extruders writer passes it
through verbatim.
All 24 assertions in 4 cases pass under [Temperature].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The physical_extruder_map translation used by IMEX per-tool PA emission was
inlined identically at two sites in GCode.cpp (tool-change and second-layer
transition). Extract to IMEXHelpers so the routing rule ("parallel mode AND
populated pem → physical index, else -1") is testable in isolation and the
call sites read as intent rather than re-deriving the conditional.
Production change is behavior-preserving:
- Same predicate (`!mode.empty() && mode != "primary"`)
- Same empty-pem short-circuit returning -1
- Same get_at() dispatch on hit
- Both call sites replaced with a single call
Four unit tests in [IMEX] cover the routing matrix:
- non-IMEX ("") and primary mode short-circuit
- parallel mode + empty pem short-circuits (defense-in-depth; get_at would
throw on empty values otherwise)
- identity pem (non-MMU IDEX) routes filament to itself
- MMU collapse routes multiple logical slots to one physical (7-slot profile
with 4-lane MMU on physical 0 and direct drives on 1/2/3)
All IMEX + Variant regression suites pass post-refactor (133 assertions / 48
cases under libslic3r, 25 assertions / 6 cases under fff_print [PressureAdvance]).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Validates that imex_parallel_mode and imex_head_filament_map survive a
full store_bbs_3mf → load_bbs_3mf cycle — the same silent-state-loss bug
class that produced the variant-vector truncation regression, applied to
IMEX plate state which rides the same XML metadata path.
- Positive round-trip: a plate with copy_mode + a non-trivial head
filament map ("1:2,2:3") is saved and reloaded; both options land on
the destination plate's config with the exact values preserved.
- Guard scope: a plate with mode="primary" and empty head-filament-map
does NOT emit metadata (per the serializer's short-circuit), and the
reload leaves both options absent from the destination config. If the
serializer ever regressed to writing primary-mode plates, the load
path would surface phantom "primary" strings on plates that shipped
clean — this catches that.
Both scenarios call set_temporary_dir to point the BBS exporter's backup
scaffolding at a writable per-process temp directory (by default it
resolves under root at runtime, which fails for non-root test
processes).
All 27 assertions in 2 test cases pass under [3mf][IMEX].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Exercises the IMEX per-tool PA emission surface added in af59501f4a
("feat: firmware-agnostic per-tool PA emission for IMEX parallel modes").
Six scenarios cover the full routing matrix:
- Negative PA returns empty across all flavors (early-exit guard).
- Klipper: bare vs EXTRUDER=extruder vs EXTRUDER=extruderN. Asserts the
tool=0 case emits the unsuffixed extruder name (first Klipper extruder
is named "extruder", not "extruder0") — a subtle edge case easy to
regress.
- RRF: bare vs D0 vs DN. The D0 case matters: passing tool=0 explicitly
must emit `D0`, not the current-tool fallback.
- Marlin 2.x: bare vs T0 vs TN.
- Marlin Legacy: tool index is silently dropped — verifies the fallback
branch can't accidentally start emitting T qualifiers on firmware that
doesn't support them.
- BBL: flag wins over firmware flavor (Marlin 2 flavor + BBL flag emits
the BBL-specific `M900 K... L1000 M10`) and BBL never emits a per-tool
qualifier regardless of the tool argument.
All 25 assertions across 6 cases pass under [PressureAdvance].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three scenarios alongside the existing child>parent stride=1 regression
test for update_non_diff_values_to_base_config:
- stride=2 child>parent: machine_max_acceleration_x (size 4 vs 2) — confirms
the truncation guard fires for the (normal,silent)-pair stride=2 path, not
just stride=1. Catches a regression class the existing test would miss
because stride=2 routes through normalize_stride2_floats and a different
set_with_restore call site.
- equal-size (2=2): exercises the path the guard does NOT short-circuit;
asserts child per-extruder values survive set_with_restore's nil-restore
merge. Catches any future change that breaks the equal-size merge — the
fix's `cur > target ? skip` predicate could regress to `cur >= target` and
silently override child values otherwise.
- non-variant scalar: layer_height in `keys` and `different_keys` but absent
from printer_options_with_variant_1/_2. Hits the is_scalar() / "nothing to
do" branch and must remain untouched. Scopes the guard's blast radius.
All four scenarios in the [Variant] tag pass: 15 assertions, 4 test cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a Catch2 scenario that builds a 2-extruder child DynamicPrintConfig
inheriting from a 1-extruder parent, calls update_non_diff_values_to_base_config
through the real printer_options_with_variant_1 / _2 key sets, and asserts
that printer_extruder_id, printer_extruder_variant, and retraction_length
retain their full size after the merge. Covers three distinct
set_with_restore<T> instantiations (Ints, Strings, Floats) and verifies
both size preservation and per-extruder value preservation.
Verified load-bearing: with the guard in update_non_diff_values_to_base_config
temporarily removed, the test fails with "1 == 2" on pe_id.values.size() and
retraction_length.values.size(); with the guard restored, all six assertions
pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
wxWidgets defines _UNICODE globally, which causes Catch2's
catch_main.cpp to provide wmain() instead of main(). This leads to
LNK2001 "unresolved external symbol main" for all test executables
when building with BUILD_TESTS=ON on MSVC.
Add DO_NOT_USE_WMAIN compile definition to Catch2WithMain target
so that Catch2 provides the standard main() entry point.
Co-authored-by: Claude Opus 4.6 (1M context) <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>
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.