The bed gravity arrow, volume rendering and the painter/support gizmos each rebuilt
the tilt up-vector from build_plate_tilt_x/y; use one helper that also tolerates
presets without the keys.
Print::has_wipe_tower() is always false for belt printers, but CLI arrange, plate checks and the pre-slice tower clamp still reserved a phantom tower footprint and wrote a clamped wipe_tower_x/y into the config.
Extension layers were all created with id 0, so every one of them could be taken for the first layer by id-only checks (ooze-prevention standby temperature, cached layer ids). Renumber the support layers after inserting them.
gcode_back_transform, first_layer_plane* and belt_printer_infinite_y fell through to invalidate_all_steps(), which re-ran tool ordering, skirt/brim and G-code export on toggles that only affect G-code export.
Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange.
The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged.
toggle_options() now runs on every value change and mode switch; rebuild the
brim_type choices only when the leading-edge entry has to be added or removed.
Every printer's config block lists belt_slice_rotation_angle (default 45), so the processor marked all G-code as belt G-code: imported flat G-code got the belt view on a belt printer, and the belt-only Z handling in the processor ran for non-belt prints whose config block precedes the body. Take the angle only from outside the config block, where only the belt header writes it.
A 90 degree tilt has no finite gravity drift per layer, so the option range
now stops at 89 degrees, matching the cap applied by the support generators.
The three support generators each computed lh * tan(tilt), which overflows
coord_t at 90 degrees and flips sign beyond it (belt sync can write up to
180). One helper now returns the tilt slope with the tilt capped at 89 degrees.
Painted support/seam facets, support volumes, seam occlusion, MMU and fuzzy skin painting (top/bottom
and side facets) and the adaptive infill octree used trafo_centered(), or trafo() with a centre-offset
shift, while the layers were sliced with the belt rotation, remap and Z lift; they now share
PrintObject::trafo_sliced().
The belt writer replaced the plate-offset-carrying writer mid-export, so belt G-code for any plate but the first kept the plate origin and long-travel clipping used the wrong frame. GCode now remembers the offset and hands it to the new writer, and the writer's first-layer probes use the plate-local point it emits.
build_plate_tilt_x/y are printer-preset keys; listing them in the per-object
frequent-settings and object-table bundles stored ignored values in object
configs and crashed the object table on the process config lookup.
Merge origin/main (00429da739) into belt-printer.
Conflicts resolved:
- src/CMakeLists.txt: keep both wxInspector workarounds.
- GCodeProcessor.cpp: keep the belt compare_pos / z_for_height lines.
- PrintObjectSlice.cpp: the belt bbox-Z guard also covers main's
printable_region_ids bookkeeping.
- TreeSupport.cpp: the belt-floor check runs before main's PendingNode
queueing.
- Tab.hpp: keep the belt fields, drop the removed upload description
fields.
- tests/libslic3r/CMakeLists.txt: keep both test files.
Also included:
- eSUN PLA belt presets declare their own filament_id (OFkrxQC4) and
scripts/filament_id_snapshot.json is regenerated, as main's filament_id
check requires.
- Custom.json version bumped to 02.04.00.05 so the belt entries reach
existing installs.
- Fix the ambiguous WithinRel call in the belt apron width test, which
otherwise breaks the fff_print build.
A valid mixed filament already slices the same on the CLI as in the GUI;
these are the places where the CLI still skipped a rule the GUI applies.
- Keep the prime tower when a mixed filament is used, even if every
--load-filaments preset is the same. A mixed filament swaps between its
components every layer, so turning the tower off left the swaps with
nothing to purge on.
- Leave a mixed slot's row and column of the flush matrix at zero when
--filament-colour triggers a recompute, as the GUI does; a mixed slot
never reaches a nozzle.
- Refuse a mixed slot that has no filament of its own. Feature filament
ids aimed at it were past the filament count, got reset to filament 1
and the model silently printed in one colour.
- Refuse a plate that uses a mixed filament whose components are
different filament types, the type half of the GUI's
Sidebar::has_broken_mixed_filament. Missing or out-of-range components
are already rejected for the whole project by validate().
get_extruders_under_cli gains an expand_mixed_slots flag so the gate
can see mixed slots rather than their components; existing callers
keep the expanded list.
Both refusals exit with the new CLI_MIXED_FILAMENT_INVALID (-69).
update_values_to_printer_extruders_for_multiple_filaments picks each
filament's value from the flattened (filament x variant) columns of every
per-filament variant option. When a column index fell past the end of the
option's values, it skipped that filament and left the zero the output
vector was created with.
The GUI always hands this function full columns, but the CLI does not:
- a CLI override of a single value, such as --nozzle-temperature=211 on a
four-filament project, came out as 211,0,0,0, so three filaments would
print at 0 C;
- loading fewer filament presets than the project has filaments left the
remaining filaments' columns missing, so filament_cooling_before_tower
came out as 10,10,0,0 and filament_ramming_volumetric_speed as -1,-1,0,0.
An out-of-range column now keeps the option's first value, the fallback
get_at() and the sibling gather step already use. The seven per-type copies
of the loop are replaced by that same gather_option_values helper, moved
above the function; it now takes its caller's name for its log lines. An
empty option, which has no first value, is given one registered default per
filament first; it used to be replaced with zeros.
On a partial load a filament whose preset was not loaded takes the first
filament's value rather than its own preset's, which the CLI does not load;
for the options seen in practice those agree.
* CLI: record command-line overrides in different_settings_to_system
Settings passed on the command line (--sparse-infill-density 25% ...) override
the loaded presets when m_extra_config is applied to m_print_config, but nothing
recorded them in different_settings_to_system. The exported project therefore
carried the new value with no mark that it was modified, and re-opening it in the
GUI reverted it to the system preset's value -- the same failure the preset-leaf
diff fixes for user presets, via a different source of override.
The key set comes from m_config, not m_extra_config. read_cli() puts only what the
user typed into m_config and setup() adds nothing but CLI-own defaults (none of
the keys run() materialises there is a preset option), whereas the CLI writes its
own values into m_extra_config (has_filament_switcher, filament_colour,
filament_map ...), which must not be reported as user overrides.
Values are snapshotted just before the apply and only keys the override actually
changed are recorded: a typed value equal to the loaded one modifies nothing, and
listing it would read as a spurious difference against what the GUI writes. Each
key lands in the column(s) whose preset type owns it -- process, every filament,
printer -- and a key already present is not duplicated. Keys no preset owns
(curr_bed_type, a project setting) land nowhere, as in the GUI.
Follow-up to #15595, split out at review.
* CLI: judge command-line overrides the way the value is read
Review follow-ups on the override recording:
- Lists were compared as whole serialized strings. read_cli() builds a fresh
one-entry list, so --nozzle-temperature 245 against 245,245,245 on a
three-filament project was recorded in every filament column although nothing
changed. Lists are now compared entry by entry with a missing entry read as the
first, as get_at() reads it (and as resize() pads).
- The log line fired for every changed key, including ones no preset owns
(curr_bed_type) and which therefore land in no column. It now fires only when a
column took the key.
- m_print_config.has(key) straight after apply(m_extra_config, true) was always
true, both configs sharing print_config_def; removed. columns.size() >= 2 also
always holds after the resize to filament_count + 2 -- different_settings_to_system
is not a CLI option, so nothing in between can shrink it -- but that rests on code
far away, so it stays a plain check rather than an assert: release builds compile
asserts out, and a _GLIBCXX_ASSERTIONS build would abort on columns[0].
Deliberately NOT done: comparing a key the loaded config lacks against its
built-in default. On reopen the GUI restores an unlisted key from the SYSTEM
preset, not the default. A 3MF written before an option existed leaves it absent
here, so --sparse-infill-density 20% (the default) against a Prusa system 15% would
go unrecorded and be reverted to 15%. Absent keys stay always-recorded:
over-recording is cosmetic, under-recording loses the value. Verified that such a
key really is absent at this point, rather than filled from the system preset.
Reported by HanifKoh and raistlin7447 in review of #15642.
* CLI: evaluate compatible_printers_condition in the compat checks
Slicing from the CLI with --load-settings exits with
CLI_PROCESS_NOT_COMPATIBLE (-17), "The selected printer is not compatible
with the process preset in the 3mf.", for process/printer pairs the GUI
accepts. Reproducible with stock, unmodified Prusa system profiles:
orca-slicer --datadir <datadir> \
--load-settings "<datadir>/system/Prusa/process/0.20mm SPEED @CORE One HF 0.4.json;<datadir>/system/Prusa/machine/Prusa CORE One HF 0.4 nozzle.json" \
--load-filaments "<datadir>/system/Prusa/filament/Prusament PETG @CORE One HF 0.4.json" \
--slice 0 --outputdir /tmp/out model.stl
The four compat checks in CLI::run did a literal name match against the
`compatible_printers` list only:
for (index ...) if (new_print_compatible_printers[index] == new_printer_system_name)
process_compatible = true;
Process profiles that declare compatibility through
`compatible_printers_condition` and leave `compatible_printers` empty are
therefore always reported incompatible -- the condition is never consulted.
For 0.20mm SPEED @CORE One HF 0.4 that condition is:
printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and
nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/
The GUI does not have this bug: is_compatible_with_printer() in Preset.cpp
treats an empty list as "no explicit constraint" and evaluates the
condition in that case.
Fix: replace the four loops with a check_compat lambda that calls
is_compatible_with_printer() -- the same helper the GUI uses -- wrapping
the already-loaded DynamicPrintConfigs in lightweight Preset /
PresetWithVendorProfile shells. The 3MF-embedded process/printer full
configs are kept in current_process_full_config /
current_printer_full_config so the condition can be evaluated for the
reprocess paths too; those fall back to the previous literal match when
the full config was not preserved.
Behaviour is unchanged where an explicit compatible_printers list exists:
is_compatible_with_printer() performs the same name match, and returns
true when both list and condition are empty, matching the existing
"old 3mf, no compatible printers, set to compatible" path.
Split out of #13731 (section 1) as a standalone, single-purpose change.
Orthogonal to the inherits-chain resolution work in #14718 / #15302 /
#15438; those decide which values a preset resolves to, this decides
whether the resulting pair is considered compatible.
* CLI: translate the 3MF's renamed compatibility keys before the compat check
The 3MF fallback fed the project config to is_compatible_with_printer() as-is,
but a project config does not carry compatible_printers or
compatible_printers_condition. PresetBundle::construct_full_config() erases both
and re-emits them as print_compatible_printers and
compatible_machine_expression_group; they are renamed back only on the
PresetBundle load path, which the CLI does not take. The check therefore saw no
list and no condition, read that as 'no constraint' and accepted every printer.
That is not just a wrong accept. An early true skips the !process_compatible
block that sets machine_switch, so the new printer is never appended to
print_compatible_printers and the exported 3MF stays marked compatible only with
the printer it came from -- which is exactly what that block exists to prevent.
Translate the two keys back before the check. Index 0 of the expression group is
the print preset; the group is filled print, filaments, printer.
Also note in the comment that profiles/BBL/{process,machine}_full/ are gitignored
and generated by nothing in-tree, so current_*_full_config is always empty and
this fallback is the only live path -- not the rare non-BBL case the original
comment implied.
Reported with measurements by HanifKoh in review of #15449.
Preset: add a config-level is_compatible_with_printer() overload
The CLI holds resolved DynamicPrintConfigs, not Presets, so it wrapped them in
throwaway Preset shells at the call site. Moving that into Preset.cpp puts the
compatibility policy -- including the documented fail-open on a malformed
compatible_printers_condition -- in one place for the GUI and the CLI, rather
than leaving a second copy of the plumbing in OrcaSlicer.cpp to drift.
Purely additive: neither existing overload changes, so no GUI behaviour moves.
Requested by HanifKoh in review of #15449.
(cherry picked from commit 14ca1972ef4d3c7d90935d159423013a40a6bd70)
* CLI: never overwrite a real compat key with an empty renamed one
7e7f0e3 translated compatible_machine_expression_group[0] into
compatible_printers_condition whenever the group vector was non-empty. A project
the CLI exported itself carries the real compatible_printers_condition AND an
all-empty group, ["", "", ""], so the valid condition was overwritten with
"", the check saw no constraint, and every printer was accepted.
That fixed GUI-shaped projects and broke CLI-shaped ones. Bisected across six
builds re-slicing one CLI-exported CORE One project with an MK4S: every build
before 7e7f0e3 gives 'compatible 0' and takes the machine-switch path; with it,
'compatible 1' and no switch.
The raw keys now win whenever they carry something; the renamed ones are only a
fallback, and an empty value is never written over a real one. Same for the list:
print_compatible_printers is used only when compatible_printers is absent or
empty and it itself is not.
Found by a peer session re-testing the installed build.
* CLI: record user overrides in different_settings_to_system for 3MF export
Three sites in CLI::run wrote an empty `different_settings_to_system` column
and left a //todo:
//todo: support user machine preset's different settings
different_settings[filament_count+1] = "";
//todo: support system process preset
different_settings[0] = "";
//todo: update different settings of filaments
different_settings[filament_index] = "";
So a 3MF exported by the CLI does not record which keys the user actually
overrode relative to the system parent. Re-opening such a project in the GUI
then shows spurious "unsaved changes", and accepting that dialog can revert
inherited process/filament/machine values to system defaults.
The column could not be filled before because the CLI had no resolved view of
the parent preset. It does now: #15438 builds a PresetBundle for inherits
resolution, so the parent can be looked up by name and diffed against the
resolved leaf. This adds no extra loading -- the bundle is the one already
built, and the helper returns "" whenever it is unavailable or the parent
cannot be found, which is the previous behaviour.
Preset metadata is filtered out of the diff: `inherits`, the three
`*_settings_id` keys, and `compatible_printers` / `compatible_prints` and
their `_condition` variants, which have their own tracking columns
(`inherits_group`, per-slot lists) and would otherwise double-count.
A value already carried by the loaded JSON still wins for the process slot, so
presets saved with a `different_settings_to_system` field behave as before;
the computed value only fills the gap where that field is absent, which is the
case for every user preset in my datadir (0 of 47 carry it).
System presets keep an empty column: there are no user overrides to record.
* CLI: diff the filament slot before load_default_gcodes_to_config
The process and machine slots compute their different_settings_to_system column
before load_default_gcodes_to_config(); the filament slot did it after. That
call materialises absent gcode keys via option(..., true), and
DynamicConfig::diff only compares keys present in both configs -- so a gcode key
the resolved leaf did not carry would go from 'not compared' to 'compared as
empty against the parent' and land in the column as an override the user never
made.
Hoisted into a local above the call, guarded by load_filament_count > 0 so the
work is skipped exactly where it was before, and assigned at the original site.
The diff now also runs before config.erase("filament_settings_id"), which is
immaterial: cli_different_settings already filters filament_settings_id along
with the other *_settings_id keys.
This is a consistency fix rather than a demonstrated defect -- resolve_preset
merges the parent config, so in practice the gcode keys are already present on
both sides and the diff is unaffected. It removes the dependence on that
invariant, which the other two slots never had.
Reported by HanifKoh in review of #15595.
A build without SLIC3R_PCH had never been tried on macOS. Three files
used what pchheader.hpp happened to include: LocalesUtils.cpp needs
<sstream> and <iomanip>, and the two dialogs need <wx/tooltip.h>. The
GTK port's headers and libstdc++ pull these in transitively, the Cocoa
port's headers and libc++ do not.
# Description
Each plate keeps a registry of the instances it holds
(`PartPlate::obj_to_instance_set`). The plate's filament list
(`get_extruders`), its wipe tower preview and position clamp, the object
list grouping and the saved project's per-plate instance list all read
it. Two paths left it stale:
* `Plater::increase_instances` (the `+` key / toolbar) adds the copy to
the model but never registers it with any plate.
* `GLCanvas3D::do_move` (drag release and arrow keys) ended with
`notify_instance_update(-1, 0)`, so only instance 0 of each selected
object was re-registered. Rotate, scale and mirror already notify every
instance.
So a copy created with `+` and dragged onto another plate stayed unknown
to that plate: the project saved afterwards listed it on no plate, and a
multi-filament copy moved onto a single-filament plate drew no wipe
tower there and never got its tower position clamped. The Print side
selects instances by geometry, so the plate still sliced, which is why
this went unnoticed.
This PR
* registers new copies with their plate at creation;
* has `do_move` notify exactly the instances it moved (every instance of
the object when a part was moved in Volume mode), rather than instance 0
or all instances - notifying an instance that stayed put invalidates its
plate's slice result, so `(-1, -1)` as used by rotate would have
un-sliced every plate holding a sibling copy;
* drops the registry entry again when `decrease_instances` removes a
copy.
A second commit finishes the switch #15532 started with
`contain_any_instance_totally()`: `get_extruders_without_support()`,
`check_single_extruder_mixed_filament_risk()` and
`check_compatible_of_nozzle_and_filament()` still tested instance 0
only, so an object whose copy - not its original - sits on the plate was
skipped by all three.
No new options, no format change. The `is_new` flag is deliberately not
passed for the copies: a copy landing on a spiral-vase plate gets the
same "apply spiral mode settings?" prompt a dragged instance gets,
instead of a silent rewrite of the object's settings.
# Screenshots/Recordings/Graphs
Before:
<img width="1920" height="1080" alt="05-moved"
src="https://github.com/user-attachments/assets/3cf9f5a9-1a4e-41e8-8c57-578f849d8c29"
/>
After:
<img width="1920" height="1080" alt="05-moved"
src="https://github.com/user-attachments/assets/1b801a7e-b7cd-4ffb-bd1d-b701f90dade6"
/>
## Tests
Re-run after the rebase, both binaries driven through the same headless
harness (Xvfb 1920x1080, llvmpipe) on the same fixture: `cubeA`
(filament 1) alone on plate 1, `cubeB` (a two-part object, filaments 2
and 1) alone on plate 2, so plate 1 shows no wipe tower at load. Select
the plate-2 object, press `+`, walk the copy onto plate 1 with 36 x Left
(10 mm per press, one `do_move` each), save, slice plate 1.
Before is main `8af92214d0` - i.e. with #15532's
`contain_any_instance_totally()` already in place, so the only
difference is this PR.
* **Before:** the saved `model_settings.config` lists plate 1 with
`cubeA` only and plate 2 with `cubeB` instance 0. The copy (instance 1)
is listed **on no plate at all**, and plate 1 draws no wipe tower even
though a two-filament object is sitting on it.
* **After:** plate 1 lists `cubeA` **and** `cubeB` instance 1; plate 2
still lists instance 0. The plate-1 tower preview appears, and slicing
plate 1 succeeds with the tower actually generated - the filament panel
reports 1.10 m / 0.48 m in its Tower column over 51 filament changes,
and the G-code carries `EXCLUDE_OBJECT_END NAME=cubeB.stl_id_1_copy_0`.
Same camera and fixture on both runs, so the screenshots above are
directly comparable.
* Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill
For patterns with curved/turning anchor lines (Hilbert Curve, Octagram
Spiral), the bridge_over_infill algorithm produced incorrect results:
1. determine_bridging_angle: sampling curved anchor orientations
produced noise across all turning directions (0/90/180/270°)
instead of a single dominant one, yielding unstable bridge angles
with 180° spread. Fix: use the configured infill_direction + 90°
directly, bypassing the noisy sampling. The old blind +0.25*PI
(Hilbert) and +1/16*PI (Octagram) offsets are removed.
2. construct_anchored_polygon: curved Hilbert/Octagram anchors
intersected each vertical scan line many times at wildly different
Y positions, producing chaotic polygon sections — holes in random
places, bridges over air, rotated bridges. Fix: replace the curved
infill polylines with synthetic straight lines parallel to
infill_direction, spaced at the real infill line spacing
(flow_spacing / density). Lines are centered on the limiting_area
bbox center so that after rotation they span the full bridged_area.
Anchors are left at full bbox length (not clipped) to guarantee
every scan line finds an anchor.
Rectilinear and other straight-line patterns are unaffected.
Known limitation: some bridge edges may still terminate over air in
edge cases where the nearest synthetic anchor line is more than one
infill spacing away from the bridge boundary. This will be addressed
in a follow-up.
* fix: anchor internal bridges to actual sparse infill
Preserve real anchors across regions and align plane-path anchor origins with printed infill. Respect lower-layer rotation templates and model alignment, and sample curved bridge boundaries more finely.
Add regression coverage for anchor alignment, bridge angles and region isolation, with Orca comments explaining the geometry constraints. Verified 175 FFF tests before the comment-only follow-up; preserve CRLF in modified files.
* Fix internal bridge support contacts and separated infill origins
Restore anchor contact after bridge smoothing and share per-body pattern origins between anchors and printed infill. Recompute origins when preparation settings change.
Cover multiline counts 1, 2 and 3 and add regressions for printed bridge support, separated infill alignment and reslicing.
* Add explicit standard headers to PrintObject tests
* test: cover surface centering when infill settings change
Verify top and bottom Archimedean Chords and Octagram Spiral paths after switching centering modes or toggling separated infills. Compare reslicing against fresh slicing and document dependent infill invalidation.
* test: preserve directional surface infill when settings change
* perf: index layer islands for connected-body detection
* test: use public print pipeline for body centering checks
* build: clear 2 warnings - cast the NSTextField the class check already proved
mainframe_text_field is NSTextField* and was assigned a bare NSView*, which
Clang reports as -Wincompatible-pointer-types. Both assignments sit inside
if ([viewObject class] == [NSTextField self]), so the runtime type is already
guaranteed, and the line above the second one casts the same variable the same
way to call setTextColor. macOS only, since nothing else compiles this file.
* build: clear 6 warning categories from the clang-cl inventory
-Wmissing-braces (9). Aggregates whose first member is itself an aggregate.
GUID's fourth member is BYTE[8], so the trailing eight bytes take their own
braces. The others were reaching for zero-initialization with {0} and say {}
now. bbs_3mf's backup Task ends in an anonymous union, which needs braces of
its own; those braces initialize the union's first member rather than the one
named at the call site, so the RemoveBackup site says so in a comment.
-Wmacro-redefined (11). SendMultiMachinePage.hpp defines five names that
Preferences.hpp, PresetBundleDialog.hpp, ExportPresetBundleDialog.hpp and
TroubleshootDialog.hpp also define with different values, so the value in
force depended on include order. All nine of this file's DESIGN_ macros take
the SEND_ prefix it already uses for its own macros, values unchanged, so a
DESIGN_ name added elsewhere later cannot collide with it again. They read as
one page-local palette, a 900 to 400 gray ramp plus sizes, so the four with
no current readers stay: dropping them would leave gaps in a named scale. test_marchingsquares.cpp defines NOMINMAX,
which libslic3r already passes as a PUBLIC compile definition, so it takes
the #ifndef guard the other suites use.
-Wbraced-scalar-init (3). Two PushStyleVar calls resolve to the float
overload, so the braces were initializing a scalar. ConfigOptionFloatsNullable
already takes an initializer_list, so the inner braces did the same thing.
-Wmicrosoft-goto (2). Both gotos in copy_file_gui jump forward over the
initialization of size, dwRead and dwWrite, which only MSVC accepts. Those
declarations move up to join the others at the top of the function.
-Wunused-private-field (3). Every use of ColourPicker's m_clrData and
m_picker_widget is behind !defined(__linux__), so on Linux they are written
and never read; the members now carry the same guard. ParamsPanel's
m_size_move is read nowhere. Tab has its own, which is the one Tab.cpp uses.
-Wnonportable-include-path (2). BaseException.h asked for "stackwalker.h"
and the file on disk is StackWalker.h.