enable_prime_tower stays on for any multi-filament project, but a belt
printer never prints the classic tower and the belt purge prism is an
ordinary object that never takes a brim, so every brim on a multi-filament
belt print was refused for nothing.
Raised in Hanif Koh's review of #14394.
validate() skipped the build-volume height check whenever the machine-frame
transform was active, which is every shipped belt profile, so a 400 mm
object passed on a 300 mm printable_height. The transform only changes how
the height is written to G-code; the clearance check from f682ab5cd3
applies regardless.
Raised in Hanif Koh's review of #14394.
Drops the [BELT-DEBUG], [BELTRACE], [BELT-CALIB] and [BELT-PREVIEW] log
lines, the SLIC3R_BELT_DIAGNOSTIC_LOG blocks, and the counters and
temporaries that existed only to feed them. Six of the purge tower lines
logged at warning level, which is Orca's default, on every plan. Raised in
Hanif Koh's review of #14394.
preslice_remap_*, preslice_remap_global and gcode_remap_* describe the
printer's kinematics and are set once by its profile. A wrong value sends
the gantry outside the machine (a user preset with the pre-slice remap in
place of the G-code remap emitted gantry moves to Y=646 mm), so they are no
longer offered in Expert mode.
On a belt printer a lift is a move along the belt axis (0.4 mm / sin 45 =
0.57 mm of belt travel out and back on every hop), not a lift away from the
part. The three belt printer bases now ship z_hop 0, the IR3 V2 leaf no
longer restates 0.4, and the BabyBelt Pro and IR3 V2 filaments stop
overriding the printer with filament_z_hop 0.4. The option stays editable.
PartPlate's containment tests treat the plate as open along Y on a belt printer with
belt_printer_infinite_y, so a long part is no longer flagged outside the plate in Prepare
while the slicer and the G-code checks accept it. The check reads the printer preset
through the app object, which does not exist headlessly, so it is guarded on the plater.
The prism's generator already sets no_brim; PrintObject::has_belt_brim() now also ignores
any brim setting on the prism (belt_purge_tower_object), so a brim on the parts beside it
never blocks purging.
ToolOrdering::has_wipe_tower() reads the first layer's flag. On a belt the first layer may
be a brim apron band, which carries neither object nor support and never gets the flag, so
with a brim the purge plan returned early and nothing was purged. Scan the layers for a
change.
With top_shell_layers = 0 the `top` vector is never filled and erasing its begin() was
undefined (found by fuzzing on a painted object dropped below the plate).
The per-filament island tour was cached by island centroids only. A later layer with the
same centroids but fewer islands (thin walls, negative volumes) reused the stale visit
list, whose catch-all index pointed past the layer's islands, and extrude_perimeters read
freed memory (three fuzz crashes, planar and belt). The per-instance island layout is part
of the cache key and the use site never indexes past the islands.
Found by fuzzing the headless slicer:
- A BBS-style 3MF without Metadata/project_settings.config segfaulted the CLI silently on
the missing printable_height option.
- A project saved with fewer filaments (or filament groups) than --load-filaments overran
the filament variant tables (segfault in the variant match) and then hit an uncaught
ConfigurationError from set_with_restore_2 (std::terminate). The tables are regenerated
for the filaments the project did not know about, the destination vectors grown first
(only from a non-empty source), the match bounded, and a failure becomes a CLI config
error.
A 3MF vertex with a nan/inf coordinate was accepted by both parsers and crashed qhull in
ModelVolume's convex hull while the file was still loading. Both vertex handlers refuse it,
and volume generation checks again whichever parser produced the geometry. The main
parser's _stop_object_xml_parser keeps a message a handler already set.
filament_minimal_purge_on_wipe_tower became variant-keyed upstream, which
invalidated the comment claiming none of full_config's keys were.
WipeTower2::extract_wipe_volumes indexes it by raw filament slot. Not reachable
with shipped presets; noted rather than worked around.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ComboBox::SetLabel is overridden and writes the text control when the text
control is shown, which it is on an editable combo. Clearing the label after a
pick therefore erased the name. Removed; the label is never written on this
control.
Suggestions are also rebuilt when the list opens. Built in add_row they filtered
against only the rows that already existed, so a row was offered names the rows
below it had taken.
Drops the 1px inset on the Primary label, which matched a frame the name field
no longer has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 commits. The count is large because texture displacement merged with its
whole branch history behind it, going back to July, alongside config and preset
file locking across instances, a foundation for configurable printer agent
connections, and a day of smaller fixes and CI work.
Two conflicts, both the same shape: each side had appended to a sorted list and
git could not choose an order. libslic3r's CMakeLists gained InstanceLock
alongside our IMEXHelpers and IMEXZones, and the preset bundle loading test
gained an include for ParallelResolve alongside ours for IMEXHelpers. Both sides
kept, alphabetical. No logic conflicted.
Verified: 740 targets build clean under -Werror, and the Release suite passes
1740 of 1740, up from 1665 -- the 75 new cases arrived with the merge and all
pass. That mattered more than usual here, since preset loading and config
locking are both areas the IMEX preset code touches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The name field becomes an editable ComboBox, the same pattern the sidebar uses
for parameters like sparse infill anchor length: predefined entries in a
drop-down, with the text still typeable. The mode table is authored for whatever
hardware the user has, so a closed list would be wrong -- nothing in the slicer
reads a mode's name except as the key a plate stores -- but the conventional
names are worth offering rather than leaving everyone to retype them.
What is offered follows the tool grid rather than a fixed list. Four carriages
get iq-copy and iq-mirror; multicolor needs a Span partner beside the primary
and a second gantry to copy the pair onto, so mc-copy and mc-mirror appear only
on a grid that can hold one. imex_resolve_routing() already refuses a multicolor
mode with no Span on the primary's gantry, and suggesting a name it would then
reject is worse than not suggesting it. Names a row already uses are dropped, so
the list only ever offers what is still free.
Two things about ComboBox matter when it is editable, which nothing else in the
tree does -- the other 78 call sites all pass wxCB_READONLY:
GetValue() returns the drop-down selection whenever there is one, so a name
typed after picking a suggestion would be silently discarded. Every read goes
through GetTextCtrl() instead. Field.cpp reconciles the same way for its own
open enums.
The constructor hands its value to TextInput as the LABEL -- the small
right-aligned slot a unit like "mm" occupies -- because a read-only combo hides
the text control and shows the label in its place. Left there, the name rendered
as a greyed echo beside the hint while the field itself sat empty. The combo is
built empty and the value written to the text control, and the selection handler
clears the label again afterwards, since SetSelection() writes there too.
The mode column widens to 176 to leave room for the drop-down arrow, with the
header spacer deriving from the same constant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Felix14-v2's review of OrcaSlicer#16019 found the tab behaving as a world of its own.
- Icons: the design_* glyphs were drawn in a fixed light grey, made for the dark ribbon, and the
toolbar re-tinted some of them by rebuilding the bitmap from a wxImage, which drops the HiDPI
scale factor Orca sets on Windows: at 150 % the icons came out half again too large for buttons
that were sized in raw pixels, overlapping and clipped. The glyphs now use Orca's sidebar icon
grey (#949494), which the icon cache maps per theme, nothing is re-tinted, toolbar glyphs drawn
for Prepare's light toolbar use their "_dark" twin, and every size is in DIP.
- Theme: the chrome colours were read once, at construction, and nothing in the tab answered a
theme switch, so switching left light surfaces and unreadable text in a dark tab and the other
way round. The colours are now {light, dark} token pairs; MainFrame::on_sys_color_changed
reaches DesignPanel::on_sys_color_changed, which moves every token colour onto the other
theme's, runs the app's dark pass and re-rasterises the icons. Card borders are StateColors,
resolved at paint time.
- Scale: MainFrame::on_dpi_changed reaches DesignPanel::msw_rescale, which re-rasterises every
icon (buttons, flyout rows, card headers, the tree's image list, now sized from its bitmaps)
and re-measures the Orca widgets.
- Mouse: the canvas no longer forces middle-drag to orbit and right-drag to pan; it reads the
drag actions in Preferences > Control like Prepare. Left-drag is shared with picking, so the
whole-body rubber band takes Shift+left-drag while left-drag is given to the camera.
- FPS counter: ImGui's display size is shared and only refreshed when a canvas sees its own
size change; the Design canvas now re-announces its size when the tab is entered (and the
editor canvas when it is left), as Plater does between Prepare and Preview.
- Viewport text: the status line and the tool readout were top-level popups over GL. A popup
does not follow its frame, so the empty-canvas hint floated over other applications, and the
readout was never taken down with the tab. Both are drawn by the canvas in the tool's ImGui
pass now, with the theme's overlay style.
- Dialogs: messages use MessageDialog/RichMessageDialog; Add/Edit Variable is one Orca dialog
with Name and Expression fields instead of two native text prompts; the Text dialog uses
TextInput, ComboBox and CheckBox (its height is a TextInput: SpinInput is integer-only), and
enumerates the installed fonts once per session. The ribbon's Confirm/Cancel, the reference
pick buttons and the expression buttons are Orca Buttons; the variable actions are icon
buttons like the other cards'.
- Undo: the tab's own Undo/Redo buttons are gone. The top bar's Undo/Redo drive the Design
history while the tab is shown, greyed to what an undo would do, as Ctrl+Z and Edit already
did.
- The first build of the tab logs how long each phase took ("Design tab build: ..."): it is
under a second here but was reported at about fifteen on Windows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
* Lock Config and Preset Files Across Instances and Write Them Atomically
Every running instance shares one OrcaSlicer.conf and one user preset
tree, and nothing kept their writers apart. Two instances saving at the
same moment, or the cloud preset sync thread writing while the GUI thread
saved, could interleave, and a reader in another instance could open a
preset JSON or .info file between truncate and close and get a partial
file, dropping that preset for the session with a parse error.
Add InstanceLock, a scoped guard that serialises the threads of one
process through a recursive mutex and other processes through an advisory
OS file lock: flock on POSIX, held on the guard's own descriptor so no
other close in the process can drop it, and LockFileEx on Windows. The
outermost guard opens the lock file and closes it on release, so nothing
stays open between saves and a data dir can be removed once nothing is
saving into it; the file itself is kept, since deleting it would let a
third instance lock a fresh file while the second still holds the old
one. It is best effort: when the lock file cannot be opened or locked, or
another instance still holds it after a second, the guard logs once and
lets the write proceed, then leaves the file alone for ten seconds, so a
hung instance never blocks every other one and a holder stuck in a
debugger does not cost a stall per save. The guard sits at the leaf
readers and writers: set_sync_info_and_save() calls save_info() under the
preset collection mutex, so a batch lock around save_user_presets() would
invert the order against the sync thread. The user preset scan reads its
files on worker threads without the guard, since the mutex would
serialise them, and takes it per file in the serial commit step, so a
save never waits for the whole scan. Each read keeps the bytes of the
preset and its .info as they were before parsing; commit compares them
with the disk under the guard and reads a file that changed again, so it
never deletes or writes back over another instance's newer save, nor
installs a .json and .info from two different saves; a preset another
instance removed in the meantime is not installed. Without the guard, in
a cool-down, the scan still loads the presets but leaves their files
alone: an unreadable file stays for the next scan, and a derived
compatible printer is not written back. Read-only scans, which is what
the CLI does, take no lock and create no lock file.
AppConfig holds OrcaSlicer.conf.lock in load() and save(); load is
included because the Windows path restores from the .bak copy. Every
user preset writer and reader holds user.lock: Preset::save(), which
writes no .info when the preset itself could not be written, since an
.info without its preset reads as a cloud deletion request, save_info(),
reload() and remove_files(), each preset the scan commits, the
bundle metadata reads and write, the .info removal after a cloud-confirmed
delete, the orphaned-.info scan on the sync thread, the bundle folder
removal on unsubscribe and the physical printer writers and delete
paths. A bundle import extracts under cache/ into a folder per process
and per import, where no scan reads.
Preset JSON, .info, bundle metadata, physical printer and config files,
and the caches and state files that already used a temporary by hand,
now go through write_file_atomically(), which writes <file>.<pid>.<n>.tmp
beside the target and renames it over, so a reader that never waits sees
a complete old or new file. A symlink is followed; a target that is not
a regular file is written in place; and when no temporary can be created
beside an existing target, or the rename itself is refused, by a Windows
reader holding the file open or a mount that cannot replace in one step,
the helper writes in place as before, since losing the save is worse
than a torn read. On POSIX the rename replaces the
target atomically where the old code removed it first and left a window
with no file at all; only a mount that refuses a one-step replace gets
the old remove-then-rename. A crash between temporary and rename leaves
the temporary behind, which no scan reads. Preset::save() returns
whether it wrote the preset, so the scan counts a compatible printer it
could not write back as an error. A failed config write keeps
the config dirty, and the idle handler waits ten seconds before retrying
while an explicit save always tries.
* Run the Cross-Process Lock Test on Every Platform
The test that checks the guard yields to a lock held elsewhere forked a
child to hold it, so it was left out on Windows. The OS lock belongs to
the handle on Windows and to the open file description elsewhere, so a
second handle in the same process is refused like another instance
would be. The test now holds the lock that way and runs everywhere.
extrude_infill() and extrude_support() reversed the layer's extrusion
entities in place while chaining them, so each export started from the
previous one's reversed toolpaths, and each copy of an object from the
copy before it. The export-time region lists now hold const pointers,
and chaining reverses a clone instead.
The display-name tests call set_user_session(), which persists the session.
The agent they built was in keychain mode, so every run saved a fake
OrcaSlicer/Auth session into the system keychain of whoever ran the tests,
replacing their real Orca Cloud login on any desktop with a working keychain.
Move them next to the other agent tests and build the agent the same way:
encrypted-file mode with a throwaway config directory.
An ironing line spacing of 0 reached the fillers from a 3MF, the CLI or
the per-filament override, which has no GUI guard. Concentric ironing
then never finished slicing, because a zero inset never shrinks the
region, and rectilinear ironing was silently dropped. Tiny positive
values produced an unprintable number of lines.
Top surface and support ironing now clamp the spacing to the 0.05 mm
floor the process GUI guard already enforces, so these configurations
iron at that spacing. Spacings at or above the floor, including every
shipped profile, are unchanged. The concentric filler also returns early
on a non-positive step so no other caller can hang it, and the filament
settings page now resets a too-small override the same way the process
page does.
simplify_path, simplify_multi_path and simplify_loop each copied the whole
PrintConfig, 10520 bytes with around 200 heap owning members, once per extrusion
entity, from inside the parallel_for over all layers. LayerRegion.cpp already
binds a reference a few hundred lines above. Three more sites in Layer.cpp do the
same thing.
The tree support changes in the same area: draw_circles() dropped layers from an
owning vector without deleting them, so every re-slice leaked the layers above
the topmost overhang. std::stable_partition rather than std::remove_if, because
remove_if leaves moved-from duplicates of kept pointers in the tail and deleting
those would be a double free. The collision and avoidance accessors returned
ExPolygons by value out of a cache that hands them out by reference, several
times per node per layer. The preview cache is released when the support step
finishes instead of at the start of the next slice; nothing reads it in between.
set_shared_object() now releases the layers the object still owns before it
starts aliasing another object's. Both clear functions are no-ops once the alias
is set, so this cannot free the sharee's layers.
PrintObjectSlice.cpp: eps was a function local static inside a parallel_for, so
slice_closing_radius was latched once per process. This one changes output for
projects that override it per object, or on a re-slice after editing it. The old
behaviour was undefined: whichever object won the race set it for the session.
Stores a painted cube, points its component back at the object that holds it and
expects the load to fail. Without the bound the test does not finish: the work
list grows until the process is killed.
# Description
With Xcode 27, building deps on macOS 26 fails at the Python install
step with a segfault, and a libpython built with Xcode 27 crashes on
macOS 12–26 the first time anything calls `os.pipe()`, which every
plugin `subprocess` call does. The macOS 27 SDK declares `pipe2()` and
`dup3()` as macOS 27-only, and CPython 3.12 calls them without a runtime
check once configure finds them, so on older systems they resolve to
NULL. This keeps CPython on the `pipe()`/`dup2()` fallbacks it already
uses with older SDKs; upstream fixed it in 3.13+
([python/cpython#153711](https://github.com/python/cpython/issues/153711))
but not in 3.12.
No change for builds with Xcode 26 or older, or on Linux and Windows.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
Rebuilt deps with Xcode 27 on macOS 26.6: the Python install step now
completes, the installed libpython no longer imports `pipe2`/`dup3`, and
CPython's `test_os`, `test_subprocess` and `test_posix` pass, apart from
one test that needs `_testcapi`, which `--disable-test-modules` leaves
out. Running CPython's configure against the macOS 26.5 and 27.0 SDKs
gives a byte-identical `pyconfig.h` for 26.5 with and without this
change, and for 27.0 with it. The x86_64 cross-build path was configured
on arm64 to confirm both of its configure runs pick up the change.
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
_generate_current_object_list expands component references through a work list
with no bound. An object whose component points back at itself, or a pair that
point at each other, makes the list grow until the process runs out of memory:
a few hundred bytes of XML take the slicer past 20 GB of resident size.
Bound the expansion by the number of objects in the file. A reference chain
longer than that has to revisit an object, so this rejects every cycle and no
acyclic file, however deeply nested. A second bound on the number of expanded
components stops an acyclic graph that fans out exponentially.
The plugin test fixtures start the interpreter before the test points
data_dir at its temporary directory, so PythonInterpreter creates
{data_dir}/python/packages and {data_dir}/log with an empty data_dir: a
python/ and log/ folder in whatever directory the tests run from. When that
is the test binary's folder, the next run's embedded-interpreter tests took
the stray python/ as their home and failed to start Python.
Give each fixture that initializes the plugin manager its own temporary
data directory, set up before initialize(), and only use the python/ folder
next to the test binary as the interpreter's home when it holds a standard
library.
With stealth mode off the home page asks for the login status every 2 s,
and while nobody is logged in that ends in clear_user_secret(), which
opened the system keychain and deleted the OrcaSlicer/Auth entry on the
UI thread every tick. A working keychain cost a D-Bus round trip per tick;
a keychain that never answers blocked the UI for 25 s per tick. It also
deleted a login another running instance had just saved, and ignored
use_encrypted_token_file, so opting out of the keychain did not help.
Remember whether this process read a secret from the store or wrote one,
and only then touch the store when a logged-out poll asks for a logout.
A logged-out instance never touches the keychain, and in encrypted-file
mode the poll no longer opens the keychain at all. A secret this process
cannot read, such as a token file encrypted for another OS user sharing
the data directory, is left alone by the poll. An explicit logout still
wipes both backends, so a token stranded by switching the token storage
option cannot sign the account back in later.
# Description
Include variant-aware validation, temperature and pressure controls,
named nozzle selection, and filament sidebar refresh.
## Support nozzle-specific filament cooling and tuning
Filament presets can require different cooling and tuning for Standard
and High Flow nozzle variants. This change makes part and auxiliary
cooling, pressure advance, temperature limits, and multitool ramming
settings follow the selected variant, with matching UI controls and
profile validation.
The Snapmaker U1 profiles in #15755 / Snorca illustrate why this
matters:
| Profile | Standard cooling | High Flow cooling |
|---|---|---|
| Snapmaker ABS | Part fan: 15–15% | Part fan: 10–60% |
| Snapmaker PETG HF, 0.4 mm | Part fan: 20–40% | Part fan: 30–60% |
| Snapmaker PETG-CF | Part fan: 0–20%; auxiliary: 0% | Part fan: 5–40%;
auxiliary: 20% |
| Snapmaker PLA Matte | Auxiliary: 80% | Auxiliary: 100% |
These differences need to survive variant selection, editing, and
slicing. Shared single values continue to apply across variants, and
short filament arrays fall back to element zero, matching the loader.
### Additional changes
- Preserve named nozzle selections when refreshing the diameter
selector.
- Enable or disable the pressure-advance input for the selected variant.
- Initialize profile-validation slicing with the printer’s declared
nozzle volume type.
- Refresh filament controls after preset loading.
Profile changes remain separate in `u1-hf`.
### Validation
- All 13 focused variant-tool tests passed.
- Broader checks encountered an existing Windows path-separator
assertion failure, reproduced with upstream code.
- C++ build and GUI validation have not been run.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
## Summary
Adds a new paint-style **Texture Displacement** gizmo that stamps
grayscale
height-map textures onto a model's surface and turns them into real
relief -
engraved or embossed detail - either as a live preview or baked into
actual mesh
geometry.
You paint where a texture applies, stack up to **8 blended texture
layers**
(image-editor semantics: Add / Subtract / Multiply / Divide), choose how
each is
projected onto the surface (Triplanar / Cylindrical / Spherical / LSCM
unwrap /
From-view), and - for the LSCM projection - lay the charts out by hand
in a new
dockable 2D **UV Editor** pane. Coarse models can be **Subdivided** or
**Remeshed** first so there are enough vertices to carry fine detail,
and the
result is committed with **Bake**, restricted to the painted area only.
The tool only ever affects the **painted** region; everything left
unpainted
keeps its original surface, and bake blends the relief seamlessly into
it with no
remeshing or hole-filling at the seam.
---
## User-facing features
- **Paint the affected area** with Brush (circle/sphere), single-Face,
or
Connected-area flood fill; or **Select whole model** - reusing the
existing
`TriangleSelector` / `FacetsAnnotation` painting machinery, one full
mask per
layer slot.
- **Up to 8 texture layers**, each with its own paint mask, texture, and
parameters, combined in slot order like image layers.
- **Per-layer controls:** Depth, Tile size, Rotation, Midlevel
(bidirectional
emboss/engrave), Smoothing, Edge smoothing, Invert, Blend mode, Tile
(Repeat / Mirrored-repeat / decal), and Projection.
- **Projection methods:** Triplanar (blended, seam-free across sharp
edges),
Cylindrical, Spherical, **Unwrap (LSCM)** - a real CGAL conformal
parameterization - and **From view** (slide-projector decals).
- **UV Editor pane** for LSCM layers: move / rotate / scale / snap / cut
/ join
islands, average texel density, Checker and Distortion overlays, manual
mark-
seam workflow, live model update while dragging.
- **View modes:** Normal (true displaced geometry = what Bake produces),
Fast
(GPU bump-shaded approximation of the active layer), Checker, Distortion
heatmap, and an independent Wireframe toggle.
- **Mesh prep:** Subdivide (1–5* uniform 1->4 split) with cyan-wireframe
preview,
and CGAL isotropic **Remesh** to a target edge length.
- **Texture library:** 10 shipped seamless 512×512 grayscale height
maps, plus
import of any PNG/JPG/BMP (converted to an 8-bit grayscale map and
copied into
the user data dir so app updates can't clobber it).
- **Bake** runs in the background (off the UI thread); the preview is
free to
explore and only Bake changes the real mesh.
---
## How it works (implementation)
- **Bake is accumulate-then-displace and topology-preserving.** The
output mesh
has exactly the input's vertices and triangles in the same order; only
displaced vertex positions differ. Each layer is evaluated against the
**base
mesh** and folded per-vertex into a shared accumulator via its blend
mode, then
every touched vertex moves once along its precomputed undisplaced
normal. This
replaced an earlier sequential re-mesh-per-layer design that was the
root cause
of the "second layer never applies" bug and made blend modes impossible.
- **Boundary vertices are pinned.** Any vertex shared with an unpainted
triangle
is never displaced, which is what keeps bakes seamless with zero
remeshing.
- **LSCM unwrap** uses a new `MeshBoolean::cgal::parameterize_lscm()`
built on
CGAL's already-vendored `Surface_mesh_parameterization` package - **no
new
external dependency**.
- **Fast GPU preview** perturbs the shading normal from the height
gradient
(analytic mm-per-mm slope for triplanar; Mikkelsen's
screen-space-derivative
method for the conformal LSCM path), so apparent depth matches the bake.
- **Background jobs:** preview compute and bake both run off the UI
thread on the
shared job worker (queued, not `replace_job`, so a preview never cancels
an
in-flight bake); a generation counter discards stale results.
- **UV Editor** shares the app's single real `wxGLContext` and reuses
the
registered `flat`/`flat_texture` shaders; island drags update one affine
matrix
per island rather than re-uploading geometry.
---
## Backward compatibility & constraints
- **Feature is fully gated behind the new gizmo** - it adds no new
default
behavior and does not touch existing slicing, profiles, or defaults.
Models
that never open the tool are unaffected.
- **Cross-platform** - pure `libslic3r` / `libslic3r_gui` /
`libslic3r_cgal`
code; no new dependency and no `deps/` rebuild. (Built and tested on
Windows;
no platform-specific APIs introduced.)
- **`.3mf` compatibility:** **baked** relief round-trips fine, since it
becomes
ordinary mesh geometry via the existing serialization path. **Unbaked**
paint
masks and layer definitions are **not yet serialized** - see
Limitations. No
existing project data is affected.
---
## Testing
Unit tests in `tests/libslic3r/test_texture_displacement.cpp` - **run
and
passing** (7 cases, 116 assertions). Coverage:
- `decode_height_texture` round-trip
- Empty-layer no-op
- Full-cube uniform displacement
- Boundary-vertex pinning on a hand-built fan mesh
- Regression: a **second layer over the same area actually contributes**
(the
bug the bake rewrite fixed)
- Table-driven check of all four blend modes
- The lowest painted layer ignoring its blend mode
`BUILD_TESTS` is `OFF` in the checked-in cache; enable to run:
```bash
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build --config Release --target libslic3r_tests -- -m
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand
```
---
Building deps with Xcode 27 on macOS 26 failed at the Python install step,
and a libpython built with Xcode 27 segfaulted on macOS 12-26 whenever a
plugin started a subprocess.
Community work from Rob Niccum, adding 0.04mm Ultra Fine and 0.08mm Extra Fine
for the 0.4 nozzle, a toolchange prime on the three IQEX machines, and
recompressed cover art and build plate model that are byte-identical in content.
Bundle version bumped to 02.04.00.13 here rather than in his branch, so the two
of us were not editing the same line while his review was open. Without the bump
the updater refuses the bundle outright, and nothing in CI catches that.
Verified rather than assumed, since his own validator predates the parallel
printing code and the fine tiers changed shape during review: the full profile
check passes all five stages tree-wide, and both new tiers slice clean on all
four 0.4 machines, 8 of 8. His last revision dropped a bottom_shell_thickness
override that was making those tiers thinner against our common process rather
than thicker against his; the emitted G-code now carries the inherited 1.0,
which is what that fix was for.
Co-Authored-By: Rob Niccum <klober81@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven commits, including a typed-config G-code export speedup, a printer agent
refactor that generalizes the infrastructure beyond Bambu, gyroid optimization,
and three CLI crash fixes.
One conflict, in PrintConfig.cpp. Upstream introduced a new_def macro and began
migrating the placeholder table onto it, adding curr_bed_type that way in the
same block where this branch had added imex_mode, imex_mode_index and
imex_mode_gcode in the older def = this->add(...) form. Both sides are kept and
ours are converted to the macro, which expands to the same three statements and
wraps label and tooltip in L() exactly as before, so nothing changes about what
is registered or what is translatable.
Note for anyone building this branch: the agent refactor adds a dependency,
LibDataChannel, so the deps tree needs dep_DataChannel built before the app will
configure. A distribution package of the same name will be found first if one is
installed, and the resulting error names a missing RelWithDebInfo location
rather than the wrong package, so point LibDataChannel_DIR at the dependency
prefix if that happens.
Verified: 789 targets build clean under -Werror, and the Release suite passes
1665 of 1665, up from 1630 before the merge -- the 35 new cases arrived with it
and all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the interface notes on the IDEX/IQEX modes editor.
Add Mode moves from below the rows to the top of the panel, beside a "?" button
that now carries the overview text as its tooltip. At the bottom the button
shifted down the page every time a mode was added, so where it sat depended on
how many modes already existed. It is an Orca Button in the Confirm style, width
matched to the mode column it creates a row in, and the panel opens on the
legend rather than on a paragraph.
Remove moves out of the right-hand column, where it sat one icon away from
Edit -- a destructive control beside the one pressed most -- to under the name
field it deletes, and its icon becomes a boxed minus rather than an X, which
read as "close". Reset joins Edit in the right-hand column, which is now
top-aligned so the icons hold position regardless of row height. Tool tiles are
square at 24px, and the header spacer tracks that width so the column titles
stay over their columns when the grid changes shape.
The two text fields were landing on GTK's near-black default border, invisible
against the panel: measured 45,45,49 against a 43,43,43 background, where the
settings fields above use 74,74,81. wxTextCtrl cannot color its own border, so
each sits in a one pixel frame taking the color TextInput derives for the
theme, and carries wxBORDER_NONE so Windows and macOS do not draw a native edge
inside it. The G-code boxes also take the monospace face EditGCodeDialog uses.
Bed zone fills drop to roughly half opacity in the Standard theme. They cover
whole quadrants for a whole session, so at swatch saturation they dominate the
scene. The collision strip is dimmed less, since it marks where a head hits
something. The deuteranopia, tritanopia and high contrast themes keep their
alphas: those are chosen for discriminability, which is the opposite trade.
Also fixes the icon size never applying. All four ScalableButton call sites
passed eight arguments, so the size bound to use_default_disabled_bitmap and
bmp_px_cnt kept its default of 16. Both are passed now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fan speeds, the recommended nozzle temperature range, minimal purge and
multi-tool ramming flow now take one value per extruder variant. Each
single value is repeated for every variant the filament lists, so every
preset loads exactly what it did before.
set_filament_pressure_advance's declaration said -1 omits the tool qualifier,
without qualification. That holds on Klipper, Marlin and BBL but not on
RepRapFirmware, where no-tool keeps the historical `D0`: a bare M572 applies to
whichever tool is selected and errors when none is, so omitting the qualifier
would make pressure advance depend on tool-selection state for every RRF user,
none of whom are using IMEX. The behavior is deliberate and unchanged; only the
declaration overstated it. GCodeWriter::set_pressure_advance and the index-space
notes in IMEXHelpers.hpp already described it correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In a parallel mode no tool changes occur, so two loops address each active
carriage explicitly: pressure advance before the print, and the second-layer
drop off the initial-layer temperature. Neither had coverage, and both fail
silently -- a carriage missing from one emits nothing at all, so it holds the
initial-layer temperature for the whole job, or runs on whatever pressure
advance the firmware was last given. The new case pins that the carriages
addressed are exactly the ones the mode declares active, each with the values of
the slot physical_extruder_map routes its head to.
The test needs filament_self_index, set here on imex_7x4_printer() so the whole
file has it. Production authors that key 1..n; its all-1s default collapses
every per-filament vector to filament 1's value through get_config_index_base(),
which leaves a per-slot assertion comparing a value against itself.
Also bounds the second-layer loop on filament_diameter alone. The bound belongs
in slot space, and filament_diameter is the one per-filament vector never
expanded per variant; the previous min() against nozzle_temperature mixed the
two index spaces without changing the result. The pressure advance loop already
bounds this way, so the two now read alike.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>