Three conflicts, all in the texture displacement gizmo, all resolved by
keeping both sides:
- Two independent additions landed next to apply_standard_mode_presets():
upstream's store_panel_state() and this branch's image-mode declarations.
- m_color_tex_source: upstream changed it from a raw const void* to a
weak_ptr to the image bytes, so a freed and reallocated image can no longer
pass for the old one. That is the better key and it is kept; the colour
correction key this branch added sits beside it, and the cache test now
checks the image, the smoothing and the correction.
The cloud upload sent only the keys that differ from the parent, so a
variant layout equal to the parent's was left out. On download the
loader sized every per-variant value by the missing layout, cut it to
one value and kept the parent's values for the other nozzles and nozzle
volume types. On dual-nozzle printers the right nozzle's settings
silently reverted to the parent's after a sync.
Upload the variant layout always, as the local save already does, and
read copies stored without it against the parent's layout when they
carry one value per parent variant.
Opened or drag-dropped G-code files ignored `; MANUAL_TOOL_CHANGE` comments, so the preview showed every move on the first filament. Only `apply_config(const PrintConfig&)` read `manual_filament_change`, and loading a file goes through `apply_config(const DynamicPrintConfig&)`. Read the option there too, initialize the flag to false, and add a regression test.
Debug builds asserted on startup because these calls raise GL errors on a core-profile context. Both draws already use shaders, so removing the calls changes nothing on compatibility contexts.
* Add CGAL-based mesh repair, drop Netfabb remnants
Add repair_mesh() in MeshRepair.cpp, a CGAL-based repair that replaces MeshBoolean::cgal::repair(). It welds cracks, cancels coincident triangles, fixes orientation, fills holes (including planar annular regions), removes local self-intersections, and unites crossing shells with libigl's exact boolean while following the nonzero winding rule used by slicing.
FixModelByCgal now repairs each volume in place without splitting it into parts, and reports whether the mesh was modified so unchanged meshes keep their painting and ids. The texture import repair prompt no longer depends on the Windows SDK, and the FIX_THROUGH_CGAL_ALWAYS conditionals are removed. The commented-out Netfabb/WIN10SDK build code is deleted.
libslic3r_cgal now builds with CGAL_LINKED_WITH_TBB and links TBB. Add docs/HLSD/mesh-repair.md and unit tests in test_mesh_repair.cpp.
* Parallelize repair while preserving separate shells
Repair connected mesh units and shells independently in parallel, keep crossing shells separate, and limit self-intersection remeshing to local defects. Update mesh-repair documentation and tests for the revised behavior.
* Make mesh repair deterministic and report its progress
Flags written from parallel loops were std::vector<bool>, whose neighbouring
bits share a word, so shells could be kept or dropped at random; they are now
std::vector<char>. Holes of up to 100 edges are filled with the search over all
triangles: the search restricted to a 3D Delaunay triangulation picks between
equally good fills differently from run to run.
Shells of fewer than four faces are dropped, as the volumes of two triangles
back to back need not cancel exactly, and a shell that self-intersection removal
changed is split again so the pieces it cut off are dropped too.
repair_mesh() takes a progress callback in place of the cancel callback, and
Fix Model reports each stage. The object's bounding box is now invalidated even
when a cancel or an error stops the repair after some volumes were replaced.
The edge-use sort, union-find and box-overlap code shared by several stages is
factored out, the topology analysed for welding is reused to build the work
units, the orientation pass checks only the edges it cut, and the output is
written from the shells without joining them first.
* Fix Split to parts crashing on flat pieces and staying offered after a repair
ModelVolume::split() added every piece to the object and then deleted the
pieces without a convex hull. When the first piece was flat, as often in a
triangle soup, that deleted the volume being split while split() and its
caller kept using it. A part made only of flat pieces lost all its volumes,
and a soup added thousands of pieces, saving a backup of the object for each
one it deleted. Pieces without a convex hull are now dropped before any joins
the object, the volume being split keeps the first remaining piece, and a part
with no solid piece is left whole. Other parts of the object without a convex
hull are no longer deleted as a side effect.
ModelVolume caches whether its mesh can be split, and set_mesh() kept the
cached value, so after Fix Model, Simplify or Emboss replaced a mesh, Split to
parts could still be offered for a single piece. set_mesh() and reset_mesh()
now clear the cache.
* Import STEP surface models with Split compound enabled
Splitting a compound kept only its solids, so a surface model, which OCCT
reads as a compound of loose faces, gave nothing to mesh and the import failed
with "Meshing of a model file failed or no valid shape". Each shell outside a
solid now becomes a part too, and loose faces stay together as one part, as
they do without splitting.
* Add Fix All for plates and re-enable --repair on the command line
ModelVolume::repair() applies repair_mesh() to a part and replaces the mesh
only when the repair changed it. Fix Model, the new Fix All and Fix All Plates
items of the plate context menu, and the command line's --repair option, which
had been commented out since the Win10 SDK repair was dropped, all use it.
With Keep painted feature after mesh change off, Fix Model cleared the painting
of every part of an object before repairing. Now only a part whose mesh changes
loses its painting, so fixing a whole project does not wipe it. The
notification counts the models that needed no repair instead of listing them as
repaired, and objects that did not change are not refreshed.
A filament_start_gcode list shorter than the filament count made the Bambu start-of-export path throw std::out_of_range. Use get_at() like the other filament_start_gcode reads, and add a regression test.
stapler fetches the notarization ticket from Apple's CloudKit server,
and a dropped connection there (NSURLError -1005, exit 68) failed the
whole macOS universal job twice on 2026-10-12, after notarization had
already been accepted. Rerunning the job passed, but repeats the full
build and both notarizations.
Wrap both stapler calls in the existing retry helper already used for
hdiutil in this step.
GLGizmosManager can be called while its gizmo list is empty, either at startup before the canvas has loaded the gizmos or after their init() failed. reset_all_states(), open_gizmo(), open_gizmo_by_shortcut() and activate_gizmo() now return early in that case. This fixes a Debug-build assert when a new project resets the gizmos at startup, and an out-of-bounds read in release builds when a gizmo shortcut key is pressed after gizmo init failed. activate_gizmo(Undefined) still returns true, since no gizmo is open. The reset_all_states() guard was first proposed in #16074 by kenrap.
Co-authored-by: kenrap <101818165+kenrap@users.noreply.github.com>
The reason recorded for CLI_NO_SUITABLE_OBJECTS (-50) assumed a 3mf being uploaded. The CLI also slices STL, OBJ and STEP input, so the reason now states what both -50 sites check: the plate is empty, or no object on it is both printable and fully inside the print volume. The exit code is unchanged.
Adds tests/cli/test_cli_no_suitable_objects.sh.
Pop the FramePadding style var before end() in the move and rotate gizmo windows. It was pushed inside the window and popped after End(), which failed ImGui's style stack check in Debug builds.
The CLI's arrange no longer rotates objects unless --allow-rotations is passed, which matches the GUI's default. STL, OBJ and STEP inputs are arranged even without --arrange, so a part that only fits the bed when rotated now exits with -50 unless --allow-rotations is given. The CLI prints that reason to stderr.
FanMover could split a move inside change_filament_gcode to place a delayed fan command. It computed the split point from the last position the G-code set, but on Bambu printers the firmware moves the head during the T command, so the inserted waypoint sent the head to a spot the template never visits (#15789).
Both toolchange paths, set_extruder() and the Type1 append_tcr(), now wrap the template output in ;_FAN_MOVER_SKIP_START/END tags when fan speed-up or kick-start is on. FanMover keeps fan commands inside them where they are and drops the tags from the output. multifilament_config() also sizes the per-filament options that the wipe tower reads by index.
This fixes the H2C start G-code template by commenting the two
`========== record data ==========` separator lines.
Those lines are currently emitted as raw text inside
`machine_start_gcode`. Since they are not comments or G-code commands,
they can make the generated start sequence invalid or firmware-dependent
when the template is used. The fix preserves the separators as comments.
Fixes#16218.
Validation:
- Parsed the modified JSON profile with Python `json.loads`.
- Checked both `record data` separator lines now start with `;` after
unescaping the G-code string.
# Description
Re-slicing no longer leaks layers in two places. Tree supports dropped
the support layers above the topmost overhang without freeing them. And
an object that had sliced itself, then became identical to another
object (for example a copy whose original lost a per-object setting or
modifier), leaked its own layers when it switched to sharing that
object's slices; it now frees them once it takes the shared layers over.
If slicing is cancelled before that point, they still leak, as before.
Tree support also stops copying its collision and avoidance areas on
every lookup, and releases its working cache once supports are generated
instead of keeping it until the next slice. Elephant-foot compensation
now uses each object's own slice closing radius, where the first object
sliced in a session used to fix the value for every later slice.
Slicing output changes only on layers with more than one region printed
with elephant-foot compensation and no raft, and only where an object's
slice closing radius differs from the value the session had fixed.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
Adds a regression test for an object that switches to sharing another
object's slices. The `fff_print` suite passes.
<!--
> 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)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Description
Fixes highlights in the texture displacement gizmo that stayed on screen
after the state behind them changed, or were drawn where they could not
be seen. Erase whole model, Select whole model and Capture current view
update the paint highlight at once instead of on the next right-click. A
stroke released outside the 3D view ends on the next mouse move, in
every paint gizmo, rather than leaving the brush stuck in its pressed
colour. The layer textures stop showing an image that has been replaced.
Selecting islands in the UV editor switches a Normal view to Fast, where
their highlight is visible instead of buried under the displaced relief,
and Normal comes back once no island is selected unless another view was
picked in the meantime.
No change to slicing output; the changes are confined to the paint
gizmos and the UV editor.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> 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)
The island highlight was hidden under the Normal view's relief. Normal comes back once no island is selected, unless another view was picked in the meantime.
The segment draw switched to an element buffer of 8-bit indices. GCN 1/2
GPUs cannot read 8-bit indices in hardware, so the driver converts them,
and on a Mac Pro 2013 FirePro under Windows Boot Camp the toolpaths come
out as scattered spikes. 16-bit indices are supported everywhere and keep
the eight shader runs per segment.
Erase whole model, Select whole model and Capture current view update the
highlight at once. A stroke released outside the 3D view ends on the next mouse
move, in every paint gizmo, instead of leaving the brush in its pressed colour.
The layer textures no longer show an image that was replaced.
* Wait for the PR's Deps Cache in clang-tidy When deps/ Changes
* Fail the clang-tidy Deps Wait When Listing Caches Keeps Failing
* Wait for Deps Through ci_deps_cache.sh in clang-tidy
Reuse the macOS deps wait for Linux: the job names, workflow and cache
ref are passed in, and a new 'own' mode waits for the PR's own build
when it changes deps/. A PR that keeps deps/ now waits for a running base
build with the same deps, and warns when it falls back to older ones. The
wait ends as soon as the deps job fails.
Every slider in the texture displacement panel can now be given an exact
number, either by Ctrl+clicking the track or via a small edit icon at
its right end. Both routes reuse ImGui's own temp-input mode.
Each slider also gets a tooltip with its description and its range, and
the panel's `?` popup lists both ways in.
Draft: description to be refined.
## General
Nothing about texture displacement was written to the `.3mf`, so a
painted relief did not survive closing the project.
Everything the user set up is saved now, so a project reopens with the
relief intact and still editable.
### What is saved
- The layer stack: every per-layer setting (depth, midlevel, smoothing,
tiling, projection blend mode, colour) and the per-volume options
- Each layer's texture, as a file in the archive (why? because user
might want to move .3mf to another computer where custom textures are
missing, or he saves .3mf then deletes textures, and returns back to
.3mf)
- The painted region, one mask per layer slot
- The UV work: LSCM seams, island placement, per-vertex overrides
- Editor state: the selected layer and Standard/Pro, so the panel
reopens where it was left
### What is not saved
- The preview mesh. It is a second copy of the model in the file, for
something the app rebuilds quickly on reopen.
- Panel docked/undocked, which goes to the application config instead
## Format
- `<volume id>.json` - the layer stack and the per-volume options
- `<volume id>_<slot>.png` - each layer's texture, referenced from the
JSON through `path_in_3mf`
Paint masks ride along as one per-triangle attribute per slot
(`paint_texture_0..7`), beside
the attributes the existing paint tools already write.
## Compatibility
Both ways:
- an older build ignores what it does not know and still opens the
project, losing only the relief
- a newer build finds nothing in an older project and leaves the volume
as today.
It sat below the Background section purely because of source order, which
read as if it were part of it. Reset moves out of both sections and becomes
Reset all, since it clears the corrections and the background key alike and
a reset living inside one of them would look like it only reached that far.
- The dropper has its own pipette icon instead of borrowing the placement
handle's, drawn in the house style: 16 px canvas, 0.4 stroke in #949494,
teal on the tip, which is the part that carries the meaning.
- It picks off the model as well as off the preview. A click on the 3D view
while it is armed reads the picture's own colour at that point, through
the same projection the bake uses, not the pixel on screen: the screen
pixel carries the lighting and the filament the colour was already
quantized to, so keying on it would remove the wrong thing. The patch
frame comes from extract_painted_patch() plus
texture_displacement_patch_frame(), so a cylindrical or spherical layer
reads at the same place the bake will.
- Placement, Color and Background fold away, with the triangle-and-text the
displacement panel's "More settings" link uses rather than ImGui's own
filled bar, which is louder than anything else in this window. Folded,
Background still says whether it is removing anything. Open by default and
remembered for the session only: this is panel state, not something a
project should carry.
- The body scrolls and the footer is pinned under it, the shape the
displacement panel already uses. With the panel this tall, Apply and Close
were the first things to go off the bottom of a short canvas.
Two colours was the N=2 case of a more useful control, so it is replaced by
a Colors slider: 0 for off, then 2 to 12. The picture is reduced to that many
colours before the filaments are matched to it, so it prints in at most that
many.
Reduced by k-means in RGB rather than by posterizing the range, because the
useful question is which colours the picture is actually made of. A tartan
asks for its own handful of greens and reds; an even split of the range would
hand it eight shades of mud. The clustering runs over a coarse 16-per-channel
histogram, 4096 weighted points instead of a million pixels, which is what
makes it cheap enough to sit on a slider, and it is seeded the k-means++ way
so a picture with one dominant colour does not get every centre inside it.
This is not the displacement tool's "Mixed colors", which caps how many mixed
filament slots a bake may create. Nothing like it existed.
Background removal can now be told which colour to remove. The four corners
stay the default, because a background that does not reach all four of them
is not a background and that makes the common case one click, but the panel
shows the colour as a swatch that is also a picker, and a dropper beside it
takes the colour from the preview. The dropper reads the decoded image rather
than the screen: the preview is scaled, and it already has the corrections
applied, so a screen read would key the corrected colour against itself.
"Corners" puts it back.
Measured on Tartan, a cube face, four filaments:
run out tris coloured filaments filament shares
image, no correction 22534 14544 4 1:91% 3:8% 4:1%
image, 2 colors 18232 10268 2 1:66% 3:34%
image, 3 colors 24458 16459 3 1:78% 3:18% 4:5%
image, 5 colors 23972 15978 4 1:90% 3:9% 4:1%
Asking for 2 gives 2 and asking for 3 gives 3. Asking for 5 gives 4, which is
the contract working the right way round: only four filaments are loaded, and
"at most" is the guarantee. No run moves the surface.
SysInfoDialog, JumpPointSearch and WebUpdatePlugin are in no CMake list on any platform, and nothing includes ConfigExceptions.hpp, wxinit.h or Utils/Profile.hpp. MultiTaskModel holds only empty namespaces. Also drop the empty GUI_App::system_info() and comments that still mention SysInfoDialog.
TriangleMeshDeal.hpp reused Timer.hpp's guard and CloneDialog.hpp reused
SingleChoiceDialog.hpp's, so including both headers of a pair silently
dropped the second one. Use #pragma once instead.
Its inline suggestions rarely apply: clang-tidy adds a missing include
to the include block at the top of the file, which a PR seldom touches,
and GitHub only accepts review suggestions on lines in the PR's diff.
Three more corrections, chosen because the output is a handful of flat
filament colours rather than a continuous image: what helps is whatever
decides which filament an area lands in and where the boundary between two
of them falls.
- Sharpen. An unsharp mask over the colour, reusing the sliding-window blur
the Smoothing slider already uses, run per channel. Worth more here than
in a photo editor: a soft edge between two colours quantizes into a ragged
band of both, and sharpening first makes it land in one place.
- Two colours. Thresholds on luma so a logo or line art prints in exactly
two filaments instead of speckling across several, with the split point on
a slider.
- Remove background. The one that is not an RGB transform. DecodedHeightTexture
gains an alpha plane, filled from the source PNG's fourth channel when it
has one, and the key additionally clears whatever colour the picture's four
corners share. A transparent texel is not black: sample_layer_color()'s
covered() now reports it the same way it reports being outside an untiled
placement, so the triangle keeps the object's own colour and the bake needs
no new concept. The preview follows: upload_color_texture() box-filters
coverage into the alpha channel it used to fill with 255, and the shader
leaves the surface alone where it is below half.
The corners rather than an eyedropper: a background that does not reach all
four of them is not a background, and a picker would be a second interaction
for the case this exists to make one click. The median of the four is taken
so one stray corner cannot decide it.
Measured on Tartan, a cube face, four filaments:
run out tris coloured max offset mm filament shares
image, no correction 22534 14544 0.000005 1:91% 3:8% 4:1%
image, sharpen 1.0 24780 16783 0.000004 1:84% 3:12% 4:3%
image, two colors 14798 6837 0.000005 2:22% 4:78%
image, bg removed 14120 2620 0.000005 1:29% 3:16% 4:55%
Background removal cuts the painted area from 14544 triangles to 2620, which
is the whole point of it; the threshold collapses to two filaments; sharpen
shifts the shares and adds boundary triangles. None of them moves the
surface. The probe's default scene is now Tartan rather than Colour Bricks,
which has too little luma range for the threshold to show anything.
* Add a /bot fix-tidy Command That Pushes clang-tidy's Missing Includes
A maintainer comments /bot fix-tidy on a PR. A read-only job runs
scripts/run_clang_tidy.sh --fix on the PR's merge commit and returns the
patch; a second job, which never runs the PR's code, accepts it only when
it adds nothing but #include lines to C++ files the PR already changes,
then commits it on the PR head and pushes with CLANG_TIDY_BOT_TOKEN.
* Post clang-tidy's Fix as Review Suggestions Instead of Pushing It
A failed clang-tidy check now runs once more with --fix and uploads the
patch as the clang-tidy-fix artifact; the check still fails. /bot
fix-tidy downloads that patch for the PR's latest commit, checks it adds
only includes to C++ files the PR changes, and posts one review: a
one-click suggestion for each include on a line the PR's diff shows, and
the whole patch with a git apply command. Nothing is pushed and no PR
code runs in the bot, so the PR's author can run it too and no token is
needed.
* Make the clang-tidy Fix in Its Own PR-Scoped Workflow on Request
clang_tidy.yml is back to main's version. clang_tidy_fix.yml runs on
every PR push but skips its job on the first attempt; /bot fix-tidy
re-runs it, so run_clang_tidy.sh --fix runs in the PR's own context
with a read-only token and the PR's cache, never main's, and the PR's
author can ask for it. When that re-run finishes, the bot posts the
patch as review suggestions as before. When the clang-tidy step fails,
the bot also comments asking the author to run the script or the
command, edits that comment on later failures, and deletes it once the
check passes.
* Give the clang-tidy Bot Write Access to PRs It Answers
* Tell /bot fix-tidy Users How to Start a Fix Run When There Is None to Re-Run
Turns every colour of the picture around the colour wheel without changing
how light it is. This matters more here than in a photo editor: the filament
set is fixed and arbitrary, so hue is what lets the picture meet the palette
rather than the other way round. With orange loaded but no red, a small turn
puts the reds onto the orange instead of onto whatever happens to be nearest.
Implemented as Rodrigues' rotation of the colour about the grey axis, then
the Rec. 709 luma is restored exactly: the rotation preserves R+G+B, not the
weighted luma, and without the correction turning the wheel would also change
which areas read as light and dark.
Measured, same scene and harness as the other corrections:
run out tris coloured max offset mm filament shares
image, no correction 12066 4052 0.000005 1:71% 2:25% 3:4%
image, hue +60 8208 268 0.000005 2:100%
image, hue -120 8208 268 0.000005 3:100%
The reddish brick moves onto a different filament in each direction and the
surface does not move.
Dropping a picture already worked, but a drag had no feedback at all: the
only way to find out whether letting go would do anything was to let go.
While a drag is over the 3D view and the Image tool is open, the canvas now
dims and shows a dashed teal border with "Drop a picture to put it on the
model" in the middle. It goes away on leave and on drop.
Three things worth knowing about how it is wired:
- wxWidgets cannot hand us the file names before the drop on GTK, so the
zone says nothing about what is actually being dragged. It is worded as an
invitation rather than a promise, and anything that is not a picture still
falls through to the model loader exactly as before.
- A drag is not a mouse move as far as the canvas is concerned, so nothing
would repaint and the zone would appear only once something else happened
to trigger a frame. set_file_drag_over() marks the canvas dirty and asks
for an extra frame.
- OnEnter and OnLeave are declared virtual without override, matching the
class's existing OnDropFiles. Marking only the new ones trips
-Winconsistent-missing-override on that pre-existing line, which is an
error here.
The zone is drawn on the foreground draw list from inside the panel's ImGui
frame, so it covers the panel as well as the view.
- The toolbar icon's stroke is 0.4 at its 16 px canvas, which renders at
1.00 px on the 40 px sprite. Measured rather than judged: every icon on the
gizmo bar was rasterized at 10x and the median thin run length taken, and
1.00 px is what thirteen of the seventeen use. The previous 0.75 came out
at 1.90, the heaviest on the bar.
- Dropping a picture on the 3D view loads it, when the Image tool is the one
open. It is imported into the user's library on the way, as the panel's
Import tile does, so it is there next time. With the tool closed a dropped
PNG goes on doing whatever it did before. This mirrors the SVG drop that
already fed the text tool, and sits next to it.
- The placement handle comes up by itself the first moment there is something
to anchor it to, so a picture can be nudged straight after the click that
placed it. Armed once, so turning it off stays off.
Two things had to change for that last one to be usable rather than annoying:
- A click that misses the handle now falls through to placing the picture.
on_mouse() used to return false there and swallow it, which is right for
the displacement tool, where the user turned the mode on deliberately, and
wrong here, where it is on from the start and would have disabled the
tool's one gesture.
- update_model_object() re-anchors the handle after a stroke that moved the
paint. Previously the anchor was only refreshed on a layer switch or
reorder, so clicking a different face left the handle behind on the old
one.
A project no longer reads texture files from outside its archive, and a
malformed value or layer slot drops only the texture layers instead of
failing the whole load. Copies of an object that share a mesh keep their
own texture paint when saved, and projects without texture paint no
longer use extra memory to load.
Five fixes to the Image tool, from using it:
- The layer now starts with no picture on it, so the panel opens on its empty
frame with a plus in it. It used to seed the first shipped colour texture,
which put somebody else's picture on screen before the user had chosen one.
The frame is the only state the panel has until a picture is loaded, and
clicking it is what creates the layer.
- Its picker offers only the user's own images. The shipped library is
grayscale height maps, which is what the displacement tool wants and the
opposite of what an image bake can use: it prints colour, and a height map
has none. The Built-in section is left out rather than shown full of things
that would all warn when picked, and the import hints now talk about
pictures instead of height maps.
- The preview stopped tiling a layer that is not tiled. The shader had no
tile_enabled uniform and wrapped the image unconditionally, so an untiled
picture previewed as a repeating pattern the bake would never produce.
in_tile() now draws the same line DecodedHeightTexture::sample() does, and
gates the colour and all three relief paths on it. Both the 140 and the 110
variant; glslangValidator passes on each.
- The panel docks against the right edge of the canvas, where the
displacement panel is, with the same undock toggle in its title row, so the
two tools do not put their controls in different corners.
- The toolbar icon's frame is square and its stroke thinner. At 16 px scaled
to the toolbar a 1 px stroke came out at 2.5 px, heavier than every
neighbour; 0.75 px matches them.
# Description
With "Support multi bed types" enabled on a Qidi printer, the
first-layer bed temperature was always taken from the Smooth PEI / High
Temp Plate setting, whichever plate type was selected.
The slicer computes the right value (`first_layer_bed_temperature` in
the G-code footer follows the selected plate), but the machine start
G-code of these presets hardcoded `[hot_plate_temp_initial_layer]`:
- `fdm_qidi_x3_common` (base of X-Max 3, X-Plus 3, X-Smart 3, X-Plus 4,
Q1 Pro)
- `Qidi Q1 Pro 0.4 nozzle`
- `Qidi X-CF Pro 0.4 nozzle`
They now use `[bed_temperature_initial_layer_single]`, which follows the
selected plate type, as most other profiles do. The Qidi bundle version
is bumped to `02.04.00.17`.
With "Support multi bed types" disabled, these printers use the default
plate (Smooth PEI / High Temp Plate), so the temperature is unchanged
there.
# Screenshots/Recordings/Graphs
N/A — profile change only.
## Tests
`python3 scripts/orca_profile_tool.py check` passes.
Sliced on Qidi X-Max 3 0.4 nozzle with "Support multi bed types"
enabled, `0.20mm Standard @Qidi XMax3`, `Generic PLA @Qidi`, with a
different bed temperature set for each plate type (first layer / other
layers).
Before, with first layer set to 50–55 °C: `M190 S54` for every plate
type.
After, a two-layer print:
| Plate type | Set (first / other) | `M190` at start | `M140` at layer 2
|
| --- | --- | --- | --- |
| Cool Plate SuperTack | 51 / 61 | 51 | 61 |
| Cool Plate | 52 / 62 | 52 | 62 |
| Textured Cool Plate | 53 / 63 | 53 | 63 |
| Engineering Plate | 54 / 64 | 54 | 64 |
| Smooth PEI / High Temp Plate | 55 / 65 | 55 | 65 |
| Textured PEI Plate | 56 / 66 | 56 | 66 |
🤖 Generated with [Claude Code](https://claude.com/claude-code)4
Five changes to the Image tool, all in its own window bar the presets:
- The picture is shown whole, the full width of the window, instead of a row
sized thumbnail beside a file name: it is the thing being placed, so that
was the wrong size for it. The box shows the corrected colour texture, so
moving a correction slider is visible in the panel as well as on the model;
a grayscale picture has no colour texture and falls back to the height
thumbnail. The file name stays underneath as a caption.
- Before anything is loaded the same box is an empty frame with a plus in it,
in the same place and the same size, rather than a button of its own.
Clicking either state opens the library, which is also where Import lives.
- Tile, off by default. A photo or a logo goes on once, so a new layer the
Image tool creates starts with a single copy and Size sets how big that
copy is. It is a default and not a pin, so the checkbox can turn it on.
- Move, the on-canvas placement handle the displacement tool has: drag the
handle on the surface to slide the picture, or one of its arrows to slide
it along one axis.
- Detail, the bake resolution, with the budget pinned to a million triangles.
Each triangle prints in one filament, so this is how sharp an edge between
two colours can come out, and it is the only thing left that decides it.
The auto budget is 750 k, chosen for relief where triangles buy shape;
here they buy the whole output, so it is worth spending more. The slider is
seeded with the resolution Auto would have picked for the model, and warns
when what it asks for no longer fits the budget.
Also fixes a short circuit in the panel: apply_standard_mode_presets(mv) ||
apply_image_mode_presets(mv) skipped the image presets on any frame the
standard ones changed something.
Measured with the texture-bake harness on the tool's shipped configuration
(tiling off, million-triangle budget, the seeded resolution), one cube face,
four filaments:
run out tris coloured max offset mm filament shares
relief, depth 0.4 mm 41040 33902 0.216000 1:91% 2:6% 3:3%
image, no correction 12066 4052 0.000005 1:71% 2:25% 3:4%
image, brightness +0.5 8208 268 0.000005 4:100%
image, contrast +0.8 11980 3910 0.000005 1:78% 2:20% 3:1%
image, saturation 0 8208 268 0.000005 3:100%
image, gamma 2.2 13176 5175 0.000004 1:67% 4:33%
image, negative 8208 268 0.000005 4:100%
Fewer coloured triangles than the previous run of the same scene because
tiling is now off: one copy covers part of the face rather than repeating
across it. The surface still does not move and every correction still changes
which filaments the picture prints in.
On Linux and macOS the app follows the system theme: at startup it
overwrites the saved dark_color_mode with the system appearance. The label
colours and the cached dark mode flag were already set from the saved value
by then, and the later "changed" check reads the mode only after the
overwrite, so it never fires. For that session every dialog that maps its
colours through UpdateDlgDarkUI keeps the old mode, e.g. white dialogs on a
dark main window after switching the desktop to dark, or on a fresh install
on a dark desktop.
Refresh the label colours and the flag when the system theme differs from
the saved mode.
# Description
## Summary:
Adds favorite filament support to the AMS material selection dropdown.
Users can now click a star icon next to a selectable filament in the AMS Materials Setting dialog to mark it as a favorite. Favorited filaments appear in a new `Favorite filaments` section at the top of the dropdown, while all compatible filaments remain available in the `All filaments` section.
This PR also adds generic right-side action icon support to Orca's custom dropdown widget:
- Adds optional action icons to `DropDown::Item`.
- Adds a new `EVT_DROPDOWN_ITEM_ACTION` event for clicks on the action icon.
- Relays the action event through `ComboBox`.
- Adds `ComboBox::RefreshDropdownIfOpen()` so callers can refresh an already-open dropdown without changing normal `ForceDropdownOpen()` behavior.
The AMS favorites implementation:
- Uses existing bundled star assets:
- `score_star_dark` for non-favorited filaments
- `score_star_light` for favorited filaments
- Stores favorite filament IDs in app config under `ams_favorite_filament_ids`.
- Stores stable filament IDs instead of display names
- Uses existing C-style string escaping helpers for persistence.
- Keeps Bambu/RFID/read-only slot behavior unchanged
No new external dependencies.
**Note:** `AMSMaterialsSetting` now uses Orca's custom `ComboBox` on macOS as well, because the favorite-star UI depends on the custom `DropDown` item renderer. I was able to test this on Windows, but macOS/Linux smoke testing is still needed and would be appreciated.
# Screenshots/Recordings:
https://github.com/user-attachments/assets/41e881be-d4de-4a7d-8f99-4297567adef2
<img width="494" height="36" alt="Screenshot 2026-05-30 103432" src="https://github.com/user-attachments/assets/e7c34ed3-db7a-4705-a1cd-4bfe7ac1616a" />
## Tests
Tested on Windows:
- Built OrcaSlicer locally
- Opened the Device tab with Bambu Lab printer with AMS
- Opened the AMS Materials Setting dialog from an editable AMS slot
- Confirmed clicking a star toggles favorite state.
- Star clicks do not select the filament row.
- Dropdown remains open after toggling a favorite.
- Confirmed favorited filaments appear under `Favorite filaments`.
- All compatible filaments remain visible under `All filaments`.
- Confirmed Filament still selected as before
- Confirmed favorites persist after closing and reopening the dialog.
- Confirmed favorites persist after restarting OrcaSlicer.
- Confirmed the favorite IDs are saved in `OrcaSlicer.conf` under `ams_favorite_filament_ids`.
- Bambu/RFID/read-only filament slots continue using the existing read-only display behavior.
Not tested:
- macOS
- Linux/Wayland
These should be tested, `ComboBox` behavior not tested on macOS.
[How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The first cut was the displacement panel with depth hidden, which left the
brushes, the eight layer slots, the UV editor, seams, tiling and the
mesh-preparation sections in a tool that needs none of them, and offered no
way to correct the picture at all. This replaces that panel and adds the
corrections.
The flow is now: load a picture, click the model to put it there, adjust it,
press Apply.
- The Image tool gets its own window, render_image_input_window(), defined in
GLGizmoImage.cpp. on_render_input_window() dispatches to it on the first
line, so the displacement panel goes back to exactly what it was rather
than being threaded with tests for which tool is drawing.
- A click places the picture: the tool pins SMART_FILL with a pointer cursor,
so one click floods the surface it lands on out to the first fold sharper
than the Spread angle. No brush, no erase toggle.
- Projection keeps Flat, Around, Ball and From view. Unwrap is left out and a
layer arriving on it falls back to Flat: seams, islands and the UV pane are
an editor's worth of interface for a tool that wants a click and a slider.
- The preview is pinned to the fast one. With no relief to build it is both
instant and exactly the final colour, since the shader's colour path does
not depend on depth_mm.
- The window shows only what this case needs: the picture, Spread, the four
projections, Size, Rotation, the corrections, Apply and Close.
Colour correction is new in libslic3r. TextureDisplacementLayer gains
brightness, contrast, saturation, gamma and color_invert, all defaulting to
the identity, and apply_color_adjustments() applies them to an image's RGB
only, never to the height: a brightness slider has no business moving
geometry. It runs at the single point every bake and preview job reads a
layer's colour from, prepare_sampleable_layers(), and at the two caches that
would otherwise miss a change the image_data pointer does not show: the fast
preview's GPU texture and the mix ranking.
Measured with the texture-bake harness, one cube face painted with a colour
image, four filaments:
run out tris coloured max offset mm filament shares
relief, depth 0.4 mm 41040 33902 0.216000 1:91% 2:6% 3:3%
image, no correction 24226 17090 0.000007 1:71% 2:25% 3:4%
image, brightness +0.5 8204 1068 0.000004 4:100%
image, contrast +0.8 22688 15552 0.000007 1:75% 2:24% 3:1%
image, saturation 0 8204 1068 0.000004 3:100%
image, gamma 2.2 24754 17618 0.000007 1:66% 4:34%
image, negative 8204 1068 0.000004 4:100%
Max offset is the distance from every output vertex to the original surface,
not a vertex-array diff: the bake refines, so a new vertex only counts as
unmoved if it lies in the old surface. Every correction moves which filaments
the picture prints in, and none of them moves the surface.
Five Plater::TakeSnapshot guards in GUI_ObjectList were unnamed temporaries, so snapshots were allowed again as soon as the snapshot was taken. Naming them keeps the suppression until the edit is done, as everywhere else.