Compare commits

..
Author SHA1 Message Date
ExPikaPaka 8ac8a75056 Include the headers GLGizmoImage.cpp uses directly
Adds the includes misc-include-cleaner named: libslic3r/Point.hpp for
Vec2d, Vec3f and Transform3d, GLGizmoTextureDisplacement.hpp for the
base class, IconManager.hpp for the icon lookup, and cmath, cstddef
and cstdlib for round, size_t and abs.
2026-10-12 15:31:21 +02:00
ExPikaPaka 60248cadd3 Move the Image tool's declarations off the line main now uses
store_panel_state() landed in the same place, from the .3mf persistence
branch. Same declarations, a few lines down, after bake_standard().
2026-10-12 15:14:22 +02:00
ExPikaPaka c07a082050 Add the pipette icon on a line of its own
Three other open texture-displacement branches add an icon to this list too.
Reflowing the lines they also touch is what turns four one-word additions
into four conflicts; a new line four lines away from the next one merges in
any order. Order in the list is irrelevant - the map keys by file name.
2026-10-12 14:03:43 +02:00
ExPikaPaka c92721cabf fix: Negative belongs with the colour corrections, not under Background
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.
2026-10-12 13:30:31 +02:00
ExPikaPaka 47a4e7447f feat: a real dropper that reads the model, foldable sections, a scrolling panel
- 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.
2026-10-12 13:21:41 +02:00
ExPikaPaka 4286cb30ba feat: a colour count instead of Two colours, and a pickable background
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.
2026-10-12 13:01:06 +02:00
ExPikaPaka 3ee9c02acf feat: sharpen, two colours, and remove background in the Image tool
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.
2026-10-12 12:37:57 +02:00
ExPikaPaka 533aaaed28 feat: add Hue to the Image tool's colour correction
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.
2026-10-12 12:22:30 +02:00
ExPikaPaka 4dc87e876e feat: show a drop zone while a file is dragged over the view
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.
2026-10-12 12:05:22 +02:00
ExPikaPaka 8bb2a1eec5 feat: match the icon weight, drop a picture onto the view, handle on by default
- 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.
2026-10-12 11:48:33 +02:00
ExPikaPaka 518f961640 feat: empty start, user images only, untiled preview, dockable Image panel
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.
2026-10-12 11:31:33 +02:00
ExPikaPaka 680a597567 feat: show the picture whole in the Image panel, add tile, move and detail
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.
2026-10-12 11:12:57 +02:00
ExPikaPaka e003b61e17 feat: make the Image gizmo a minimal load, click, correct, apply tool
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.
2026-10-12 10:20:44 +02:00
ExPikaPaka a800e0c186 feat: add an Image gizmo for zero-displacement colour
A separate toolbar tool for the simple case of texture displacement: put a
colour picture on the model with no relief at all.

It is a thin wrapper, not a fork. GLGizmoImage subclasses
GLGizmoTextureDisplacement and overrides one new protected virtual,
image_mode(). Painting, the layer stack, the paint masks, the texture
library, the previews and the bake are the same code reached the same way;
a zero-depth bake refines the painted area and writes the per-triangle
filament indices exactly as a relief bake does, it just displaces by
nothing.

In the base gizmo, image_mode() does three things:

- apply_image_mode_presets() pins every layer to depth 0 and turns colour on
  wherever the image has any, alongside the existing Standard-mode presets.
- The panel leaves out Depth, Midlevel, Invert, Edge fade and Blend, and the
  Standard/Pro switch, which is what keeps the mesh-preparation sections
  hidden. Bake is disabled when no image carries colour, since that bake
  would refine the mesh and produce nothing.
- add_texture_layer() seeds a new layer from the first library texture that
  has colour, rather than the first one, because most of the shipped library
  is grayscale height maps.

The two tools share one layer stack, so opening the Image tool on a volume
whose layers carry relief flattens them. Only depth changes: the paint, the
image, the tiling, the mapping and any relief already baked into the mesh
are untouched.

The toolbar icon follows Orca's menu-icon style: 16 px, 1 px #949494 frame,
#009688 on the picture inside.

The shortcut is registered but unbound: every single letter the Plater
context can reach is already taken, and a default that shadows one of them
is worse than none. docs/HLSD/keyboard-shortcuts.md now documents that an
empty default chord is a supported choice.

Measured with a probe built against this tree in the texture-bake harness:
a depth-0 bake of a painted cube face leaves every output vertex within
7 nm of the original surface (tolerance 1 um) while colouring 17090 of
24226 output triangles across 3 filaments; the same scene at depth 0.4 mm
moves vertices up to 0.216 mm.
2026-10-12 08:29:10 +02:00
65 changed files with 2633 additions and 3041 deletions
-8
View File
@@ -1,18 +1,10 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit.
#
# Clang also records the modification time of every input in a precompiled header
# and rejects the header once one differs. ccache caches the compile that creates
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
# with the old file's time, and every compile that uses it fails. The build system
# already rebuilds the header when an input changes. This covers the dependencies,
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif ()
endforeach ()
+3 -1
View File
@@ -124,7 +124,9 @@ Control on the Global page, and from the `?` key on the page of the view that re
the dialog should list it; the row's section heading is the `section_table` entry
above it, so a new section needs an entry there too. Pick a default that does not
collide inside its contexts; the `[Shortcuts]` tests check every default against the
others.
others. A default `KeyChord{}` leaves the action unbound: it is listed in the dialog
for the user to bind and matches nothing until they do, which is the honest answer
when every key its contexts can reach is already spoken for.
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
-1
View File
@@ -309,7 +309,6 @@ src/slic3r/GUI/BBLStatusBarPrint.cpp
src/slic3r/GUI/BBLStatusBarSend.cpp
src/slic3r/GUI/BaseTransparentDPIFrame.cpp
src/slic3r/GUI/CloneDialog.cpp
src/slic3r/GUI/LegacyPrinterMigrationDialog.cpp
src/slic3r/GUI/ColorDecomposeDialog.cpp
src/slic3r/GUI/ColorDecomposeSupport.cpp
src/slic3r/GUI/ConfigWizard.cpp
-3
View File
@@ -1,3 +0,0 @@
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 876 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g transform="rotate(-45 8 8)"><path d="M8 14.4 6.5 11.2 9.5 11.2 Z" style="fill:#009688"/><path d="M6.5 7.3 6.5 11.2 8 14.4 9.5 11.2 9.5 7.3" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linejoin:round;stroke-linecap:round"/><rect x="5.5" y="3.8" width="5" height="3.5" rx="0.9" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linejoin:round"/></g></svg>

After

Width:  |  Height:  |  Size: 458 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="5.9" cy="6.3" r="1.25" style="fill:#009688"/><path d="M3.4 11.9 6.4 7.7 8.8 10.7 10.4 8.6 12.6 11.9 Z" style="fill:#009688"/><rect x="2.375" y="2.375" width="11.25" height="11.25" rx="1.25" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 386 B

@@ -1,795 +0,0 @@
{
"presets": {
"Bambu ABS @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu ABS @BBL X1C": [
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu ASA @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PA6-CF @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PA6-CF @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PA6-CF @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PA6-CF @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PA6-CF @BBL X1C": [
"Bambu Lab P1P 0.6 nozzle",
"Bambu Lab P1P 0.8 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab P1S 0.8 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle",
"Bambu Lab X1 Carbon 0.8 nozzle"
],
"Bambu PA6-CF @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PAHT-CF @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PAHT-CF @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PAHT-CF @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PAHT-CF @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle",
"Bambu Lab P1P 0.8 nozzle"
],
"Bambu PAHT-CF @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PAHT-CF @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab P1S 0.8 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle",
"Bambu Lab X1 Carbon 0.8 nozzle"
],
"Bambu PAHT-CF @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PC @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PETG @0.2 nozzle": [
"Qidi Q1 Pro 0.2 nozzle"
],
"Bambu PETG Basic @BBL H2D 0.6 nozzle": [
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PETG Basic @BBL H2DP 0.6 nozzle": [
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PETG Basic @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PETG Basic @BBL P2S 0.6 nozzle": [
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PETG Basic @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PETG HF @BBL P2S 0.6 nozzle": [
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PETG HF @BBL X1C": [
"Bambu Lab P1P 0.4 nozzle",
"Bambu Lab P1S 0.4 nozzle"
],
"Bambu PETG HF @BBL X1C 0.8 nozzle": [
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PETG HF @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PETG Translucent @BBL H2D 0.6 nozzle": [
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PETG Translucent @BBL H2DP 0.6 nozzle": [
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PETG Translucent @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PETG Translucent @BBL P2S 0.6 nozzle": [
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PETG Translucent @BBL X1C 0.8 nozzle": [
"Bambu Lab P1P 0.6 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PETG-CF @BBL H2D 0.6 nozzle": [
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PETG-CF @BBL H2DP 0.6 nozzle": [
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PETG-CF @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PETG-CF @BBL P2S 0.6 nozzle": [
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PLA Basic @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Basic @BBL X1C": [
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Basic @BBL X2D 0.6 nozzle": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PLA Dynamic @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Dynamic @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Galaxy @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Galaxy @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Glow @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Glow @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Lite @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle"
],
"Bambu PLA Lite @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PLA Lite @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Lite @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Lite @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PLA Marble @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle",
"Bambu Lab P1P 0.8 nozzle"
],
"Bambu PLA Marble @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Matte @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Matte @BBL X1C": [
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Matte @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PLA Metal @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Metal @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Silk+ @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PLA Silk+ @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PLA Silk+ @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PLA Silk+ @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Silk+ @BBL X1C": [
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Silk+ @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PLA Sparkle @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Sparkle @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Tough+ @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Tough+ @BBL X1C": [
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA Tough+ @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PLA Translucent @BBL H2D 0.8 nozzle": [
"Bambu Lab H2D 0.6 nozzle"
],
"Bambu PLA Translucent @BBL H2DP 0.8 nozzle": [
"Bambu Lab H2D Pro 0.6 nozzle"
],
"Bambu PLA Translucent @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PLA Wood @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu PLA Wood @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu PLA-CF @BBL H2D 0.6 nozzle": [
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PLA-CF @BBL H2DP 0.6 nozzle": [
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PLA-CF @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PLA-CF @BBL P2S 0.6 nozzle": [
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PLA-CF @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu PPA-CF @BBL X1C": [
"Bambu Lab P1P 0.6 nozzle",
"Bambu Lab P1P 0.8 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab P1S 0.8 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle",
"Bambu Lab X1 Carbon 0.8 nozzle"
],
"Bambu PVA @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu PVA @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu PVA @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu PVA @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu PVA @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu Support For PLA @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu Support For PLA @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu Support For PLA @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu Support For PLA @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu Support For PLA @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu Support For PLA/PETG @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu Support For PLA/PETG @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu Support For PLA/PETG @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu Support For PLA/PETG @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu Support For PLA/PETG @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu Support for ABS @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu Support for ABS @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu Support for ABS @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu Support for ABS @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu Support for ABS @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu TPU 90A @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu TPU 90A @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu TPU 95A HF @BBL H2D": [
"Bambu Lab H2D 0.4 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu TPU 95A HF @BBL H2DP": [
"Bambu Lab H2D Pro 0.4 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu TPU 95A HF @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu TPU 95A HF @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu TPU 95A HF @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Bambu TPU for AMS @BBL H2D": [
"Bambu Lab H2D 0.6 nozzle",
"Bambu Lab H2D 0.8 nozzle"
],
"Bambu TPU for AMS @BBL H2DP": [
"Bambu Lab H2D Pro 0.6 nozzle",
"Bambu Lab H2D Pro 0.8 nozzle"
],
"Bambu TPU for AMS @BBL H2S": [
"Bambu Lab H2S 0.6 nozzle",
"Bambu Lab H2S 0.8 nozzle"
],
"Bambu TPU for AMS @BBL P1P": [
"Bambu Lab P1P 0.6 nozzle"
],
"Bambu TPU for AMS @BBL P2S": [
"Bambu Lab P2S 0.6 nozzle",
"Bambu Lab P2S 0.8 nozzle"
],
"Bambu TPU for AMS @BBL X1C": [
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 Carbon 0.6 nozzle"
],
"Bambu TPU for AMS @BBL X2D": [
"Bambu Lab X2D 0.8 nozzle"
],
"Blocks ABS @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks ASA @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks ASA-CF @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PA @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PA-CF @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PC @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PETG @Blocks_Pro_S100": [
"BLOCKS RD50 V2 0.4 nozzle",
"BLOCKS RD50 V2 0.6 nozzle",
"BLOCKS RD50 V2 0.8 nozzle",
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PLA @Blocks_Pro_S100": [
"BLOCKS RD50 V2 0.4 nozzle",
"BLOCKS RD50 V2 0.6 nozzle",
"BLOCKS RD50 V2 0.8 nozzle",
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PLA-CF @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks PVA @Blocks_RD50_V2": [
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Blocks TPU @Blocks_Pro_S100": [
"BLOCKS RD50 V2 0.4 nozzle",
"BLOCKS RD50 V2 0.6 nozzle",
"BLOCKS RD50 V2 0.8 nozzle",
"BLOCKS RF50 0.4 nozzle",
"BLOCKS RF50 0.6 nozzle",
"BLOCKS RF50 0.8 nozzle"
],
"Cubicon ABS @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon ABS-A100 @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon ABSk @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon PA-CF @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon PC @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon PETG @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon PLA @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon PLA+ @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Cubicon PLAi21 @Cubicon xCeler-I 0.4 nozzle": [
"Cubicon xCeler-Plus 0.4 nozzle"
],
"Flashforge HS PLA": [
"Flashforge AD5X 0.4 nozzle",
"Flashforge AD5X 0.6 nozzle",
"Flashforge AD5X 0.8 nozzle"
],
"Flashforge PETG-CF": [
"Flashforge AD5X 0.4 nozzle",
"Flashforge AD5X 0.6 nozzle",
"Flashforge AD5X 0.8 nozzle"
],
"Flashforge PLA-CF": [
"Flashforge AD5X 0.4 nozzle",
"Flashforge AD5X 0.6 nozzle",
"Flashforge AD5X 0.8 nozzle"
],
"Flashforge TPU 95A": [
"Flashforge AD5X 0.4 nozzle",
"Flashforge AD5X 0.6 nozzle",
"Flashforge AD5X 0.8 nozzle"
],
"FusRock PAHT-CF": [
"Flashforge Guider 3 Ultra 0.6 Nozzle"
],
"FusRock PET-CF": [
"Flashforge Guider 3 Ultra 0.6 Nozzle"
],
"Generic ABS @Anycubic": [
"Anycubic Kobra 3 0.4 nozzle"
],
"Generic ABS @Creality": [
"Creality CR-10 SE 0.2 nozzle",
"Creality CR-10 SE 0.4 nozzle",
"Creality CR-10 SE 0.6 nozzle",
"Creality CR-10 SE 0.8 nozzle",
"Creality CR-6 SE 0.2 nozzle",
"Creality CR-6 SE 0.4 nozzle",
"Creality CR-6 SE 0.6 nozzle",
"Creality CR-6 SE 0.8 nozzle",
"Creality CR-M4 0.4 nozzle",
"Creality Ender-3 V2 0.4 nozzle"
],
"Generic ABS @Creality Ender-3V3-all": [
"Creality Ender-3 V3 Plus 0.4 nozzle",
"Creality Ender-3 V3 Plus 0.6 nozzle"
],
"Generic ABS @Creality Hi-all": [
"Creality Hi 0.4 nozzle"
],
"Generic ABS @Creality K1-all": [
"Creality K1 SE 0.4 nozzle",
"Creality K1C 0.4 nozzle"
],
"Generic ABS @Creality K2-all": [
"Creality K2 0.4 nozzle",
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Plus 0.6 nozzle",
"Creality K2 Plus 0.8 nozzle",
"Creality K2 Pro 0.4 nozzle"
],
"Generic ABS @Flashforge G3U": [
"Flashforge Guider 3 Ultra 0.6 Nozzle"
],
"Generic ASA @Creality Ender-3V3-all": [
"Creality Ender-3 V3 Plus 0.4 nozzle",
"Creality Ender-3 V3 Plus 0.6 nozzle"
],
"Generic ASA @Creality K1-all": [
"Creality K1 SE 0.4 nozzle",
"Creality K1C 0.4 nozzle"
],
"Generic ASA @Creality K2-all": [
"Creality K2 0.4 nozzle",
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Pro 0.4 nozzle"
],
"Generic ASA @Flashforge G3U": [
"Flashforge Guider 3 Ultra 0.6 Nozzle"
],
"Generic HIPS @Flashforge": [
"Flashforge Guider 3 Ultra 0.6 Nozzle"
],
"Generic PA @Prusa": [
"Prusa MINIIS 0.25 nozzle",
"Prusa MINIIS 0.4 nozzle",
"Prusa MINIIS 0.6 nozzle",
"Prusa MINIIS 0.8 nozzle"
],
"Generic PA-CF @Creality Ender-3V3-all": [
"Creality Ender-3 V3 Plus 0.4 nozzle",
"Creality Ender-3 V3 Plus 0.6 nozzle"
],
"Generic PA-CF @Creality K1-all": [
"Creality K1C 0.4 nozzle"
],
"Generic PA-CF @Creality K2-all": [
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Plus 0.6 nozzle",
"Creality K2 Plus 0.8 nozzle"
],
"Generic PA-CF @Prusa": [
"Prusa MINIIS 0.25 nozzle",
"Prusa MINIIS 0.4 nozzle",
"Prusa MINIIS 0.6 nozzle",
"Prusa MINIIS 0.8 nozzle"
],
"Generic PC @Creality K1-all": [
"Creality K1C 0.4 nozzle"
],
"Generic PETG @Creality": [
"Creality CR-6 SE 0.2 nozzle",
"Creality CR-6 SE 0.4 nozzle",
"Creality CR-6 SE 0.6 nozzle",
"Creality CR-6 SE 0.8 nozzle",
"Creality CR-M4 0.4 nozzle"
],
"Generic PETG @Creality Ender-3V3-all": [
"Creality Ender-3 V3 0.4 nozzle",
"Creality Ender-3 V3 0.6 nozzle",
"Creality Ender-3 V3 Plus 0.4 nozzle",
"Creality Ender-3 V3 Plus 0.6 nozzle",
"Creality Ender-3 V3 SE 0.2 nozzle",
"Creality Ender-3 V3 SE 0.4 nozzle",
"Creality Ender-3 V3 SE 0.6 nozzle",
"Creality Ender-3 V3 SE 0.8 nozzle"
],
"Generic PETG @Creality Hi-all": [
"Creality Hi 0.4 nozzle"
],
"Generic PETG @Creality K1-all": [
"Creality K1 SE 0.4 nozzle",
"Creality K1C 0.4 nozzle"
],
"Generic PETG @Creality K2-all": [
"Creality K2 0.4 nozzle",
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Plus 0.6 nozzle",
"Creality K2 Plus 0.8 nozzle",
"Creality K2 Pro 0.4 nozzle"
],
"Generic PETG @Flashforge G3U": [
"Flashforge Guider 3 Ultra 0.6 Nozzle",
"Flashforge Guider 3 Ultra 0.8 Nozzle"
],
"Generic PETG @Sovol Zero Hardened Steel nozzle": [
"Sovol Zero 0.4 nozzle"
],
"Generic PETG HF @BBL X1C": [
"Bambu Lab P1S 0.4 nozzle",
"Bambu Lab P1S 0.6 nozzle",
"Bambu Lab X1 0.4 nozzle",
"Bambu Lab X1 0.6 nozzle",
"Bambu Lab X1 0.8 nozzle"
],
"Generic PETG-CF @Flashforge G3U": [
"Flashforge Guider 3 Ultra 0.6 Nozzle",
"Flashforge Guider 3 Ultra 0.8 Nozzle"
],
"Generic PLA @Creality": [
"Creality CR-10 SE 0.2 nozzle",
"Creality CR-10 SE 0.4 nozzle",
"Creality CR-10 SE 0.6 nozzle",
"Creality CR-10 SE 0.8 nozzle",
"Creality CR-6 SE 0.2 nozzle",
"Creality CR-6 SE 0.4 nozzle",
"Creality CR-6 SE 0.6 nozzle",
"Creality CR-6 SE 0.8 nozzle",
"Creality CR-M4 0.4 nozzle"
],
"Generic PLA @Creality Ender-3V3-all": [
"Creality Ender-3 V3 0.4 nozzle",
"Creality Ender-3 V3 0.6 nozzle",
"Creality Ender-3 V3 Plus 0.4 nozzle",
"Creality Ender-3 V3 Plus 0.6 nozzle"
],
"Generic PLA @Creality K1-all": [
"Creality K1 SE 0.4 nozzle",
"Creality K1C 0.4 nozzle"
],
"Generic PLA @Creality K2-all": [
"Creality K2 0.4 nozzle",
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Plus 0.6 nozzle",
"Creality K2 Plus 0.8 nozzle",
"Creality K2 Pro 0.4 nozzle"
],
"Generic PLA @Flashforge G3U": [
"Flashforge Guider 3 Ultra 0.6 Nozzle",
"Flashforge Guider 3 Ultra 0.8 Nozzle"
],
"Generic PLA @Hi-all": [
"Creality Hi 0.4 nozzle",
"Creality Hi 0.6 nozzle"
],
"Generic PLA @Sovol Zero Hardened Steel nozzle": [
"Sovol Zero 0.4 nozzle"
],
"Generic PLA Silk @Sovol Zero Hardened Steel nozzle": [
"Sovol Zero 0.4 nozzle"
],
"Generic PLA-CF @Creality K1-all": [
"Creality K1 SE 0.4 nozzle",
"Creality K1C 0.4 nozzle"
],
"Generic PLA-CF @Creality K2-all": [
"Creality K2 0.4 nozzle",
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Pro 0.4 nozzle"
],
"Generic PLA-CF @Flashforge G3U": [
"Flashforge Guider 3 Ultra 0.6 Nozzle",
"Flashforge Guider 3 Ultra 0.8 Nozzle"
],
"Generic PLA-CF @Prusa": [
"Prusa MINIIS 0.25 nozzle",
"Prusa MINIIS 0.4 nozzle",
"Prusa MINIIS 0.6 nozzle",
"Prusa MINIIS 0.8 nozzle"
],
"Generic TPU @Anycubic": [
"Anycubic Kobra 3 0.4 nozzle"
],
"Generic TPU @Creality": [
"Creality CR-10 SE 0.2 nozzle",
"Creality CR-10 SE 0.4 nozzle",
"Creality CR-10 SE 0.6 nozzle",
"Creality CR-10 SE 0.8 nozzle"
],
"Generic TPU @Creality Ender-3V3-all": [
"Creality Ender-3 V3 0.4 nozzle",
"Creality Ender-3 V3 0.6 nozzle",
"Creality Ender-3 V3 Plus 0.4 nozzle",
"Creality Ender-3 V3 Plus 0.6 nozzle",
"Creality Ender-3 V3 SE 0.2 nozzle",
"Creality Ender-3 V3 SE 0.4 nozzle",
"Creality Ender-3 V3 SE 0.6 nozzle",
"Creality Ender-3 V3 SE 0.8 nozzle"
],
"Generic TPU @Creality K1-all": [
"Creality K1 SE 0.4 nozzle",
"Creality K1C 0.4 nozzle"
],
"Generic TPU @Creality K2-all": [
"Creality K2 0.4 nozzle",
"Creality K2 Plus 0.4 nozzle",
"Creality K2 Pro 0.4 nozzle"
],
"HATCHBOX PETG @0.2 nozzle": [
"Qidi Q1 Pro 0.2 nozzle"
],
"QIDI PETG Basic @0.2 nozzle": [
"Qidi Q1 Pro 0.2 nozzle"
],
"QIDI PETG Rapido @0.2 nozzle": [
"Qidi Q1 Pro 0.2 nozzle"
],
"QIDI PETG Translucent @0.2 nozzle": [
"Qidi Q1 Pro 0.2 nozzle"
],
"re3D PC @0.4 nozzle": [
"re3D Gigabot 4 0.8 nozzle",
"re3D Gigabot 4 XLT 0.8 nozzle",
"re3D Terabot 4 0.8 nozzle"
],
"re3D PETG @0.4 nozzle": [
"re3D Gigabot 4 0.8 nozzle",
"re3D Gigabot 4 XLT 0.8 nozzle",
"re3D Terabot 4 0.8 nozzle"
],
"re3D PLA @0.4 nozzle": [
"re3D Gigabot 4 0.8 nozzle",
"re3D Gigabot 4 XLT 0.8 nozzle",
"re3D Terabot 4 0.8 nozzle"
],
"re3D rPETG @0.8 nozzle": [
"re3D GigabotX 2 1.75 nozzle",
"re3D GigabotX 2 XLT 1.75 nozzle",
"re3D TerabotX 2 1.75 nozzle"
],
"re3D rPP @0.8 nozzle": [
"re3D GigabotX 2 1.75 nozzle",
"re3D GigabotX 2 XLT 1.75 nozzle",
"re3D TerabotX 2 1.75 nozzle"
]
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Qidi",
"version": "02.04.00.17",
"version": "02.04.00.16",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
@@ -83,7 +83,7 @@
"extruder_clearance_height_to_lid": "120",
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"thumbnails_format": "PNG",
"default_filament_profile": [
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
@@ -100,7 +100,7 @@
"default_filament_profile": [
"Generic PLA @Qidi"
],
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
"scan_first_layer": "0"
}
@@ -24,7 +24,7 @@
"210x210/COLPIC",
"110x110/PNG"
],
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"scan_first_layer": "0"
}
@@ -51,6 +51,11 @@ uniform float tex_aspect;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
// False means the layer places a single copy rather than repeating: outside that copy there is
// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got
// this right while the preview wrapped the image and showed a tiled pattern that was never going
// to be printed.
uniform bool tile_enabled;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
@@ -218,6 +223,14 @@ int nearest_palette_entry(vec3 rgb)
return best;
}
// True where this uv has texture on it: everywhere when the layer repeats, and only inside the
// single placed copy when it does not. The bake's own sampler draws the same line (see
// DecodedHeightTexture::sample), so the preview now stops at the same edge it does.
bool in_tile(vec2 uv)
{
return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -243,9 +256,9 @@ void main()
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv;
color_uv = uv;
have_uv = true;
have_uv = in_tile(uv);
float h = texture2D(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0);
vec3 sigmaS = dFdx(world_pos.xyz);
vec3 sigmaT = dFdy(world_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal);
@@ -262,10 +275,12 @@ void main()
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
// uv first: outside a single placed copy there is no shell to march through.
vec2 uv = project_uv(tex_pos, triangle_normal);
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) *
(in_tile(uv) ? 1.0 : 0.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -302,7 +317,7 @@ void main()
}
color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true;
have_uv = in_tile(uv);
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
@@ -311,7 +326,8 @@ void main()
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) *
(have_uv ? 1.0 : 0.0);
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
@@ -341,7 +357,12 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
// The fourth channel is coverage: where the picture is not there, the surface keeps its own
// colour rather than being painted the black that a transparent texel's rgb reads as.
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) {
vec4 tex_rgba = texture2D(color_tex, color_uv);
if (tex_rgba.a >= 0.5)
albedo = palette_rgb[nearest_palette_entry(tex_rgba.rgb)];
}
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -110,6 +110,11 @@ uniform float tex_aspect;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
// False means the layer places a single copy rather than repeating: outside that copy there is
// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got
// this right while the preview wrapped the image and showed a tiled pattern that was never going
// to be printed.
uniform bool tile_enabled;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
@@ -284,6 +289,14 @@ int nearest_palette_entry(vec3 rgb)
return best;
}
// True where this uv has texture on it: everywhere when the layer repeats, and only inside the
// single placed copy when it does not. The bake's own sampler draws the same line (see
// DecodedHeightTexture::sample), so the preview now stops at the same edge it does.
bool in_tile(vec2 uv)
{
return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -321,9 +334,9 @@ void main()
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv;
color_uv = uv;
have_uv = true;
have_uv = in_tile(uv);
float h = texture(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0);
vec3 sigmaS = dFdx(world_pos.xyz);
vec3 sigmaT = dFdy(world_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal);
@@ -343,10 +356,12 @@ void main()
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
// uv first: outside a single placed copy there is no shell to march through.
vec2 uv = project_uv(tex_pos, triangle_normal);
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) *
(in_tile(uv) ? 1.0 : 0.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -383,7 +398,7 @@ void main()
}
color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true;
have_uv = in_tile(uv);
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
@@ -396,7 +411,8 @@ void main()
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
// gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) *
(have_uv ? 1.0 : 0.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame.
float cs = cos(rotation_rad);
@@ -429,7 +445,12 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
// The fourth channel is coverage: where the picture is not there, the surface keeps its own
// colour rather than being painted the black that a transparent texel's rgb reads as.
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) {
vec4 tex_rgba = texture(color_tex, color_uv);
if (tex_rgba.a >= 0.5)
albedo = palette_rgb[nearest_palette_entry(tex_rgba.rgb)];
}
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
-154
View File
@@ -11,7 +11,6 @@ commands:
normalize rewrite profile files into their canonical shape
trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json
legacy-compat write the printers user presets can be migrated back to
options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
@@ -25,9 +24,6 @@ After adding, renaming or deleting profile files, run:
normalize -> update-index -> generate-id -> check
normalize supplies missing types; update-index registers presets before id
generation.
After renaming filament presets or narrowing their compatible_printers, also
rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2
can be migrated back to their printers.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
@@ -3076,131 +3072,6 @@ def fix_variant_arrays(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False, s
return len(changed), errors
# ---------------------------------------------------------------------------
# legacy-compat
# ---------------------------------------------------------------------------
LEGACY_COMPAT_PATH = os.path.normpath(
os.path.join(SCRIPTS_DIR, "..", "resources", "info", "legacy_compatible_printers.json"))
def _renamed_from(data):
"""A preset's renamed_from names. Profiles write one ';'-separated string, as the
loader's unescape_strings_cstyle reads it; none of them quote or escape a name."""
value = data.get("renamed_from") or []
return [n for v in _as_list(value) for n in v.split(";") if n]
def _flatten_presets(profiles_dir):
"""Every bundle's composed filament and printer presets, for legacy-compat.
Returns (filaments, printers): filaments maps (vendor, name) to a dict with the
resolved compatible_printers, filament_id, extruder variants, renamed_from and
whether it is instantiated; printers maps each instantiated printer name to a
dict with its renamed_from and extruder variants. instantiation is read from the
preset's own file, as the loader requires every file to state it.
"""
def instantiated(data):
return str(data.get("instantiation", "")).lower() == "true"
ofl = load_vendor_configs(profiles_dir, OFL)["filament"]
filaments, printers = {}, {}
for vendor in list_vendor_names(profiles_dir):
configs = load_vendor_configs(profiles_dir, vendor)
cache = {}
for name, (_rel, data) in configs["filament"].items():
config = compose_config(name, configs["filament"], ofl, cache, vendor == OFL)
filaments[(vendor, name)] = {
"compatible_printers": set(_as_list(config.get("compatible_printers", ([],))[0])),
"filament_id": config.get("filament_id", (None,))[0],
"variants": set(_as_list(config.get("filament_extruder_variant", ([],))[0])),
"instantiation": instantiated(data),
"renamed_from": _renamed_from(data),
}
cache = {}
for name, (_rel, data) in configs["machine"].items():
if instantiated(data):
config = compose_config(name, configs["machine"], {}, cache)
printers[name] = {
"renamed_from": _renamed_from(data),
"variants": set(_as_list(config.get("printer_extruder_variant", ([],))[0])),
}
return filaments, printers
def legacy_compatible_printers(base_dir, profiles_dir=PROFILES_DIR):
"""Printers each current system filament preset covered in the base release
but no longer lists, kept only where a current preset with the same
filament_id still covers that printer.
User presets store only their diff, so they inherit their parent's
compatible_printers; when a profile change narrows that list, a user preset
made against the base release disappears from printers it was used on. The
app offers to write these printers into such user presets. Requiring a same-id
successor leaves out printers a filament was deliberately taken off, and
requiring a shared extruder variant keeps a printer from picking up another
variant's values.
Keyed by the current preset name: the app resolves a renamed parent
before the map is consulted. Printer names are translated through the
printers' renamed_from. Returns {preset name: sorted printer names}.
"""
old_filaments, _ = _flatten_presets(base_dir)
new_filaments, new_printers = _flatten_presets(profiles_dir)
renamed_printer = {old: new for new, rec in new_printers.items() for old in rec["renamed_from"]}
old_by_name = {}
for (vendor, name), rec in old_filaments.items():
old_by_name.setdefault(name, []).append((vendor, rec))
covering = {} # filament_id -> printers some current instantiated preset covers
for rec in new_filaments.values():
if rec["instantiation"] and rec["filament_id"]:
covering.setdefault(rec["filament_id"], set()).update(rec["compatible_printers"])
def shares_variant(filament, printer):
variants = new_printers[printer]["variants"]
return not filament["variants"] or not variants or bool(filament["variants"] & variants)
result = {}
for (vendor, name), rec in sorted(new_filaments.items()):
# An empty list means every printer, so nothing can have been lost.
if not rec["instantiation"] or not rec["compatible_printers"]:
continue
old_printers = set()
for old_name in [name] + rec["renamed_from"]:
candidates = old_by_name.get(old_name, [])
same_vendor = [r for v, r in candidates if v == vendor]
for old in same_vendor or [r for _, r in candidates]:
old_printers |= old["compatible_printers"]
old_printers = {renamed_printer.get(p, p) for p in old_printers}
lost = old_printers - rec["compatible_printers"]
lost &= covering.get(rec["filament_id"], set()) & set(new_printers)
lost = {p for p in lost if shares_variant(rec, p)}
if lost:
result[name] = sorted(lost)
return result
def run_legacy_compat(base_dir, profiles_dir, output, check):
presets = legacy_compatible_printers(base_dir, profiles_dir)
if check:
try:
with open(output, encoding="utf-8") as f:
current = json.load(f).get("presets")
except (OSError, ValueError, AttributeError):
current = None
if current != presets:
print_error(f"{output} is out of date; rerun legacy-compat --base {base_dir}")
return 1
print_success(f"{output} is up to date")
return 0
with open(output, "w", encoding="utf-8", newline="\n") as f:
f.write(json.dumps({"presets": presets}, indent=4, sort_keys=True, ensure_ascii=False) + "\n")
print_info(f"wrote {len(presets)} presets / {sum(map(len, presets.values()))} printers to {output}")
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -3222,17 +3093,11 @@ examples:
preview resizing the variant arrays check rejects in that bundle
orca_profile_tool.py check --strict --vendor BBL
also hold every preset's inherited variant arrays to its own width
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles
rewrite resources/info/legacy_compatible_printers.json against 2.4.2's
profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2)
after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> check
normalize supplies missing types; update-index registers presets before id
generation.
After renaming filament presets or narrowing their compatible_printers, also
rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2
can be migrated back to their printers.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
"""
@@ -3362,22 +3227,6 @@ def build_parser():
"Use trim only for deliberate unindexed-file cleanup, previewed with\n"
"--dry-run; it can also delete newly authored presets.")
legacy_cmd = add(
"legacy-compat", [profiles_opt],
"write the printers user presets keep from an older release",
"Compare the tree with an older release's profiles and write, per current\n"
"filament preset, the printers it covered then and no longer lists, where a\n"
"preset with the same filament_id still covers them. The loader adds these\n"
"to user presets that inherit the preset, so a narrowed compatible_printers\n"
"does not hide them on printers they were made for.")
legacy_cmd.add_argument("--base", required=True, metavar="DIR",
help="the older release's resources/profiles directory")
legacy_cmd.add_argument("--output", default=LEGACY_COMPAT_PATH, metavar="FILE",
help="map to write (default: resources/info/"
"legacy_compatible_printers.json)")
legacy_cmd.add_argument("--check", action="store_true",
help="fail if the map differs from what --base implies; write nothing")
return parser
@@ -3411,9 +3260,6 @@ def main(argv=None):
return run_generate_id(profiles_dir, vendors, args.filament_id, args.setting_id,
args.dry_run)
if args.command == "legacy-compat":
return run_legacy_compat(args.base, profiles_dir, args.output, args.check)
if args.command == "fix-variant":
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
strict=args.strict)
-91
View File
@@ -1258,97 +1258,6 @@ class TestVariantNames(TreeCase):
self.assertIn('holds the legacy variant "Direct Drive Normal"', out)
# ---------------------------------------------------------------------------
# legacy-compat
# ---------------------------------------------------------------------------
class TestLegacyCompat(unittest.TestCase):
"""A base release and the current tree, each with one vendor."""
def setUp(self):
self.base, self.cur = Tree(), Tree()
self.addCleanup(self.base.cleanup)
self.addCleanup(self.cur.cleanup)
@staticmethod
def printer(tree, name, **extra):
tree.write("V", f"machine/{name}.json",
{"type": "machine", "name": name, "instantiation": "true", **extra})
tree.index("V", "machine", name, f"machine/{name}.json")
@staticmethod
def filament(tree, name, printers, filament_id="OFpetg01", **extra):
tree.write("V", f"filament/{name}.json",
{"type": "filament", "name": name, "instantiation": "true",
"filament_id": filament_id, "compatible_printers": printers, **extra})
tree.index("V", "filament", name, f"filament/{name}.json")
def generate(self):
return apt.legacy_compatible_printers(self.base.profiles, self.cur.profiles)
def test_renamed_and_narrowed_preset_keeps_printers_a_same_id_sibling_covers(self):
for tree in (self.base, self.cur):
for p in ("A 0.4", "A 0.6", "B 0.4"):
self.printer(tree, p)
self.filament(self.base, "PETG @A-all", ["A 0.4", "A 0.6", "B 0.4"])
self.filament(self.cur, "PETG @V A-all", ["A 0.6"], renamed_from="PETG @A-all;PETG A-all")
self.filament(self.cur, "PETG @A 0.4", ["A 0.4"])
self.assertEqual(self.generate(), {"PETG @V A-all": ["A 0.4"]})
def test_printer_without_a_successor_is_left_out(self):
for tree in (self.base, self.cur):
for p in ("A 0.4", "A 0.6"):
self.printer(tree, p)
self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"])
self.filament(self.cur, "PETG @A", ["A 0.6"])
self.filament(self.cur, "Other @A", ["A 0.4"], filament_id="OFother1")
self.assertEqual(self.generate(), {})
def test_printer_names_follow_printer_renames(self):
self.printer(self.base, "Old 0.4")
self.printer(self.base, "A 0.6")
self.printer(self.cur, "New 0.4", renamed_from="Old 0.4")
self.printer(self.cur, "A 0.6")
self.filament(self.base, "PETG @A", ["Old 0.4", "A 0.6"])
self.filament(self.cur, "PETG @A", ["A 0.6"])
self.filament(self.cur, "PETG @New", ["New 0.4"])
self.assertEqual(self.generate(), {"PETG @A": ["New 0.4"]})
def test_printer_without_a_shared_extruder_variant_is_left_out(self):
for tree in (self.base, self.cur):
self.printer(tree, "Direct 0.4", printer_extruder_variant=["Direct Drive Standard"])
self.printer(tree, "Bowden 0.4", printer_extruder_variant=["Bowden Standard"])
self.filament(self.base, "PETG @all", ["Direct 0.4", "Bowden 0.4"])
self.filament(self.cur, "PETG @all", ["Direct 0.4"],
filament_extruder_variant=["Direct Drive Standard"])
self.filament(self.cur, "PETG @Bowden", ["Bowden 0.4"])
self.assertEqual(self.generate(), {})
def test_preset_now_listing_no_printer_is_skipped(self):
for tree in (self.base, self.cur):
self.printer(tree, "A 0.4")
self.filament(self.base, "PETG @A", ["A 0.4"])
self.filament(self.cur, "PETG @A", [])
self.assertEqual(self.generate(), {})
def test_check_passes_on_a_current_map_and_fails_on_a_stale_one(self):
for tree in (self.base, self.cur):
for p in ("A 0.4", "A 0.6"):
self.printer(tree, p)
self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"])
self.filament(self.cur, "PETG @A", ["A 0.6"])
self.filament(self.cur, "PETG @A 0.4", ["A 0.4"])
output = os.path.join(self.cur.dir, "map.json")
argv = ["legacy-compat", "--base", self.base.profiles, "--profiles", self.cur.profiles,
"--output", output]
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(apt.main(argv), 0)
self.assertEqual(apt.main(argv + ["--check"]), 0)
with open(output, "w", encoding="utf-8") as f:
json.dump({"presets": {}}, f)
self.assertEqual(apt.main(argv + ["--check"]), 1)
class TestDispatch(TreeCase):
def test_each_command_reaches_its_own_writer(self):
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
+4 -168
View File
@@ -22,7 +22,6 @@
#include "libslic3r/BrimEarsPoint.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r_version.h"
#include "bbs_3mf.hpp"
@@ -355,13 +354,6 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
// BBS
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
@@ -855,9 +847,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector<std::string> custom_seam;
std::vector<std::string> mmu_segmentation;
std::vector<std::string> fuzzy_skin;
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
// BBS
std::vector<std::string> face_properties;
@@ -878,8 +867,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
custom_seam.clear();
mmu_segmentation.clear();
fuzzy_skin.clear();
for (std::vector<std::string> &slot : texture_displacement)
slot.clear();
}
};
@@ -1213,11 +1200,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
/*IdToSlaSupportPointsMap m_sla_support_points;
IdToSlaDrainHolesMap m_sla_drain_holes;*/
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
// are built only after the whole archive has been walked, so by the time a volume names its JSON
// every file it could refer to is already in here, whatever order the archive happened to be in.
std::map<std::string, std::string> m_texture_displacement_files;
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
std::string m_curr_metadata_name;
std::string m_curr_characters;
std::string m_name;
@@ -1747,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//plate->pattern_file = it->second->pattern_file;
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
plate->top_file = it->second->top_file;
plate->pick_file = it->second->pick_file;
plate->pattern_bbox_file = it->second->pattern_bbox_file;
plate->pick_file = it->second->pick_file.empty();
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
plate->config = it->second->config;
if (!plate->thumbnail_file.empty())
@@ -2091,13 +2073,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (_is_svg_shape_file(name)) {
_extract_embossed_svg_shape_file(name, archive, stat);
}
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
std::string contents(stat.m_uncomp_size, '\0');
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
m_texture_displacement_files.emplace(name, std::move(contents));
else
add_error("Error while reading texture displacement data");
}
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
m_parsing_slice_info = true;
//extract slice info from archive
@@ -3335,45 +3310,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}*/
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
// being built, which is after the whole archive has been walked - so every file it can refer to is
// already extracted, and no deferral is needed.
//
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
// picking the texture again rather than a hard failure.
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
{
const auto json = m_texture_displacement_files.find(json_path);
if (json == m_texture_displacement_files.end()) {
add_error("Missing texture displacement data: " + json_path);
return;
}
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementOptions options;
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
add_error("Malformed texture displacement data: " + json_path);
return;
}
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
// the project must never read the file it names: that would let whoever made the project pick any
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
// `path` stays a label of where the texture came from; a layer the archive carries no image for
// loads without one, its settings intact, until the user picks a texture again.
for (TextureDisplacementLayer &layer : layers) {
if (layer.path_in_3mf.empty())
continue;
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
if (image == m_texture_displacement_files.end()) {
add_error("Missing texture displacement image: " + layer.path_in_3mf);
continue;
}
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
}
volume.texture_displacement_layers = std::move(layers);
volume.texture_displacement_options = options;
}
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
@@ -4030,12 +3966,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
mask.resize(m_curr_object->geometry.triangles.size());
mask.back() = paint;
}
// BBS
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
}
@@ -5387,18 +5317,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->mmu_segmentation_facets.touch();
volume->fuzzy_skin_facets.shrink_to_fit();
volume->fuzzy_skin_facets.touch();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
if (mask.empty())
continue; // no triangle painted on this slot
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
facets.reserve(triangles_count);
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
if (!mask[i].empty())
facets.set_triangle_from_string(i, mask[i]);
facets.shrink_to_fit();
facets.touch();
}
}
volume->set_type(volume_data->part_type);
@@ -5414,8 +5332,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY)
volume->name = metadata.value;
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
_apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format
@@ -5580,28 +5496,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->seam_facets.shrink_to_fit();
volume->mmu_segmentation_facets.shrink_to_fit();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
if (mask.empty())
continue; // written by a build without the feature
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
facets.reserve(triangles_count);
for (size_t i = 0; i < triangles_count; ++i) {
const size_t index = volume_data.first_triangle_id + i;
if (index < mask.size() && !mask[index].empty())
facets.set_triangle_from_string(i, mask[index]);
}
facets.shrink_to_fit();
facets.touch();
}
volume->set_type(volume_data.part_type);
// apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY)
volume->name = metadata.value;
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
_apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format
@@ -5905,12 +5805,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
mask.resize(current_object->geometry.triangles.size());
mask.back() = paint;
}
// BBS
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
}
@@ -6914,9 +6808,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
stream << "</Types>";
@@ -7341,14 +7232,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (iter != m_shared_meshes.end())
{
const ModelVolume* shared_volume = iter->second.second;
bool same_texture_paint = true;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
if ((shared_volume->supported_facets.equals(volume->supported_facets))
&& (shared_volume->seam_facets.equals(volume->seam_facets))
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
&& same_texture_paint)
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
{
auto data = iter->second.first;
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
@@ -7764,20 +7651,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
output_buffer += "\"";
}
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
// not know, so a project written here still opens in a build without the feature - it just
// loses the paint, which is also all it could have done with it.
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
if (texture_paint.empty())
continue;
output_buffer += " ";
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
output_buffer += "=\"";
output_buffer += texture_paint;
output_buffer += "\"";
}
// BBS
if (i < its.properties.size()) {
std::string prop_str = its.properties[i].to_string();
@@ -8227,42 +8100,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return file_path;
}
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
// every reader has to parse just to list the objects.
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
const std::string &id)
{
if (volume.texture_displacement_layers.empty())
return;
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
// being exported is const (and may be saved again, elsewhere, with different paths).
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
for (TextureDisplacementLayer &layer : layers) {
layer.path_in_3mf.clear();
if (!layer.image_data || layer.image_data->empty())
continue;
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
// No deflate: a PNG is already compressed, so a second pass only costs time.
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
MZ_NO_COMPRESSION))
layer.path_in_3mf = path;
}
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
return;
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
}
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
{
std::stringstream stream;
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
@@ -8373,8 +8211,6 @@ bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archiv
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
}
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
tc.has_value())
+1 -1
View File
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_is_role_based_fan_on.fill(false);
m_role_based_fan_marker_layer.fill(-1);
m_fan_mover.reset();
m_fan_mover.release();
m_ordering_cache.clear();
// Belt printer: initialize belt-specific writer via virtual hook.
+349
View File
@@ -0,0 +1,349 @@
#include "JumpPointSearch.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Point.hpp"
#include "libslic3r/AStar.hpp"
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#include <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <oneapi/tbb/scalable_allocator.h>
//#define DEBUG_FILES
#ifdef DEBUG_FILES
#include "libslic3r/SVG.hpp"
#endif
namespace Slic3r {
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
coord_t dx = abs(x1 - x0);
coord_t dy = abs(y1 - y0);
coord_t x = x0;
coord_t y = y0;
coord_t n = 1 + dx + dy;
coord_t x_inc = (x1 > x0) ? 1 : -1;
coord_t y_inc = (y1 > y0) ? 1 : -1;
coord_t error = dx - dy;
dx *= 2;
dy *= 2;
for (; n > 0; --n) {
if (!fn(x, y)) return;
if (error > 0) {
x += x_inc;
error -= dy;
} else {
y += y_inc;
error += dx;
}
}
}
// will draw the line twice, second time with and offset of 1 in the direction of normal
// may call the fn on the same coordiantes multiple times!
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
normal.x() = ceil(normal.x());
normal.y() = ceil(normal.y());
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
dda(x0, y0, x1, y1, fn);
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
}
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
{
public:
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
struct Node
{
CellPositionType position;
CellPositionType incoming_dir;
};
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
private:
CellPositionType target;
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
{
CellPositionType next = start + forward_dir;
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
if (is_passable(next)) {
return next;
} else {
return start;
}
}
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
{
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
return false;
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
return false;
}
}
public:
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
{
const CellPositionType & pos = from.position;
const CellPositionType & forward_dir = from.incoming_dir;
std::vector<CellPositionType> dirs_to_check{};
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
dirs_to_check = all_directions;
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
}
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
}
dirs_to_check.push_back(horizontal_check_dir);
dirs_to_check.push_back(vertical_check_dir);
dirs_to_check.push_back(forward_dir);
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
dirs_to_check.push_back(forward_dir);
}
for (const CellPositionType &dir : dirs_to_check) {
CellPositionType jp = find_jump_point(pos, dir);
if (jp != pos) fn(Node{jp, dir});
}
}
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
};
void JPSPathFinder::clear()
{
inpassable.clear();
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
add_obstacles(bed_shape);
}
void JPSPathFinder::add_obstacles(const Lines &obstacles)
{
auto store_obstacle = [&](coord_t x, coord_t y) {
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
inpassable.insert(Pixel{x, y});
return true;
};
for (const Line &l : obstacles) {
Pixel start = pixelize(l.a);
Pixel end = pixelize(l.b);
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
}
}
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
{
Pixel start = pixelize(p0);
Pixel end = pixelize(p1);
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
if (inpassable.find(start) != inpassable.end()) {
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
start = Pixel(x, y);
return false;
}
return true;
});
}
if (inpassable.find(end) != inpassable.end()) {
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
end = Pixel(x, y);
return false;
}
return true;
});
}
BoundingBox search_box = max_search_box;
search_box.max -= Pixel(1, 1);
search_box.min += Pixel(1, 1);
BoundingBox bounding_square(Points{start, end});
bounding_square.max += Pixel(5, 5);
bounding_square.min -= Pixel(5, 5);
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
search_box.max = search_box.max.cwiseMin(bounding_square.max);
search_box.min = search_box.min.cwiseMax(bounding_square.min);
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
std::unordered_map<size_t, QNode> astar_cache{};
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
std::vector<decltype(tracer)::Node> out_nodes;
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
// path not found - just reconstruct the best path from astar cache.
// Note that astar_cache is NOT empty - at least the starting point should always be there
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
std::vector<size_t> keys;
keys.reserve(astar_cache.size());
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
out_path.push_back(end);
while (closest_qnode != astar::Unassigned) {
out_path.push_back(astar_cache[closest_qnode].node.position);
closest_qnode = astar_cache[closest_qnode].parent;
}
} else {
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
out_path.push_back(start);
}
#ifdef DEBUG_FILES
auto scaled_points = [](const Points &ps) {
Points r;
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
return r;
};
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
svg.draw(scaled_point(end), "purple", scale_(0.4));
svg.draw(scaled_point(start), "green", scale_(0.4));
#endif
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
tmp_path.reserve(out_path.size());
// Some path found, reverse and remove points that do not change direction
std::reverse(out_path.begin(), out_path.end());
{
tmp_path.push_back(out_path.front()); // first point
for (size_t i = 1; i < out_path.size() - 1; i++) {
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
#endif
tmp_path.clear();
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
// insert the Px+k point to result and continue search from Px+k
{
tmp_path.push_back(out_path.front()); // first point
size_t index_of_last_stored_point = 0;
for (size_t i = 1; i < out_path.size(); i++) {
if (i - index_of_last_stored_point < 2) continue;
bool passable = true;
auto store_obstacle = [&](coord_t x, coord_t y) {
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
passable = false;
return false;
}
return true;
};
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
if (!passable) {
tmp_path.push_back(out_path[i - 1]);
index_of_last_stored_point = i - 1;
}
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
svg.Close();
#endif
// before returing the path, transform it from pixels back to points.
// Also replace the first and last pixel by input points so that result path patches input params exactly.
for (Pixel &p : out_path) { p = unpixelize(p); }
out_path.front() = p0;
out_path.back() = p1;
return Polyline(out_path);
}
} // namespace Slic3r
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "Line.hpp"
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#include "BoundingBox.hpp"
#include "Polygon.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
class JPSPathFinder
{
using Pixel = Point;
std::unordered_set<Pixel, PointHash> inpassable;
coordf_t print_z;
BoundingBox max_search_box;
Lines bed_shape;
const coord_t resolution = scaled(1.5);
Pixel pixelize(const Point &p) { return p / resolution; }
Point unpixelize(const Pixel &p) { return p * resolution; }
public:
JPSPathFinder() = default;
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
void clear();
void add_obstacles(const Lines &obstacles);
Polyline find_path(const Point &start, const Point &end);
};
} // namespace Slic3r
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
-90
View File
@@ -81,7 +81,6 @@
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include "nlohmann/json.hpp"
#include <sstream>
#include "PlaceholderParser.hpp"
@@ -2729,95 +2728,6 @@ void PresetCollection::update_after_user_presets_loaded()
return;
}
const Preset *PresetCollection::system_parent_with_inherited_printers(const Preset &preset) const
{
if (!preset.is_user() || preset.inherits().empty())
return nullptr;
const Preset *parent = get_preset_parent(preset);
if (parent == nullptr || !parent->is_system)
return nullptr;
// A user preset that sets its own compatible_printers keeps exactly those.
const auto *own = preset.config.option<ConfigOptionStrings>("compatible_printers");
const auto *inherited = parent->config.option<ConfigOptionStrings>("compatible_printers");
return own != nullptr && inherited != nullptr && own->values == inherited->values ? parent : nullptr;
}
std::vector<LegacyPrinterMigration> PresetCollection::legacy_printer_migrations(const std::map<std::string, std::vector<std::string>> &printers_by_parent) const
{
std::vector<LegacyPrinterMigration> migrations;
for (const Preset &preset : m_presets) {
const Preset *parent = system_parent_with_inherited_printers(preset);
if (parent == nullptr)
continue;
// An empty list already covers every printer.
const std::vector<std::string> &own = preset.config.option<ConfigOptionStrings>("compatible_printers")->values;
if (own.empty())
continue;
// The map names the parent as it was when the map was written; a later rename keeps that name in renamed_from.
auto it = printers_by_parent.find(parent->name);
for (auto old_name = parent->renamed_from.begin(); it == printers_by_parent.end() && old_name != parent->renamed_from.end(); ++old_name)
it = printers_by_parent.find(*old_name);
if (it == printers_by_parent.end())
continue;
LegacyPrinterMigration migration{preset.name, parent->name, {}};
for (const std::string &printer : it->second)
if (std::find(own.begin(), own.end(), printer) == own.end())
migration.printers.push_back(printer);
if (!migration.printers.empty())
migrations.push_back(std::move(migration));
}
return migrations;
}
bool PresetCollection::apply_legacy_printer_migration(const LegacyPrinterMigration &migration)
{
lock();
auto it = this->find_preset_internal(migration.preset);
if (it == m_presets.end() || it->name != migration.preset || system_parent_with_inherited_printers(*it) == nullptr) {
unlock();
return false;
}
Preset &preset = *it;
std::vector<std::string> printers = preset.config.option<ConfigOptionStrings>("compatible_printers")->values;
for (const std::string &printer : migration.printers)
if (std::find(printers.begin(), printers.end(), printer) == printers.end())
printers.push_back(printer);
const bool selected = m_idx_selected == static_cast<size_t>(it - m_presets.begin());
unlock();
// Only compatible_printers changes in the file. Saving the whole preset would also rewrite an inherits that
// names a renamed parent by its old name, the only name older releases know it by.
{
InstanceLock instance_lock(user_presets_lock_path());
try {
boost::nowide::ifstream in(preset.file);
json j = json::parse(in);
j["compatible_printers"] = printers;
if (const std::error_code ec = write_file_atomically(preset.file, j.dump(1, '\t'))) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << preset.file << ": " << ec.message();
return false;
}
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not migrate " << preset.file << ": " << err.what();
return false;
}
}
lock();
preset.config.option<ConfigOptionStrings>("compatible_printers")->values = printers;
if (selected)
m_edited_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values = printers;
unlock();
// Push the change to the cloud copy, as an edit in the settings tab would.
if (!preset.setting_id.empty()) {
preset.sync_info = "update";
preset.save_info();
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << preset.name << " gets " << migration.printers.size()
<< " printer(s) its parent listed in an older release";
return true;
}
//BBS: validate_preset
bool PresetCollection::validate_preset(const std::string &preset_name, std::string &inherit_name)
{
-20
View File
@@ -551,14 +551,6 @@ struct PresetConfigSubstitutions {
// PrusaSlicer and reading the user Print / Filament / Printer profiles.
using PresetsConfigSubstitutions = std::vector<PresetConfigSubstitutions>;
// A user filament preset that no longer shows on printers its system parent listed in an older release,
// and those printers. Migrating it writes them into the preset's own compatible_printers.
struct LegacyPrinterMigration {
std::string preset;
std::string parent;
std::vector<std::string> printers;
};
// Collections of presets of the same type (one of the Print, Filament or Printer type).
class PresetCollection
{
@@ -609,14 +601,6 @@ public:
void save_user_presets(const std::string& dir_path, const std::string& type, std::map<std::string, std::string>& need_to_delete_list);
bool load_user_preset(std::string name, std::map<std::string, std::string> preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin = PresetOrigin(PresetOrigin::Kind::User));
void update_after_user_presets_loaded();
// User presets store only their diff, so a user preset inherits its system parent's compatible_printers
// and disappears from a printer once a profile update narrows that list. Lists the user presets that
// inherit the list and the printers their parent covered in an older release (printers_by_parent, keyed
// by the parent's name then) but no longer lists.
std::vector<LegacyPrinterMigration> legacy_printer_migrations(const std::map<std::string, std::vector<std::string>> &printers_by_parent) const;
// Adds the migration's printers to the preset's own compatible_printers and saves the preset to disk.
// False if the preset no longer inherits its parent's list or could not be saved.
bool apply_legacy_printer_migration(const LegacyPrinterMigration &migration);
//BBS: get user presets
int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets);
void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id);
@@ -909,10 +893,6 @@ public:
size_t num_default_presets() { return m_num_default_presets; }
protected:
// The system parent of a user preset whose compatible_printers is the parent's, or nullptr: the presets
// legacy_printer_migrations() considers.
const Preset* system_parent_with_inherited_printers(const Preset &preset) const;
PresetCollection() = default;
// Deleted by the std::recursive_mutex member. PresetBundle copies by assignment.
PresetCollection(const PresetCollection &other) = delete;
-81
View File
@@ -2,7 +2,6 @@
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/directory.hpp>
#include <boost/filesystem/file_status.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/optional/optional.hpp>
#include <boost/algorithm/string/split.hpp>
@@ -25,7 +24,6 @@
#include <memory>
#include <initializer_list>
#include <map>
#include <mutex>
#include <iterator>
#include <functional>
#include <limits>
@@ -40,8 +38,6 @@
#include "ParallelResolve.hpp"
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#include "Time.hpp"
#include "Zipper.hpp"
#include "PrintConfig.hpp"
#include "PublishSettings.hpp"
#include "FilamentMixer.hpp"
@@ -100,34 +96,6 @@ using json = nlohmann::json;
namespace Slic3r {
// Per current system filament preset, the printers it listed in an older release and no longer does
// (resources/info/legacy_compatible_printers.json, written by orca_profile_tool.py legacy-compat).
// The file ships with the build, so it is read once it opens; until then (e.g. presets loaded before
// resources_dir() is set) every call tries again and there are no legacy printers.
static std::map<std::string, std::vector<std::string>> legacy_compatible_printers()
{
static std::mutex mutex;
static std::map<std::string, std::vector<std::string>> printers_by_parent;
static bool read = false;
std::lock_guard<std::mutex> guard(mutex);
if (!read) {
const fs::path path = fs::path(resources_dir()) / "info" / "legacy_compatible_printers.json";
boost::nowide::ifstream in(path.string());
if (!in) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << path.string() << " not found";
return {};
}
read = true;
try {
printers_by_parent = json::parse(in).at("presets").get<std::map<std::string, std::vector<std::string>>>();
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not read " << path.string() << ": " << err.what();
}
}
return printers_by_parent;
}
// Project-level options imported from a loaded 3MF into project_config. s_project_options_published
// below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync.
// s_project_options_published additionally carries wipe_tower_rotation_angle, which normal
@@ -1077,55 +1045,6 @@ bool PresetBundle::backup_user_folder() const
}
}
bool PresetBundle::backup_user_folder_to_zip(const std::string &zip_path) const
{
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << zip_path;
try {
Zipper zipper(zip_path);
// Directories get their own entries, which extract_archive_confined() needs to recreate them.
zipper.add_entry(std::string(PRESET_USER_DIR) + "/", nullptr, 0);
for (const fs::directory_entry &entry : fs::recursive_directory_iterator(user_dir)) {
const std::string name = (fs::path(PRESET_USER_DIR) / entry.path().lexically_relative(user_dir)).generic_string();
if (fs::is_directory(entry.status())) {
zipper.add_entry(name + "/", nullptr, 0);
continue;
}
if (!fs::is_regular_file(entry.status()))
continue;
boost::nowide::ifstream in(entry.path().string(), std::ios::binary);
const std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!in.good() && !in.eof())
throw Slic3r::RuntimeError("can not read " + entry.path().string());
zipper.add_entry(name, data.data(), data.size());
}
zipper.finalize();
return true;
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what();
boost::system::error_code ec;
fs::remove(zip_path, ec);
return false;
}
}
std::vector<LegacyPrinterMigration> PresetBundle::legacy_printer_migrations() const
{
return filaments.legacy_printer_migrations(legacy_compatible_printers());
}
std::string PresetBundle::migrate_legacy_printers(const std::vector<LegacyPrinterMigration> &migrations, std::vector<std::string> &failed)
{
const std::string zip_path = data_dir() + "/filament_preset_migration_" +
Utils::time2str(Utils::TimeZone::utc, Utils::TimeFormat::iso8601Z) + "_backup.zip";
if (!backup_user_folder_to_zip(zip_path))
return {};
for (const LegacyPrinterMigration &migration : migrations)
if (!filaments.apply_legacy_printer_migration(migration))
failed.push_back(migration.preset);
return zip_path;
}
std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const
{
if (filament_id.empty())
-10
View File
@@ -376,16 +376,6 @@ public:
bool use_bbl_device_tab();
bool backup_user_folder() const;
// Writes the user preset folder into a zip archive at zip_path, its entries under "user/".
bool backup_user_folder_to_zip(const std::string &zip_path) const;
// User filament presets that no longer show on printers their parent listed in an older release
// (resources/info/legacy_compatible_printers.json).
std::vector<LegacyPrinterMigration> legacy_printer_migrations() const;
// Backs up the user preset folder into a zip in data_dir(), then writes each migration's printers into its
// preset file. Returns the zip's path, or an empty string when the backup failed and nothing was migrated.
// The presets that could not be migrated are added to failed.
std::string migrate_legacy_printers(const std::vector<LegacyPrinterMigration> &migrations, std::vector<std::string> &failed);
//BBS: project embedded preset logic
PresetsConfigSubstitutions load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule);
+298 -260
View File
@@ -19,7 +19,6 @@
#include <mutex>
#include <numeric>
#include <optional>
#include "nlohmann/json.hpp"
#include <queue>
#include <ratio>
#include <string>
@@ -116,6 +115,20 @@ float DecodedHeightTexture::sample(const Vec2f &uv, bool tile_enabled, TextureTi
return top * (1.f - ty) + bottom * ty;
}
float DecodedHeightTexture::sample_alpha(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
{
TexelTap tap;
if (!has_alpha())
return 1.f; // no coverage plane means the picture is everywhere, which is the usual case
if (!texel_tap(uv, tile_enabled, tile_method, tap))
return 0.f;
auto at = [this](int x, int y) { return float(alpha[size_t(y) * size_t(width) + size_t(x)]) / 255.f; };
const float top = at(tap.x0, tap.y0) * (1.f - tap.tx) + at(tap.x1, tap.y0) * tap.tx;
const float bottom = at(tap.x0, tap.y1) * (1.f - tap.tx) + at(tap.x1, tap.y1) * tap.tx;
return top * (1.f - tap.ty) + bottom * tap.ty;
}
Vec3f DecodedHeightTexture::sample_color(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
{
TexelTap tap;
@@ -262,6 +275,272 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
return decode_height_texture(layer).has_color();
}
namespace {
// Unsharp mask over the colour: blur a copy and push each texel away from it. The blur is the same
// sliding-window helper the Smoothing slider uses, run per channel, so the cost is the image size
// rather than the image size times the radius.
//
// Worth having here for a reason it would not be in a photo editor: the picture is about to be
// reduced to a handful of flat colours, and a soft edge between two of them quantizes into a ragged
// band of both. Sharpening first makes that boundary land in one place.
void sharpen_rgb(DecodedHeightTexture &tex, float amount)
{
if (!tex.has_color() || amount <= 0.f)
return;
const size_t n = size_t(tex.width) * size_t(tex.height);
// A small radius, scaled with the image so a 4k picture is not sharpened a tenth as hard as a
// 512 one. Deliberately not tied to `amount`: that is how much, this is how wide.
const int radius = std::max(1, int(std::lround(double(std::min(tex.width, tex.height)) / 512.0)));
const float k = std::clamp(amount, 0.f, 1.f) * 1.5f;
std::vector<uint8_t> plane(n);
for (int c = 0; c < 3; ++c) {
for (size_t i = 0; i < n; ++i)
plane[i] = tex.rgb[i * 3 + size_t(c)];
smooth_height_pixels_box(plane, tex.width, tex.height, radius);
for (size_t i = 0; i < n; ++i) {
const float sharp = float(tex.rgb[i * 3 + size_t(c)]) + k * (float(tex.rgb[i * 3 + size_t(c)]) - float(plane[i]));
tex.rgb[i * 3 + size_t(c)] = uint8_t(std::lround(std::clamp(sharp, 0.f, 255.f)));
}
}
}
// Reduces the picture to at most `count` colours, by k-means in RGB.
//
// "At most": two of the chosen colours can still match the same filament, in which case the print
// uses fewer. The guarantee runs the right way round, which is the one that matters - asking for
// three colours cannot produce four.
//
// k-means rather than a fixed palette or an even split of the luma range, because the useful
// question is which colours this picture is actually made of. A tartan asks for its own handful of
// greens and reds; posterizing the range would hand it eight shades of mud instead. The clustering
// runs over a coarse histogram, not over the pixels: at 1024 x 1024 that is 4096 weighted points
// instead of a million, which is what makes it cheap enough to sit on a slider.
void reduce_colors(DecodedHeightTexture &tex, int count)
{
if (!tex.has_color() || count < 2 || count > 64)
return;
const size_t n = size_t(tex.width) * size_t(tex.height);
// 16 levels per channel, each bin keeping the mean of what fell in it, so a colour is not
// snapped to a bin corner before the clustering has had a say.
struct Bin { double r = 0, g = 0, b = 0; uint32_t n = 0; };
std::vector<Bin> bins(size_t(16 * 16 * 16));
for (size_t i = 0; i < n; ++i) {
const uint8_t r = tex.rgb[i * 3], g = tex.rgb[i * 3 + 1], b = tex.rgb[i * 3 + 2];
Bin &bin = bins[(size_t(r >> 4) << 8) | (size_t(g >> 4) << 4) | size_t(b >> 4)];
bin.r += r; bin.g += g; bin.b += b; ++bin.n;
}
struct Point { float c[3]; double w; };
std::vector<Point> pts;
pts.reserve(bins.size());
for (const Bin &bin : bins)
if (bin.n > 0)
pts.push_back({ { float(bin.r / bin.n), float(bin.g / bin.n), float(bin.b / bin.n) }, double(bin.n) });
if (pts.size() <= size_t(count))
return; // already no more colours than asked for
// Seeded the k-means++ way, deterministically: the heaviest point first, then each next one is
// whichever point is furthest from everything chosen so far, weighted by how much of the image
// it is. Picking the N fullest bins instead would put every centre inside the one dominant
// colour of a picture that has one.
std::vector<std::array<float, 3>> centre;
{
size_t best = 0;
for (size_t i = 1; i < pts.size(); ++i)
if (pts[i].w > pts[best].w)
best = i;
centre.push_back({ pts[best].c[0], pts[best].c[1], pts[best].c[2] });
std::vector<double> d2(pts.size(), std::numeric_limits<double>::max());
while (int(centre.size()) < count) {
const auto &last = centre.back();
double top = -1.0;
size_t pick = 0;
for (size_t i = 0; i < pts.size(); ++i) {
const double dr = pts[i].c[0] - last[0], dg = pts[i].c[1] - last[1], db = pts[i].c[2] - last[2];
d2[i] = std::min(d2[i], dr * dr + dg * dg + db * db);
if (const double score = d2[i] * pts[i].w; score > top) {
top = score;
pick = i;
}
}
if (top <= 0.0)
break; // fewer distinct colours than asked for
centre.push_back({ pts[pick].c[0], pts[pick].c[1], pts[pick].c[2] });
}
}
// Lloyd's algorithm over the weighted bins. Twelve passes: on a histogram this coarse it has
// stopped moving well before that, and a fixed count keeps the slider's cost predictable.
const int k = int(centre.size());
for (int iter = 0; iter < 12; ++iter) {
std::vector<std::array<double, 3>> sum(size_t(k), { 0.0, 0.0, 0.0 });
std::vector<double> wsum(size_t(k), 0.0);
for (const Point &p : pts) {
int best = 0;
double bd = std::numeric_limits<double>::max();
for (int c = 0; c < k; ++c) {
const double dr = p.c[0] - centre[size_t(c)][0], dg = p.c[1] - centre[size_t(c)][1],
db = p.c[2] - centre[size_t(c)][2];
if (const double d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
}
for (int ch = 0; ch < 3; ++ch)
sum[size_t(best)][size_t(ch)] += double(p.c[ch]) * p.w;
wsum[size_t(best)] += p.w;
}
for (int c = 0; c < k; ++c)
if (wsum[size_t(c)] > 0.0)
for (int ch = 0; ch < 3; ++ch)
centre[size_t(c)][size_t(ch)] = float(sum[size_t(c)][size_t(ch)] / wsum[size_t(c)]);
}
// Every texel takes its nearest centre. Parallel by rows, as the tonal pass is.
const size_t rows = size_t(tex.height), cols = size_t(tex.width);
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
for (size_t y = range.begin(); y < range.end(); ++y)
for (size_t x = 0; x < cols; ++x) {
uint8_t *px = tex.rgb.data() + (y * cols + x) * 3;
int best = 0;
float bd = std::numeric_limits<float>::max();
for (int c = 0; c < k; ++c) {
const float dr = float(px[0]) - centre[size_t(c)][0], dg = float(px[1]) - centre[size_t(c)][1],
db = float(px[2]) - centre[size_t(c)][2];
if (const float d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
}
for (int ch = 0; ch < 3; ++ch)
px[size_t(ch)] = uint8_t(std::lround(std::clamp(centre[size_t(best)][size_t(ch)], 0.f, 255.f)));
}
});
}
// Keys one colour out, so a logo on a white card prints as the logo and the object's own colour
// shows through the rest.
//
// The corners are 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; the median of the four is taken so a single
// stray corner cannot decide it. The panel's dropper and swatch override it when the picture needs
// something else keyed out.
void remove_background(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
{
if (!tex.has_color() || !layer.bg_remove || tex.width < 2 || tex.height < 2)
return;
const size_t n = size_t(tex.width) * size_t(tex.height);
float key[3];
if (layer.bg_from_corners) {
const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width),
n - 1 };
for (int c = 0; c < 3; ++c) {
std::array<float, 4> v{};
for (int i = 0; i < 4; ++i)
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
std::sort(v.begin(), v.end());
key[c] = 0.5f * (v[1] + v[2]);
}
} else {
for (int c = 0; c < 3; ++c)
key[c] = std::clamp(layer.bg_color[c], 0.f, 1.f) * 255.f;
}
// Euclidean in 0..255 RGB, against a tolerance scaled to the diagonal of that cube, so the
// slider reads as "how different from the background still counts as background".
const float limit = std::clamp(layer.bg_tolerance, 0.f, 1.f) * 441.67f;
if (!tex.has_alpha())
tex.alpha.assign(n, 255);
for (size_t i = 0; i < n; ++i) {
const float dr = float(tex.rgb[i * 3]) - key[0];
const float dg = float(tex.rgb[i * 3 + 1]) - key[1];
const float db = float(tex.rgb[i * 3 + 2]) - key[2];
if (std::sqrt(dr * dr + dg * dg + db * db) <= limit)
tex.alpha[i] = 0;
}
}
} // namespace
bool has_color_adjustments(const TextureDisplacementLayer &layer)
{
return layer.brightness != 0.f || layer.contrast != 0.f || layer.saturation != 1.f ||
layer.gamma != 1.f || layer.color_invert || layer.hue != 0.f || layer.sharpness > 0.f ||
layer.color_count >= 2 || layer.bg_remove;
}
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
{
if (!tex.has_color() || !has_color_adjustments(layer))
return;
// A contrast of -1 collapses the image to mid grey and +1 doubles the slope about it, which is
// the range a single slider can offer without a scale nobody can read off the handle.
const float slope = 1.f + std::clamp(layer.contrast, -1.f, 1.f);
const float bright = std::clamp(layer.brightness, -1.f, 1.f);
const float sat = std::max(0.f, layer.saturation);
const float inv_g = 1.f / std::max(0.01f, layer.gamma);
const bool negate = layer.color_invert;
// Hue is a rotation of the colour about the grey axis. Rodrigues' formula about (1,1,1)/sqrt(3),
// then the luma is put back exactly (the rotation preserves R+G+B, not the Rec. 709 weighting),
// so turning the wheel changes which filament an area matches without also changing how light
// it is.
const float hue_rad = layer.hue * float(M_PI) / 180.f;
const float hue_cos = std::cos(hue_rad), hue_sin = std::sin(hue_rad);
const bool rotate = layer.hue != 0.f;
constexpr float LW[3] = { 0.2126f, 0.7152f, 0.0722f }; // Rec. 709 luma
// Each output byte depends only on its own pixel, so the rows are independent. At 1024 x 1024
// this runs on every preview rebuild and every bake, which is what makes it worth splitting.
const size_t rows = size_t(tex.height);
const size_t cols = size_t(tex.width);
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
for (size_t y = range.begin(); y < range.end(); ++y) {
uint8_t *row = tex.rgb.data() + y * cols * 3;
for (size_t x = 0; x < cols; ++x) {
uint8_t *px = row + x * 3;
float c[3];
for (int k = 0; k < 3; ++k) {
float v = float(px[k]) * (1.f / 255.f);
v = std::pow(v, inv_g);
v = (v - 0.5f) * slope + 0.5f;
v += bright;
c[k] = v;
}
if (rotate) {
const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
// The component of the colour across the grey axis, which is what a hue is.
const float mean = (c[0] + c[1] + c[2]) / 3.f;
const float p[3] = { c[0] - mean, c[1] - mean, c[2] - mean };
// k x p for the unit grey axis k = (1,1,1)/sqrt(3).
constexpr float INV_SQRT3 = 0.57735027f;
const float x[3] = { INV_SQRT3 * (p[1] - p[2]), INV_SQRT3 * (p[2] - p[0]),
INV_SQRT3 * (p[0] - p[1]) };
for (int k = 0; k < 3; ++k)
c[k] = mean + p[k] * hue_cos + x[k] * hue_sin;
const float luma_now = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
for (float &v : c)
v += luma - luma_now;
}
if (sat != 1.f) {
// Rec. 709 luma, the same weights the rest of the colour path uses to decide
// what counts as light and dark.
const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
for (float &v : c)
v = luma + (v - luma) * sat;
}
for (int k = 0; k < 3; ++k) {
float v = std::clamp(c[k], 0.f, 1.f);
if (negate)
v = 1.f - v;
px[k] = uint8_t(std::lround(v * 255.f));
}
}
}
});
// Sharpen, threshold and the background key are all passes over the whole image rather than
// per-texel arithmetic, so they come after the loop above and in this order: sharpening a
// thresholded image would only ring along the one edge it has left, and the key has to read the
// colours the user is actually looking at.
sharpen_rgb(tex, layer.sharpness);
reduce_colors(tex, layer.color_count);
remove_background(tex, layer);
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{
DecodedHeightTexture result;
@@ -314,6 +593,10 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
result.height = h;
result.pixels.resize(size_t(w) * size_t(h));
result.rgb.resize(size_t(w) * size_t(h) * 3);
// A fourth channel is coverage, and it is the only thing that can say "the picture is
// not here" as opposed to "the picture is black here". Kept only when the source has it.
if (bpp >= 4)
result.alpha.resize(size_t(w) * size_t(h));
// decode_colored_png() hands its buffer back bottom-up - it is shared with the CLI's
// plate-thumbnail loader, which expects that - while this type, and decode_png()'s
// grayscale path above, are top-to-bottom. Reverse the rows on the way in so a colour
@@ -327,6 +610,8 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
result.rgb[dst * 3 + 1] = g;
result.rgb[dst * 3 + 2] = b;
result.pixels[dst] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
if (bpp >= 4)
result.alpha[dst] = src[3];
}
}
@@ -1860,10 +2145,14 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
// Outside a non-tiled placement there is no texture at all - the same hard edge sample() gives the
// height. Checked explicitly because sample_color() reports it as black, which is a real colour.
auto covered = [&](const Vec2f &planar) {
if (layer.tile_enabled)
return true;
const Vec2f uv = xf(planar);
return uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f;
if (!layer.tile_enabled && !(uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f))
return false;
// Where the picture is transparent there is no colour to print, which is the same answer as
// being outside the placement - not black, which is a colour. Half coverage counts as
// covered: a triangle prints in one filament either way, so the only useful question is
// which side of the edge it falls on.
return texture.sample_alpha(uv, layer.tile_enabled, layer.tile_method) >= 0.5f;
};
if (lscm_uv != nullptr) {
@@ -3242,9 +3531,13 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
if (data.triangles_to_split.empty())
continue;
const DecodedHeightTexture tex = decode_height_texture(*layer);
DecodedHeightTexture tex = decode_height_texture(*layer);
if (tex.empty() || (need_color && !tex.has_color()))
continue;
// The one place the bake and every preview job read a layer's colour from, so correcting it
// here is what keeps what the panel shows and what the bake prints the same image.
if (need_color)
apply_color_adjustments(tex, *layer);
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
@@ -5167,260 +5460,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
return out;
}
// ---------------------------------------------------------------------------------------------
// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather
// than the cereal stream next to them.
// ---------------------------------------------------------------------------------------------
namespace {
nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); }
nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); }
Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback)
{
if (!j.is_array() || j.size() != 2)
return fallback;
return Vec2f(j[0].get<float>(), j[1].get<float>());
}
Vec3f vec3_from_json(const nlohmann::json &j, const Vec3f &fallback)
{
if (!j.is_array() || j.size() != 3)
return fallback;
return Vec3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
}
// Reads key `k` into `out` when it is present and of the expected type, leaving `out` alone otherwise.
// That "leave it alone" is the whole point: every member starts at its struct default, so a project
// written by an older build simply keeps the defaults for whatever it did not know about.
template<class T> void read(const nlohmann::json &j, const char *k, T &out)
{
const auto it = j.find(k);
if (it == j.end())
return;
try {
out = it->get<T>();
} catch (...) {
}
}
void read_enum(const nlohmann::json &j, const char *k, int &out)
{
const auto it = j.find(k);
if (it != j.end() && it->is_number_integer())
out = it->get<int>();
}
} // namespace
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementOptions &options)
{
nlohmann::json root;
root["version"] = 1;
nlohmann::json &opt = root["options"];
opt["displace_border"] = options.displace_border;
opt["smooth_enabled"] = options.smooth_enabled;
opt["smooth_strength"] = options.smooth_strength;
opt["smooth_iterations"] = options.smooth_iterations;
opt["smooth_skip_border"] = options.smooth_skip_border;
opt["pipeline_v2"] = options.pipeline_v2;
opt["v2_refine_mm"] = options.v2_refine_mm;
opt["v2_regularize"] = options.v2_regularize;
opt["v2_max_triangles_k"] = options.v2_max_triangles_k;
opt["v2_relocate"] = options.v2_relocate;
opt["v2_flip_edges"] = options.v2_flip_edges;
opt["color_mix_enabled"] = options.color_mix_enabled;
opt["color_despeckle"] = options.color_despeckle;
opt["color_mix_count"] = options.color_mix_count;
opt["active_slot"] = options.active_slot;
opt["panel_mode"] = options.panel_mode;
nlohmann::json &arr = root["layers"];
arr = nlohmann::json::array();
for (const TextureDisplacementLayer &l : layers) {
nlohmann::json j;
j["slot"] = l.slot;
j["name"] = l.name;
j["path"] = l.path;
j["path_in_3mf"] = l.path_in_3mf;
j["depth_mm"] = l.depth_mm;
j["tiling_scale"] = l.tiling_scale;
j["rotation_deg"] = l.rotation_deg;
j["offset"] = vec2_to_json(l.offset);
j["invert"] = l.invert;
j["midlevel"] = l.midlevel;
j["smoothing"] = l.smoothing;
j["edge_smoothing"] = l.edge_smoothing;
j["edge_smoothing_amount"] = l.edge_smoothing_amount;
j["auto_connect_islands"] = l.auto_connect_islands;
j["tile_enabled"] = l.tile_enabled;
j["tile_method"] = int(l.tile_method);
j["projection_method"] = int(l.projection_method);
j["blend_mode"] = int(l.blend_mode);
j["color_enabled"] = l.color_enabled;
j["lscm_seam_angle_deg"] = l.lscm_seam_angle_deg;
j["island_padding_mm"] = l.island_padding_mm;
j["view_project_right"] = vec3_to_json(l.view_project_right);
j["view_project_up"] = vec3_to_json(l.view_project_up);
j["view_project_projective"] = l.view_project_projective;
j["view_project_matrix"] = l.view_project_matrix;
// Flat pairs rather than nested arrays: shorter, and the reader can simply ignore a trailing
// odd element instead of having to validate every sub-array.
nlohmann::json &seams = j["lscm_seam_edges"];
seams = nlohmann::json::array();
for (const auto &[a, b] : l.lscm_seam_edges) {
seams.push_back(a);
seams.push_back(b);
}
nlohmann::json &islands = j["islands"];
islands = nlohmann::json::array();
for (const TextureIsland &island : l.islands)
islands.push_back(nlohmann::json::array(
{ island.offset.x(), island.offset.y(), island.rotation_deg, island.scale }));
j["island_groups"] = l.island_groups;
nlohmann::json &overrides = j["lscm_uv_overrides"];
overrides = nlohmann::json::array();
for (const auto &[key, uv] : l.lscm_uv_overrides)
overrides.push_back(nlohmann::json::array({ key, uv.x(), uv.y() }));
arr.push_back(std::move(j));
}
return root.dump();
}
bool texture_displacement_layers_from_json(const std::string &text,
std::vector<TextureDisplacementLayer> &layers,
TextureDisplacementOptions &options)
{
nlohmann::json root;
try {
root = nlohmann::json::parse(text);
} catch (...) {
return false;
}
if (!root.is_object())
return false;
TextureDisplacementOptions out_options;
if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
const nlohmann::json &opt = *it;
read(opt, "displace_border", out_options.displace_border);
read(opt, "smooth_enabled", out_options.smooth_enabled);
read(opt, "smooth_strength", out_options.smooth_strength);
read(opt, "smooth_iterations", out_options.smooth_iterations);
read(opt, "smooth_skip_border", out_options.smooth_skip_border);
read(opt, "pipeline_v2", out_options.pipeline_v2);
read(opt, "v2_refine_mm", out_options.v2_refine_mm);
read(opt, "v2_regularize", out_options.v2_regularize);
read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
read(opt, "v2_relocate", out_options.v2_relocate);
read(opt, "v2_flip_edges", out_options.v2_flip_edges);
read(opt, "color_mix_enabled", out_options.color_mix_enabled);
read(opt, "color_despeckle", out_options.color_despeckle);
read(opt, "color_mix_count", out_options.color_mix_count);
read(opt, "active_slot", out_options.active_slot);
read(opt, "panel_mode", out_options.panel_mode);
out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
}
std::vector<TextureDisplacementLayer> out_layers;
// read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays
// below read their numbers with get<>(), which throws on one; that rejects the document, like text that
// does not parse.
try {
if (const auto it = root.find("layers"); it != root.end() && it->is_array()) {
for (const nlohmann::json &j : *it) {
if (!j.is_object())
continue;
TextureDisplacementLayer l;
read(j, "slot", l.slot);
// The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the
// document rather than reaching them.
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
return false;
read(j, "name", l.name);
read(j, "path", l.path);
read(j, "path_in_3mf", l.path_in_3mf);
read(j, "depth_mm", l.depth_mm);
read(j, "tiling_scale", l.tiling_scale);
read(j, "rotation_deg", l.rotation_deg);
if (const auto o = j.find("offset"); o != j.end())
l.offset = vec2_from_json(*o, l.offset);
read(j, "invert", l.invert);
read(j, "midlevel", l.midlevel);
read(j, "smoothing", l.smoothing);
read(j, "edge_smoothing", l.edge_smoothing);
read(j, "edge_smoothing_amount", l.edge_smoothing_amount);
read(j, "auto_connect_islands", l.auto_connect_islands);
read(j, "tile_enabled", l.tile_enabled);
read(j, "color_enabled", l.color_enabled);
int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode);
read_enum(j, "tile_method", tile_method);
read_enum(j, "projection_method", projection);
read_enum(j, "blend_mode", blend);
l.tile_method = TextureTileMethod(tile_method);
l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4));
l.blend_mode = TextureBlendMode(blend);
read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg);
read(j, "island_padding_mm", l.island_padding_mm);
if (const auto v = j.find("view_project_right"); v != j.end())
l.view_project_right = vec3_from_json(*v, l.view_project_right);
if (const auto v = j.find("view_project_up"); v != j.end())
l.view_project_up = vec3_from_json(*v, l.view_project_up);
read(j, "view_project_projective", l.view_project_projective);
if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12)
for (size_t i = 0; i < 12; ++i)
l.view_project_matrix[i] = (*v)[i].get<float>();
if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array())
for (size_t i = 0; i + 1 < v->size(); i += 2)
l.lscm_seam_edges.emplace_back((*v)[i].get<int>(), (*v)[i + 1].get<int>());
if (const auto v = j.find("islands"); v != j.end() && v->is_array())
for (const nlohmann::json &e : *v) {
if (!e.is_array() || e.size() != 4)
continue;
TextureIsland island;
island.offset = Vec2f(e[0].get<float>(), e[1].get<float>());
island.rotation_deg = e[2].get<float>();
island.scale = e[3].get<float>();
l.islands.push_back(island);
}
read(j, "island_groups", l.island_groups);
if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array())
for (const nlohmann::json &e : *v) {
if (!e.is_array() || e.size() != 3)
continue;
l.lscm_uv_overrides.emplace_back(e[0].get<int>(), Vec2f(e[1].get<float>(), e[2].get<float>()));
}
out_layers.push_back(std::move(l));
}
}
} catch (const nlohmann::json::exception &) {
return false;
}
layers = std::move(out_layers);
options = out_options;
return true;
}
} // namespace Slic3r
+53 -30
View File
@@ -293,6 +293,32 @@ struct TextureDisplacementLayer
// for the boundary to be drawn on.
bool color_enabled = false;
// Colour correction, applied to the image's RGB before it is matched against the filaments and
// before the preview quantizes it. Never to the height: the relief is what the grayscale says,
// and a brightness slider has no business moving geometry. The defaults below are the identity,
// so a layer that never meets the Image tool behaves exactly as it did.
float brightness = 0.f; // -1 .. 1, added last, after contrast
float contrast = 0.f; // -1 .. 1, pivoted on mid grey; -1 flattens to grey, 1 doubles the slope
float hue = 0.f; // -180 .. 180 degrees, rotated about the grey axis at constant luma
float sharpness = 0.f; // 0 .. 1 unsharp mask amount, 0 is off
// How many colours the picture is allowed to use, 0 for as many as it likes. Reduced by k-means
// in RGB before the filaments are matched, so the result uses at most this many of them: 2 for a
// logo that should be two flat colours, 4 or 5 for a poster, off for a photograph.
int color_count = 0; // 0, or 2 .. 12
// Leaves the background of a picture unprinted, so the object's own colour shows through instead
// of a slab of white behind a logo. Texels the source image marked transparent always count;
// `bg_remove` additionally keys out one colour.
bool bg_remove = false;
float bg_tolerance = 0.15f; // 0 .. 1, distance from the keyed colour that still counts as background
// The colour keyed out. Taken from the median of the picture's four corners while
// `bg_from_corners` is set, which is right for nearly every logo on a flat card; the panel's
// dropper and swatch clear it and put a colour of the user's own here.
bool bg_from_corners = true;
float bg_color[3] = { 1.f, 1.f, 1.f };
float saturation = 1.f; // 0 .. 2, 1 leaves it alone, 0 is grayscale
float gamma = 1.f; // 0.2 .. 5, 1 leaves it alone; above 1 lifts the midtones
bool color_invert = false; // negative of the colour, as opposed to `invert` which flips relief
bool empty() const { return !image_data || image_data->empty(); }
template<class Archive> void save(Archive &ar) const
@@ -302,7 +328,9 @@ struct TextureDisplacementLayer
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness,
contrast, saturation, gamma, color_invert, hue, sharpness, color_count, bg_remove,
bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
}
template<class Archive> void load(Archive &ar)
{
@@ -314,7 +342,8 @@ struct TextureDisplacementLayer
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled);
view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert, hue, sharpness,
color_count, bg_remove, bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
@@ -397,41 +426,14 @@ struct TextureDisplacementOptions
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
int color_despeckle = 2;
// Where the panel was left, rather than anything the bake reads. It lives with the volume because
// that is what it describes: reopening a project should put the user back on the layer they were
// editing, in the mode they were editing it in, not on slot 0 in Standard.
int active_slot = 0;
int panel_mode = 0; // 0 Standard, 1 Pro
template<class Archive> void serialize(Archive &ar)
{
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
active_slot, panel_mode);
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
}
};
// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
// make every future field a project-breaking change. A JSON object tolerates both directions - an
// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
// new ones degrade gracefully in older builds.
//
// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
// responsible for writing it and for filling `image_data` back in on load.
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementOptions &options);
// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a
// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
// keeps its default.
bool texture_displacement_layers_from_json(const std::string &text,
std::vector<TextureDisplacementLayer> &layers,
TextureDisplacementOptions &options);
// How much detail a height texture carries: central differences of the grey image, the mean gradient
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
@@ -475,11 +477,19 @@ struct DecodedHeightTexture
// has_color() is what the whole colour feature keys off: a layer set to colour a model with a
// grayscale texture on it simply colours nothing.
std::vector<uint8_t> rgb;
// Coverage: 0 where the picture is not there at all, 255 where it is fully there. Empty when the
// source image had no alpha and nothing has keyed a background out, which is the common case and
// means "opaque everywhere". Distinct from a black texel, which is a colour.
std::vector<uint8_t> alpha;
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; }
bool has_alpha() const { return !empty() && alpha.size() == size_t(width) * size_t(height); }
// Coverage at a uv, bilinear, in [0, 1]. 1 everywhere when there is no alpha plane.
float sample_alpha(const Vec2f &uv, bool tile_enabled = true,
TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false,
// a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border
// row/column being smeared outward forever (which is what clamping the coordinate would do, and
@@ -516,6 +526,19 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
bool height_texture_has_color(const TextureDisplacementLayer &layer);
// True when `layer`'s colour correction is anything other than the identity, so callers can skip
// both the work and the cache invalidation it would otherwise force.
bool has_color_adjustments(const TextureDisplacementLayer &layer);
// Applies that correction to an already decoded image, in place, to `rgb` only. Order is gamma,
// contrast, brightness, saturation, invert, each clamped back into range - the order a photo editor
// applies them in, and the one that keeps contrast pivoted on mid grey rather than on a brightness
// the slider has already moved.
//
// Deliberately not folded into decode_height_texture(): that result is cached per image, shared by
// every layer using the same file, and keyed on the image bytes alone.
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
//
// Deliberately a callback rather than a function here: matching a colour to a filament is a
+4 -1
View File
@@ -1,4 +1,5 @@
#pragma once
#ifndef libslic3r_Timer_hpp_
#define libslic3r_Timer_hpp_
#include "TriangleMesh.hpp"
@@ -9,3 +10,5 @@ public:
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
};
} // namespace Slic3r
#endif // libslic3r_Timer_hpp_
+1
View File
@@ -109,6 +109,7 @@ extern std::string format_memsize_MB(size_t n);
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
// Return string to be added to the boost::log output to inform about the current process memory allocation.
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
// Latter is used to get the memory info from SysInfoDialog.
extern std::string log_memory_info(bool ignore_loglevel = false);
extern void disable_multi_threading();
// Returns the size of physical memory (RAM) in bytes.
+3 -1
View File
@@ -1744,7 +1744,9 @@ std::string format_diameter_to_str(double diameter, int precision)
return oss.str();
}
// Returns platform-specific string to be used as log output.
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
// The latter parses the string with (semi)colons as separators, it should look about as
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
std::string log_memory_info(bool ignore_loglevel)
{
std::string out;
+5 -2
View File
@@ -186,6 +186,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoFlatten.hpp
GUI/Gizmos/GLGizmoFuzzySkin.cpp
GUI/Gizmos/GLGizmoFuzzySkin.hpp
GUI/Gizmos/GLGizmoImage.cpp
GUI/Gizmos/GLGizmoImage.hpp
GUI/Gizmos/GLGizmoMeasure.cpp
GUI/Gizmos/GLGizmoMeasure.hpp
GUI/Gizmos/GLGizmoMeshBoolean.cpp
@@ -362,8 +364,6 @@ set(SLIC3R_GUI_SOURCES
GUI/Lazy.hpp
GUI/LazyPage.cpp
GUI/LazyPage.hpp
GUI/LegacyPrinterMigrationDialog.cpp
GUI/LegacyPrinterMigrationDialog.hpp
GUI/LibVGCode/LibVGCodeWrapper.hpp
GUI/LibVGCode/LibVGCodeWrapper.cpp
GUI/LinuxDisplayBackend.cpp
@@ -418,6 +418,8 @@ set(SLIC3R_GUI_SOURCES
GUI/MultiSendMachineModel.hpp
GUI/MultiTaskManagerPage.cpp
GUI/MultiTaskManagerPage.hpp
GUI/MultiTaskModel.cpp
GUI/MultiTaskModel.hpp
GUI/NetworkTestDialog.cpp
GUI/NetworkTestDialog.hpp
GUI/Notebook.cpp
@@ -802,6 +804,7 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp
Utils/Process.cpp
Utils/Process.hpp
Utils/Profile.hpp
Utils/RaycastManager.cpp
Utils/RaycastManager.hpp
Utils/Repetier.cpp
+49 -173
View File
@@ -16,7 +16,6 @@
#include <wx/checklst.h>
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/DropDown.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include <utility>
#include "slic3r/GUI/wxExtensions.hpp"
@@ -173,26 +172,7 @@ void AMSMaterialsSetting::create()
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
auto regroup_favorites = [this]() {
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
return;
const FilamentInfos* info = selected_filament_info();
const std::string filament_id = info ? info->filament_id : std::string();
rebuild_filament_dropdown_items(filament_id);
};
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
regroup_favorites();
e.Skip();
});
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -211,9 +191,11 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
#ifdef __APPLE__
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
#else
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
#endif
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
@@ -438,7 +420,6 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
AMSMaterialsSetting::~AMSMaterialsSetting()
{
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -550,9 +531,10 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
std::string selected_ams_id;
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
const FilamentInfos* selected_info = selected_filament_info();
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
std::string filament_id = filament_item.filament_id;
if (it->filament_id.compare(filament_id) == 0) {
selected_ams_id = it->filament_id;
break;
}
@@ -700,13 +682,12 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
ams_setting_id = "";
// the combobox item
const FilamentInfos* selected_info = selected_filament_info();
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
// check filament info (rack-gated per-nozzle blacklist evaluation)
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
filament_id, ams_filament_id, ams_setting_id);
filament_item.filament_id, ams_filament_id, ams_setting_id);
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
wxString info_msg;
@@ -1005,133 +986,6 @@ static void _collect_filament_info(const wxString& shown_name,
query_filament_types[shown_name] = filament.config.get_filament_type();
}
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
{
const int selection = m_comboBox_filament->GetSelection();
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
return nullptr;
return &m_filament_list_items[selection].filament;
}
std::set<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
{
std::set<std::string> favorite_ids;
std::vector<std::string> values;
const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
favorite_ids.insert(values.begin(), values.end());
}
return favorite_ids;
}
void AMSMaterialsSetting::save_favorite_filament_ids(const std::set<std::string>& favorite_ids)
{
std::vector<std::string> values(favorite_ids.begin(), favorite_ids.end());
wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
wxGetApp().app_config->save();
}
void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
{
std::vector<DropDown::Item> items;
m_filament_list_items.clear();
auto append_header = [&items, this](const wxString& label) {
DropDown::Item item;
item.text = label;
item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
items.push_back(item);
m_filament_list_items.push_back(FilamentListItem{});
};
auto append_filament = [&items, this](const wxString& name) {
auto info_it = map_filament_items.find(name);
if (info_it == map_filament_items.end())
return;
DropDown::Item item;
item.text = name;
items.push_back(item);
m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
};
// Favorites are listed again at the top under their own header. Without any, the list is the
// plain filament list.
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
std::vector<wxString> favorites;
for (const wxString& name : m_sorted_filament_items) {
auto info_it = map_filament_items.find(name);
if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
favorites.push_back(name);
}
if (!favorites.empty()) {
append_header(_L("Favorite filaments"));
for (const wxString& name : favorites)
append_filament(name);
append_header(_L("All filaments"));
}
for (const wxString& name : m_sorted_filament_items)
append_filament(name);
int selection_idx = -1;
if (!selected_filament_id.empty()) {
for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
selection_idx = i;
break;
}
}
}
// Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
// which would leave the previous filament's name on the combo.
m_comboBox_filament->Clear();
m_comboBox_filament->SetItems(items);
update_favorite_stars();
m_comboBox_filament->SetSelection(selection_idx);
m_favorites_regroup = false;
}
void AMSMaterialsSetting::update_favorite_stars()
{
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
const FilamentListItem& row = m_filament_list_items[i];
if (row.is_filament) {
const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
}
}
}
void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
{
if (filament_id.empty())
return;
std::set<std::string> favorite_ids = load_favorite_filament_ids();
auto iter = favorite_ids.find(filament_id);
if (iter == favorite_ids.end())
favorite_ids.insert(filament_id);
else
favorite_ids.erase(iter);
save_favorite_filament_ids(favorite_ids);
}
void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
{
const int index = evt.GetInt();
if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
return;
toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
update_favorite_stars();
m_favorites_regroup = true;
}
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
{
if (!obj) return;
@@ -1145,12 +999,13 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
m_input_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n);
wxArrayString filament_items;
wxString bambu_filament_name;
wxString hint_filament_name; // the hint type to be selected
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
std::unordered_map<wxString, wxString> query_filament_types; //
std::set<std::string> filament_id_set;
m_sorted_filament_items.clear();
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
map_filament_items.clear();
@@ -1180,7 +1035,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
if (alias.empty())
continue;
m_sorted_filament_items.push_back(alias);
filament_items.push_back(alias);
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
FilamentInfos filament_infos;
@@ -1189,6 +1044,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
map_filament_items[alias] = filament_infos;
if (filament_it->filament_id == ams_filament_id) {
hint_filament_name = from_u8(alias);
bambu_filament_name = from_u8(alias);
@@ -1359,11 +1215,24 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
return name_compare != 0 ? name_compare < 0 : left < right;
};
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
}
rebuild_filament_dropdown_items(ams_filament_id);
int selection_idx = m_comboBox_filament->GetSelection();
// traverse the hint selection idx
int selection_idx = -1;
{
for(int i = 0; i < filament_items.size(); i++)
{
if (hint_filament_name == filament_items[i])
{
selection_idx = i;
break;
}
}
}
m_comboBox_filament->Set(filament_items);
m_comboBox_filament->SetSelection(selection_idx);
post_select_event(selection_idx);
if (selection_idx < 0) {
@@ -1420,7 +1289,6 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
m_filament_type = "";
const FilamentInfos* selected_info = selected_filament_info();
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
std::ostringstream stream;
@@ -1441,8 +1309,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two
// halves of the dialog cannot disagree about which filaments this machine can use.
if (selected_info) {
const std::string& filament_id = selected_info->filament_id;
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
if (!selected.empty()) {
const std::string filament_id = map_filament_items[selected].filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
if (it->filament_id != filament_id)
@@ -1495,7 +1364,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
//reset cali
int cali_select_idx = -1;
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
if ( !this->obj || m_filament_selection < 0) {
m_input_k_val->Enable(false);
m_input_n_val->Enable(false);
m_button_confirm->Disable(); // ORCA No need to change style
@@ -1514,9 +1383,21 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
ams_filament_id = "";
ams_setting_id = "";
if (selected_info) {
ams_filament_id = selected_info->filament_id;
ams_setting_id = selected_info->setting_id;
if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
if ( itor != map_filament_items.end()) {
ams_filament_id = itor->second.filament_id;
ams_setting_id = itor->second.setting_id;
break;
}
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
ams_filament_id = it->filament_id;
ams_setting_id = it->setting_id;
break;
}
}
}
wxArrayString items;
@@ -1648,11 +1529,6 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
m_button_reset->Rescale(); // ORCA
m_button_confirm->Rescale(); // ORCA
m_button_close->Rescale(); // ORCA
// An open list would keep the measurements of the old stars, so close it before replacing them.
m_comboBox_filament->CloseDropdown();
m_star_filled.msw_rescale();
m_star_empty.msw_rescale();
update_favorite_stars();
this->Refresh();
}
+6 -21
View File
@@ -31,7 +31,6 @@
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <wx/panel.h>
#include <map>
#include <set>
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
@@ -157,7 +156,6 @@ protected:
void create_panel_kn(wxWindow* parent);
void on_dpi_changed(const wxRect &suggested_rect) override;
void on_select_filament(wxCommandEvent& evt);
void on_toggle_favorite_filament(wxCommandEvent& evt);
void on_select_cali_result(wxCommandEvent &evt);
void on_select_ok(wxCommandEvent &event);
void on_select_reset(wxCommandEvent &event);
@@ -165,11 +163,6 @@ protected:
void on_clr_picker(wxMouseEvent &event);
bool is_virtual_tray();
void update_widgets();
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
void update_favorite_stars();
std::set<std::string> load_favorite_filament_ids() const;
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
void toggle_favorite_filament(const std::string& filament_id);
void update_filament_editing(bool is_printing);
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
@@ -209,7 +202,12 @@ protected:
int m_pa_cali_select_id = 0;
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
#ifdef __APPLE__
wxComboBox *m_comboBox_filament;
#else
ComboBox *m_comboBox_filament;
#endif
ComboBox * m_comboBox_cali_result;
TextInput* m_readonly_filament;
@@ -217,20 +215,7 @@ protected:
std::string filament_id;
std::string setting_id;
};
struct FilamentListItem {
FilamentInfos filament;
bool is_filament{false};
};
const FilamentInfos* selected_filament_info() const;
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
std::map<wxString, FilamentInfos> map_filament_items;
std::vector<wxString> m_sorted_filament_items;
std::vector<FilamentListItem> m_filament_list_items;
ScalableBitmap m_star_filled;
ScalableBitmap m_star_empty;
// Favorites changed while the list was open; their rows are regrouped before it opens again.
bool m_favorites_regroup{false};
std::map<std::string, FilamentInfos> map_filament_items;
};
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
+4 -1
View File
@@ -1,4 +1,5 @@
#pragma once
#ifndef slic3r_GUI_SingleChoice_hpp_
#define slic3r_GUI_SingleChoice_hpp_
#include "GUI_Utils.hpp"
#include "Plater.hpp"
@@ -32,3 +33,5 @@ private:
};
}} // namespace Slic3r::GUI
#endif
+19
View File
@@ -0,0 +1,19 @@
#include <exception>
#include <stdexcept>
#include "libslic3r/Exception.hpp"
namespace Slic3r {
class ConfigError : public Slic3r::RuntimeError {
using Slic3r::RuntimeError::RuntimeError;
};
namespace GUI {
class ConfigGUITypeError : public ConfigError {
using ConfigError::ConfigError;
};
} // namespace GUI
} // namespace Slic3r
+6 -54
View File
@@ -12,7 +12,6 @@
#include "DeviceManager.hpp"
#include "HMS.hpp"
#include "PresetBundleDialog.hpp"
#include "LegacyPrinterMigrationDialog.hpp"
#include "WebUserLoginDialog.hpp"
#include "WebViewDialog.hpp"
#include "slic3r/Utils/BBLCloudServiceAgent.hpp"
@@ -1110,8 +1109,6 @@ void GUI_App::post_init()
}
this->check_new_version_sf();
if (!cw_showed)
this->check_legacy_printer_migration(false);
const auto cloud_provider = get_printer_cloud_provider();
if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) {
// this->check_privacy_version(0);
@@ -3235,16 +3232,9 @@ bool GUI_App::on_init_inner()
#ifdef _MSW_DARK_MODE
#ifndef __WINDOWS__
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
const bool saved_dark_mode = dark_mode();
wxSystemAppearance app = wxSystemSettings::GetAppearance();
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
GUI::wxGetApp().app_config->save();
if (dark_mode() != saved_dark_mode) {
init_label_colours();
Update_dark_mode_flag();
}
#endif // __APPLE__
@@ -4865,6 +4855,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
}
void GUI_App::system_info()
{
//SysInfoDialog dlg;
//dlg.ShowModal();
}
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
@@ -6819,50 +6815,6 @@ void GUI_App::reload_settings()
}
}
void GUI_App::check_legacy_printer_migration(bool by_user)
{
const std::vector<LegacyPrinterMigration> migrations = preset_bundle->legacy_printer_migrations();
if (migrations.empty()) {
if (by_user)
MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."),
_L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal();
return;
}
if (!by_user && app_config->get_bool("legacy_printer_migration_dont_remind"))
return;
LegacyPrinterMigrationDialog dlg(mainframe, migrations, !by_user);
const bool migrate = dlg.ShowModal() == wxID_YES;
if (dlg.dont_remind())
app_config->set_bool("legacy_printer_migration_dont_remind", true);
if (!migrate)
return;
std::vector<std::string> failed;
const std::string backup = preset_bundle->migrate_legacy_printers(migrations, failed);
if (backup.empty()) {
show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated."));
return;
}
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
tab->reload_config();
tab->update_changed_ui();
}
if (plater_)
plater_->sidebar().update_all_preset_comboboxes();
wxString message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n"
"A backup of your presets was saved to:\n%1%"), backup);
if (!failed.empty()) {
message += "\n\n" + _L("These presets could not be migrated:");
for (const std::string &name : failed)
message += "\n" + from_u8(name);
}
MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal();
}
//BBS reload when logout
void GUI_App::remove_user_presets()
{
+1 -4
View File
@@ -519,6 +519,7 @@ public:
void check_printer_presets();
void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const;
@@ -567,10 +568,6 @@ public:
void check_update(bool show_tips, int by_user);
void check_new_version(bool show_tips = false, int by_user = 0);
void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets that lost printers their parent listed in an older release.
// At startup it asks once per release unless the user chose not to be reminded; by_user also reports
// when there is nothing to migrate.
void check_legacy_printer_migration(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
void enter_force_upgrade();
+8 -11
View File
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item);
if (obj_idx != -1) {
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
(*m_objects)[obj_idx]->ensure_on_bed();
cnv->reload_scene(true, true);
update_info_items(obj_idx);
@@ -2779,13 +2779,12 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
GLCanvas3D* cnv = plater->canvas3D();
switch (type) {
case InfoItemType::CustomSupports: {
case InfoItemType::CustomSupports:
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
Plater::TakeSnapshot(plater, "Remove support painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->supported_facets.reset();
break;
}
// BBS: remove CustomSeam
@@ -2796,21 +2795,19 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
}
break;
case InfoItemType::MmSegmentation: {
case InfoItemType::MmSegmentation:
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
Plater::TakeSnapshot(plater, "Remove color painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->mmu_segmentation_facets.reset();
break;
}
case InfoItemType::FuzzySkin: {
case InfoItemType::FuzzySkin:
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->fuzzy_skin_facets.reset();
break;
}
// BBS: remove Sinking
case InfoItemType::Undef : assert(false); break;
@@ -6721,7 +6718,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
else if (col == colSinking) {
Plater * plater = wxGetApp().plater();
GLCanvas3D *cnv = plater->canvas3D();
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item);
(*m_objects)[obj_idx]->ensure_on_bed();
+891
View File
@@ -0,0 +1,891 @@
#include "GLGizmoImage.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/format.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GLTexture.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/IconManager.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <imgui.h>
#include <optional>
#include <string>
#include <vector>
#include <wx/string.h>
namespace Slic3r::GUI {
GLGizmoImage::GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id)
: GLGizmoTextureDisplacement(parent, icon_filename, sprite_id)
{
}
bool GLGizmoImage::on_init()
{
if (!GLGizmoTextureDisplacement::on_init())
return false;
// Everything the base set up stays - it is the same painting machinery - except the key that
// opens it, which is this tool's own.
m_shortcut = Shortcut::GizmoImage;
return true;
}
std::string GLGizmoImage::on_get_name() const
{
return _u8L("Image");
}
wxString GLGizmoImage::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
{
// Same paint, same masks; only the undo step's wording follows the tool the user is in.
const bool erasing = shift_down || (m_erase_mode != (button_down == GLGizmoPainterBase::Button::Right));
return erasing ? _L("Remove image from the area") : _L("Place image");
}
bool GLGizmoTextureDisplacement::pick_background_at(const Vec2d &mouse_pos)
{
ModelVolume *mv = texture_volume();
ModelObject *mo = m_c->selection_info()->model_object();
TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr;
if (mv == nullptr || mo == nullptr || layer == nullptr || layer->empty())
return false;
const int idx = texture_volume_raycaster_index();
if (idx < 0)
return false;
const Selection &selection = m_parent.get_selection();
const Transform3d trafo = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
const Camera &camera = wxGetApp().plater()->get_camera();
Vec3f hit = Vec3f::Zero(), normal = Vec3f::Zero();
if (!m_c->raycaster()->raycasters()[size_t(idx)]->unproject_on_mesh(
mouse_pos, trafo, camera, hit, normal,
m_c->object_clipper()->get_clipping_plane()))
return false;
// The picture's own colour at that point, read through the projection the bake uses, rather
// than 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.
//
// Uncorrected, deliberately. The key is applied at the end of the correction chain, against
// colours the earlier passes have already moved, so it has to be stated in those terms - the
// same reason the panel's dropper reads the corrected preview.
const indexed_triangle_set &base = mv->mesh().its;
const indexed_triangle_set patch =
extract_painted_patch(base, mv->texture_displacement_facet(layer->slot).get_data());
Vec3f center = Vec3f::Zero(), axis = Vec3f::UnitZ(), average_normal = Vec3f::UnitZ();
if (!patch.indices.empty())
texture_displacement_patch_frame(patch, texture_displacement_vertex_normals(base), center, axis,
average_normal);
DecodedHeightTexture tex = decode_height_texture(*layer);
apply_color_adjustments(tex, *layer);
Vec3f rgb;
if (!sample_layer_color(tex, *layer, hit, normal, rgb, center, axis))
return false;
for (int c = 0; c < 3; ++c)
layer->bg_color[c] = std::clamp(rgb[c], 0.f, 1.f);
layer->bg_from_corners = false;
layer->bg_remove = true;
m_image_bg_picking = false;
m_preview_params_dirty = true;
m_parent.set_as_dirty();
return true;
}
void GLGizmoTextureDisplacement::set_file_drag_over(bool over)
{
if (!image_mode() || m_file_drag_over == over)
return;
m_file_drag_over = over;
// 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.
m_parent.set_as_dirty();
m_parent.request_extra_frame();
}
// Drawn over the whole canvas on the foreground list, from inside the panel's ImGui frame, which is
// what puts it above the 3D view and above the panel itself.
void GLGizmoTextureDisplacement::render_image_drop_hint()
{
if (!m_file_drag_over)
return;
const ImVec2 size = ImGui::GetIO().DisplaySize;
if (size.x < 1.f || size.y < 1.f)
return;
ImDrawList *dl = ImGui::GetForegroundDrawList();
const ImU32 teal = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA);
// Dim the view so the zone reads as a state the canvas is in, not as decoration on top of it.
dl->AddRectFilled(ImVec2(0.f, 0.f), size, IM_COL32(0, 0, 0, 70));
const float inset = std::round(ImGui::GetFontSize() * 1.2f);
const ImVec2 a(inset, inset), b(size.x - inset, size.y - inset);
if (b.x <= a.x || b.y <= a.y)
return;
// A dashed border, drawn as runs along the four edges: ImGui 1.83 has no dashed stroke.
const float dash = std::round(ImGui::GetFontSize() * 0.75f);
const float gap = dash;
const float th = std::max(2.f, std::round(ImGui::GetFontSize() * 0.12f));
for (float x = a.x; x < b.x; x += dash + gap) {
const float x1 = std::min(x + dash, b.x);
dl->AddLine(ImVec2(x, a.y), ImVec2(x1, a.y), teal, th);
dl->AddLine(ImVec2(x, b.y), ImVec2(x1, b.y), teal, th);
}
for (float y = a.y; y < b.y; y += dash + gap) {
const float y1 = std::min(y + dash, b.y);
dl->AddLine(ImVec2(a.x, y), ImVec2(a.x, y1), teal, th);
dl->AddLine(ImVec2(b.x, y), ImVec2(b.x, y1), teal, th);
}
// What dropping here will do. Worded as an invitation rather than a promise: anything that is
// not a picture falls through to the model loader, exactly as it did before this tool existed.
const std::string text = _u8L("Drop a picture to put it on the model");
const ImVec2 ts = ImGui::CalcTextSize(text.c_str());
const float pad = std::round(ImGui::GetFontSize() * 0.8f);
const ImVec2 c((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
const ImVec2 r0(c.x - ts.x * 0.5f - pad, c.y - ts.y * 0.5f - pad * 0.6f);
const ImVec2 r1(c.x + ts.x * 0.5f + pad, c.y + ts.y * 0.5f + pad * 0.6f);
dl->AddRectFilled(r0, r1, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.92f), ImGui::GetStyle().FrameRounding);
dl->AddRect(r0, r1, teal, ImGui::GetStyle().FrameRounding, 0, th * 0.5f);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), text.c_str());
}
bool GLGizmoTextureDisplacement::drop_image_file(const std::string &path)
{
if (!image_mode() || texture_volume() == nullptr)
return false;
// Imported rather than just read: the same thing the panel's Import tile does, so a dropped
// picture lands in "My images" and is there the next time without hunting for the file again.
std::string error;
const std::optional<TextureLibraryEntry> entry = import_texture_to_library(path, error);
if (!entry) {
show_error(nullptr, error);
return false;
}
// The tool starts with no layer at all, so a drop before anything is loaded has to make one.
if (active_layer() == nullptr)
add_texture_layer();
TextureDisplacementLayer *layer = active_layer();
if (layer == nullptr)
return false;
set_layer_texture(*layer, *entry);
m_parent.set_as_dirty();
return true;
}
// ---------------------------------------------------------------------------------------------
// The Image tool's window.
//
// A member of GLGizmoTextureDisplacement rather than of GLGizmoImage, and defined here rather than
// in that class's own file: it needs the same private machinery the displacement panel uses (the
// layer, the texture picker, the previews, the bake), and keeping it here leaves that 1700 line
// panel untouched instead of threading it with tests for which tool is drawing.
//
// What it deliberately leaves out, all of which only means something once the surface moves or once
// there is more than one picture: brushes and the erase toggle, the layer stack, blend modes, depth,
// midlevel, edge fade, tiling mode, the unwrap projection and its UV editor, seams, the wireframe
// and the mesh-preparation sections.
// ---------------------------------------------------------------------------------------------
void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, float bottom_limit)
{
ModelObject *mo = m_c->selection_info()->model_object();
if (mo == nullptr)
return;
ModelVolume *mv = texture_volume();
// The palette follows project state nobody tells this gizmo about (the filaments in the
// sidebar), so it is checked once a frame and a change re-runs the preview.
cached_palette();
if (m_palette_changed)
m_preview_params_dirty = true;
// Everything this tool fixes, re-pinned each frame so what the preview shows is what Apply will
// produce: Standard's recipe, zero depth with colour on, a click that floods the connected
// surface rather than a brush, and the fast preview, which with no relief to build is both
// instant and exactly the final colour.
// Two statements, not one `||`: short-circuiting would skip the image presets on any frame the
// standard ones changed something.
const bool std_changed = apply_standard_mode_presets(mv);
const bool image_changed = apply_image_mode_presets(mv);
if (std_changed || image_changed)
m_preview_params_dirty = true;
m_tool_type = ToolType::SMART_FILL;
m_cursor_type = TriangleSelector::CursorType::POINTER;
m_erase_mode = false;
m_auto_update = true;
if (!m_use_shaded_preview)
apply_view_mode(1);
float scale = m_parent.get_scale();
#ifdef WIN32
const int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
scale *= (float) dpi / (float) DPI_DEFAULT;
#endif // WIN32
const float approx_height = m_imgui->scaled(22.f);
y = std::min(y, bottom_limit - approx_height);
// Docked (the default) it is pinned to the right edge of the 3D canvas and cannot be moved, the
// same place and the same rule as the displacement panel, so the two tools do not put their
// controls in different corners. Undocked it becomes an ordinary floating window with a title
// bar to drag it by; the position is deliberately not seeded on undock, so it stays exactly
// where it was and the user simply gains the ability to move it.
ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse;
if (!m_undocked) {
flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar;
// Right-aligned (pivot 1), so the width the window auto-resized to last frame does not need
// to be known here.
float right = float(m_parent.get_canvas_size().get_width()) - m_imgui->scaled(0.5f);
GizmoImguiSetNextWIndowPos(right, y, 0.f, 0.f, ImGuiCond_Always, 1.0f, 0.0f);
}
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
GizmoImguiBegin(get_name(), flags);
ensure_panel_icons();
const float previous_body_h = m_panel_body_h;
const float previous_footer_h = m_panel_footer_h;
const ImGuiStyle &style = ImGui::GetStyle();
const bool busy = m_bake_in_progress || m_prepare_in_progress;
const float panel_w = m_imgui->scaled(16.f);
const float frame_h = ImGui::GetFrameHeight();
const float icon_sm = std::round(frame_h * 1.05f);
const float gap_s = std::round(m_imgui->scaled(0.4f));
const float label_w = m_imgui->scaled(4.6f);
const float wrap_w = m_imgui->scaled(15.f);
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
const ImVec4 col_frame = ImGui::GetStyleColorVec4(ImGuiCol_Separator);
const auto hover_tip = [&](const auto &text) {
if (ImGui::IsItemHovered())
m_imgui->tooltip(text, wrap_w);
};
// Label column plus a slider filling the rest of the row, the same shape the displacement panel
// uses, so the two tools read as one family.
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max,
const char *format, bool log) -> bool {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(label);
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
return ImGui::SliderFloat(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
};
// Borderless icon button, for the dock toggle: always the grey monochrome variant.
const auto icon_button = [&](int uid, const std::string &iconfile, const wxString &label,
const wxString &tip_text) -> bool {
const auto it = m_panel_icon_map.find(iconfile);
const float pad = std::max(1.f, std::round(icon_sm * 0.14f));
bool clicked = false;
ImGui::PushID(uid);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
if (it != m_panel_icon_map.end() && !it->second.empty() && it->second[0]->is_valid()) {
const IconManager::Icon &ic = *it->second[0];
clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id,
ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad));
} else {
clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm));
}
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImGui::PopID();
hover_tip(tip_text);
return clicked;
};
// A section header that folds its contents away. ImGui's own CollapsingHeader draws a filled bar
// that is louder than anything else in this window, so this is the same triangle-and-text the
// displacement panel's "More settings" link uses, at heading weight.
const auto section = [&](const char *id, const wxString &label, bool &open) {
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
const ImVec2 ts = ImGui::CalcTextSize(label.ToUTF8().data());
const ImVec2 p = ImGui::GetCursorScreenPos();
ImGui::PushID(id);
if (ImGui::InvisibleButton("##sec", ImVec2(panel_w, ts.y + gap_s)))
open = !open;
const bool hot = ImGui::IsItemHovered();
ImDrawList *dl = ImGui::GetWindowDrawList();
const ImU32 col = ImGui::GetColorU32(hot ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const float cx = p.x + 0.5f * arrow, cy = p.y + 0.5f * ts.y, hs = 0.32f * arrow;
if (open)
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs),
ImVec2(cx, cy + 0.5f * hs), col);
else
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx - 0.5f * hs, cy + hs),
ImVec2(cx + 0.5f * hs, cy), col);
dl->AddText(ImVec2(p.x + arrow + gap_s, p.y), col, label.ToUTF8().data());
ImGui::PopID();
return open;
};
// The integer twin of float_row, for the colour count.
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max,
const char *format) -> bool {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(label);
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
};
// Framed icon toggle, the displacement panel's one cut down to what this window needs.
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, const wxString &label,
const wxString &tip_text) -> bool {
const auto it = m_panel_icon_map.find(iconfile);
const float pad = std::max(1.f, std::round(icon_sm * 0.14f));
bool clicked = false;
ImGui::PushID(uid);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
ImGui::PushStyleColor(ImGuiCol_Button, active ? ImVec4(orca.x, orca.y, orca.z, 0.22f) : ImVec4(0.f, 0.f, 0.f, 0.f));
if (it != m_panel_icon_map.end() && int(it->second.size()) > (active ? 1 : 0) &&
it->second[active ? 1 : 0]->is_valid()) {
const IconManager::Icon &ic = *it->second[active ? 1 : 0];
clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id,
ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad));
} else {
clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm));
}
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
ImGui::GetColorU32(active ? orca : col_frame), style.FrameRounding);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImGui::PopID();
hover_tip(tip_text);
return clicked;
};
TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr;
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->push_bold_font();
m_imgui->text(wxString::FromUTF8(get_name(false)));
m_imgui->pop_bold_font();
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - icon_sm));
if (icon_button(920, "canvas_drag.svg", m_undocked ? _L("Dock panel") : _L("Undock panel"),
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back "
"against the right edge.")))
m_undocked = !m_undocked;
}
ImGui::Separator();
// Everything between the title and the footer scrolls as one, so Apply and Close stay on screen
// on a short canvas instead of being the first thing pushed off the bottom. Sized from last
// frame's content and footer heights, as the displacement panel's body is.
const float bottom = m_undocked ? ImGui::GetIO().DisplaySize.y : bottom_limit;
const float max_body_h = std::max(m_imgui->scaled(8.f), bottom - ImGui::GetCursorScreenPos().y -
m_panel_footer_h - style.WindowPadding.y -
style.ItemSpacing.y);
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
const float scrollbar_w = m_imgui->scaled(0.5f);
const ImVec4 grab = wxGetApp().dark_mode() ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::BeginChild("##image_body", ImVec2(panel_w, body_h), false);
// The picture, shown whole and the full width of the window: it is the thing being placed, so a
// row-sized thumbnail beside a file name is the wrong size for it. Clicking it opens the library,
// which is also where Import lives, and before anything is loaded the same box is an empty frame
// with a plus in it - one target, in the same place, whichever state the tool is in.
const auto open_library = [&](int slot) {
m_picker_slot = slot;
m_picker_open_request = true;
};
// A layer with no picture on it is the starting state, not a broken one: the tool creates the
// layer empty and the panel stays on the frame below until a picture has been chosen.
const bool has_picture = layer != nullptr && !layer->empty();
const bool placed = mv != nullptr && mv->is_texture_displacement_painted();
// The handle comes up by itself the first moment there is something to anchor it to, so the
// picture can be nudged straight after the click that placed it without hunting for a toggle.
// Armed once: turning it back off has to stay off, not be undone on the next frame.
if (has_picture && placed && !m_image_handle_armed) {
m_image_handle_armed = true;
if (update_adjust_anchor())
m_adjust_texture_mode = true;
}
if (!has_picture) {
const float box_h = std::round(panel_w * 0.6f);
const ImVec2 p0 = ImGui::GetCursorScreenPos();
m_imgui->disabled_begin(busy || mv == nullptr);
const bool clicked = ImGui::InvisibleButton("##load", ImVec2(panel_w, box_h));
m_imgui->disabled_end();
const bool hot = ImGui::IsItemHovered();
ImDrawList *dl = ImGui::GetWindowDrawList();
const ImU32 col = ImGui::GetColorU32(hot ? orca : col_frame);
dl->AddRect(p0, ImVec2(p0.x + panel_w, p0.y + box_h), col, style.FrameRounding);
const float cx = p0.x + 0.5f * panel_w, cy = p0.y + 0.5f * box_h;
const float arm = std::round(box_h * 0.18f), thick = std::max(1.f, std::round(box_h * 0.035f));
dl->AddLine(ImVec2(cx - arm, cy), ImVec2(cx + arm, cy), col, thick);
dl->AddLine(ImVec2(cx, cy - arm), ImVec2(cx, cy + arm), col, thick);
hover_tip(_u8L("Choose a picture to put on the model."));
if (clicked) {
if (layer == nullptr)
add_texture_layer(); // created with no picture on it, on purpose
open_library(layer != nullptr ? layer->slot : m_active_layer_slot);
}
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
ImGui::TextDisabled("%s", _u8L("Choose a picture, then click the model to put it there.").c_str());
ImGui::PopTextWrapPos();
} else {
// The colour texture is the corrected image at full resolution, which is exactly what should
// be previewed - move a correction slider and the box follows. A grayscale picture has no
// colour texture, so it falls back to the height thumbnail.
GLTexture *shown = get_layer_color_texture(*layer);
if (shown == nullptr)
shown = get_layer_thumbnail(*layer);
const float aspect = std::max(0.05f, layer_texture_aspect(*layer));
const float box_h = std::clamp(std::round(panel_w / aspect), std::round(panel_w * 0.35f),
std::round(panel_w * 1.1f));
const ImVec2 box_min = ImGui::GetCursorScreenPos();
bool box_hit = false;
if (shown != nullptr) {
box_hit = ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h),
ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0);
} else {
box_hit = ImGui::Button("?##thumb", ImVec2(panel_w, box_h));
}
hover_tip(m_image_bg_picking ?
_u8L("Click the background, here or on the model.") :
_u8L("Choose a different picture. Where you placed it and how it is corrected stay as "
"they are."));
if (box_hit) {
if (!m_image_bg_picking) {
open_library(layer->slot);
} else {
// Read straight out of the decoded image rather than off the screen: the preview is
// scaled, and it already has the corrections applied, so a screen read would key the
// corrected colour against itself.
const ImVec2 rel((ImGui::GetIO().MousePos.x - box_min.x) / panel_w,
(ImGui::GetIO().MousePos.y - box_min.y) / box_h);
const DecodedHeightTexture &tex = decode_height_texture(*layer);
if (tex.has_color()) {
const int px = std::clamp(int(rel.x * float(tex.width)), 0, tex.width - 1);
const int py = std::clamp(int(rel.y * float(tex.height)), 0, tex.height - 1);
const size_t i = (size_t(py) * size_t(tex.width) + size_t(px)) * 3;
for (int c = 0; c < 3; ++c)
layer->bg_color[c] = float(tex.rgb[i + size_t(c)]) / 255.f;
layer->bg_from_corners = false;
layer->bg_remove = true;
m_preview_params_dirty = true;
}
m_image_bg_picking = false;
}
}
const std::string name = layer->name.empty() ? _u8L("Image") : layer->name;
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
ImGui::TextDisabled("%s", name.c_str());
ImGui::PopTextWrapPos();
if (!height_texture_has_color(*layer)) {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
m_imgui->warning_text(_L("This image is grayscale, so it has no colors to print. Choose a color picture."));
ImGui::PopTextWrapPos();
}
// Placing it: one click floods the surface it lands on, out to the first fold sharper than
// the angle below. No brush, no erase toggle - on a flat side or a cube face the click alone
// is the whole gesture, and the slider is there for the cases where it is not.
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
ImGui::TextDisabled("%s", placed ? _u8L("Click the model to move the picture somewhere else.").c_str() :
_u8L("Click the model to put the picture there.").c_str());
ImGui::PopTextWrapPos();
float fill_angle = m_smart_fill_angle;
if (float_row("##fill_angle", _L("Spread"), &fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false))
m_smart_fill_angle = fill_angle;
hover_tip(_u8L("How far a click spreads from the spot you hit: it keeps going while the surface stays "
"flatter than this, and stops at a sharper fold. Small values keep it to one face."));
ImGui::Separator();
if (section("place", _L("Placement"), m_image_sec_place)) {
// Projection, minus Unwrap: that one is an editor's worth of UI (seams, islands, the UV pane)
// for a tool whose whole point is a click and a few sliders.
{
struct Mapping { const char *file; wxString label; wxString tip; TextureProjectionMethod method; };
const Mapping mappings[] = {
{ "menu_obj_cube.svg", _L("Flat"),
_L("Flat - lays the picture straight onto the surface from whichever axis it faces, and blends "
"where it wraps around an edge. The right choice for a face or a flat side."),
TextureProjectionMethod::Triplanar },
{ "menu_obj_cylinder.svg", _L("Around"),
_L("Around - wraps the picture around the area, for a round part like a mug or a tube."),
TextureProjectionMethod::Cylindrical },
{ "menu_obj_sphere.svg", _L("Ball"),
_L("Ball - wraps the picture around the area in both directions, for a dome or a sphere."),
TextureProjectionMethod::Spherical },
{ "texture_displacement_map_view.svg", _L("From view"),
_L("From view - throws the picture onto the area from where you are looking now, like a slide "
"projector. Line the view up with the surface first, because faces turned away stretch."),
TextureProjectionMethod::ViewProjected },
};
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
// Plain text, like every other label in this window: the displacement panel greys its
// headings because it has many, and here there are five rows in total.
m_imgui->text(_L_CONTEXT("Mapping", "Texture Displacement"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
if (mi > 0)
ImGui::SameLine(0.f, gap_s);
const bool on = layer->projection_method == mappings[mi].method;
if (icon_toggle(900 + mi, mappings[mi].file, on, mappings[mi].label, mappings[mi].tip) && !on) {
// Capture the current view the moment "From view" is chosen, so it does something
// sensible immediately rather than projecting from a stale direction.
if (mappings[mi].method == TextureProjectionMethod::ViewProjected)
capture_view_projection(*layer);
layer->projection_method = mappings[mi].method;
m_preview_params_dirty = true;
}
}
}
if (layer->projection_method == TextureProjectionMethod::ViewProjected) {
if (m_imgui->button(_u8L("Capture current view"))) {
capture_view_projection(*layer);
m_preview_params_dirty = true;
}
hover_tip(_u8L("Throws the picture on again from where you are looking now. Orbit the model, press "
"this, and it is re-laid from the new angle."));
}
m_preview_params_dirty |= float_row("##size", _L("Size"), &layer->tiling_scale, 1.f, 300.f, "%.1f mm", true);
hover_tip(_u8L("How wide the picture is on the model, in millimetres."));
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer->rotation_deg, 0.f, 360.f, "%.0f°", false);
hover_tip(_u8L("Turns the picture on the surface, for lining it up with an edge of the model."));
// Sliding it into place, and whether it repeats. Both belong on one row: the handle is the
// coarse control and Tile decides what it is sliding - one copy, or a pattern.
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(_L("Move"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
if (icon_toggle(910, "texture_displacement_adjust.svg", m_adjust_texture_mode, _L("Move on the model"),
_L("Move on the model - drag the handle that appears on the surface to slide the "
"picture around, or one of its two arrows to slide it along just that axis."))) {
if (!m_adjust_texture_mode) {
update_model_object(); // flush any pending placement before anchoring to it
if (update_adjust_anchor())
m_adjust_texture_mode = true;
else
show_error(nullptr, _u8L("Click the model to put the picture somewhere first."));
} else {
m_adjust_texture_mode = false;
m_adjust_drag_handle = AdjustHandle::None;
}
}
ImGui::SameLine(0.f, gap_s * 2.f);
if (m_imgui->bbl_checkbox(_L("Tile"), layer->tile_enabled))
m_preview_params_dirty = true;
hover_tip(_u8L("Repeats the picture over the area instead of placing a single copy. Off by "
"default, because a photo or a logo goes on once; Size then sets how big that "
"one copy is."));
}
// The one number that decides how sharp the printed picture is. Each triangle prints in a
// single filament, so the mesh under the paint has to be finer than the detail wanted out of
// the image - and the budget above is what stops that running away.
{
TextureDisplacementOptions &opts = mv->texture_displacement_options;
float edge = opts.v2_refine_mm;
m_imgui->disabled_begin(busy);
if (float_row("##resolution", _L("Detail"), &edge, 0.02f, 2.f, "%.2f mm", true)) {
opts.v2_refine_mm = edge;
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
hover_tip(_u8L("How fine the mesh is made under the picture, in millimetres. Each triangle prints "
"in one filament, so this is how sharp an edge between two colors can come out. "
"Smaller costs triangles fast: halving it needs four times as many."));
// Refining past the budget is not an error - the bake simplifies back down to it - but the
// result then carries less of the picture than this slider asks for, and without saying so
// the only sign is a print that came out softer than expected.
const size_t budget = size_t(std::max(0, opts.v2_max_triangles_k)) * 1000;
const size_t needed = estimated_refined_triangles(*mv, edge);
if (budget > 0 && needed > budget * 5 / 4) {
const auto count = [](size_t n) {
return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) :
Slic3r::format("%1% k", (n + 500) / 1000);
};
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
m_imgui->warning_text(Slic3r::format(_u8L("Needs about %1% triangles, budget %2% - it will be "
"simplified back down and come out softer."),
count(needed), count(budget)));
ImGui::PopTextWrapPos();
}
}
} // Placement
ImGui::Separator();
if (section("color", _L("Color"), m_image_sec_color)) {
// Colour correction. Every one of these changes which filament each area lands in, which is
// why they are here rather than left to whatever the file happened to contain: a photo that
// prints as mud usually only needs contrast.
m_preview_params_dirty |= float_row("##brightness", _L("Brightness"), &layer->brightness, -1.f, 1.f, "%.2f", false);
hover_tip(_u8L("Lightens or darkens the whole picture before it is matched to your filaments."));
m_preview_params_dirty |= float_row("##contrast", _L("Contrast"), &layer->contrast, -1.f, 1.f, "%.2f", false);
hover_tip(_u8L("Pushes light and dark apart. Raise it when a photo prints as mud; lower it when "
"everything collapses into the lightest and darkest filament."));
m_preview_params_dirty |= float_row("##saturation", _L("Saturation"), &layer->saturation, 0.f, 2.f, "%.2f", false);
hover_tip(_u8L("How strong the colors are. At 0 the picture is gray, so it prints in your grays and "
"blacks; raising it pushes each area towards a purer filament."));
m_preview_params_dirty |= float_row("##hue", _L("Hue"), &layer->hue, -180.f, 180.f, "%.0f°", false);
hover_tip(_u8L("Turns every color of the picture around the color wheel, without changing how light "
"it is. Your filaments are a fixed, arbitrary set, so this is what lets the picture "
"meet them: if you have orange loaded but no red, a small turn puts the reds on it."));
m_preview_params_dirty |= float_row("##gamma", _L("Gamma"), &layer->gamma, 0.2f, 5.f, "%.2f", true);
hover_tip(_u8L("Lifts or drops the midtones without touching the brightest and darkest parts. Use it "
"when the middle of the picture is the part coming out wrong."));
m_preview_params_dirty |= float_row("##sharpen", _L("Sharpen"), &layer->sharpness, 0.f, 1.f, "%.2f", false);
hover_tip(_u8L("Crisps up the edges before the picture is matched to your filaments. Worth more here "
"than in a photo: a soft edge between two colors quantizes into a ragged band of "
"both, and sharpening first makes it land in one place."));
// How many colours the picture may use. 2 is a logo in two flat colours, 4 or 5 a poster,
// off a photograph. This is not the same as the displacement tool's "Mixed colors", which
// caps how many mixed filament slots a bake may create rather than what the picture uses.
{
int count = layer->color_count;
if (int_row("##colors", _L("Colors"), &count, 0, 12, count == 0 ? _u8L("Off").c_str() : "%d")) {
// 1 is not a picture, so the slider steps over it on the way out of Off.
layer->color_count = count == 1 ? 2 : count;
m_preview_params_dirty = true;
}
hover_tip(_u8L("Reduces the picture to this many colors before your filaments are matched to "
"it, so it prints in at most that many. The colors are the ones the picture is "
"actually made of, not an even split of the range. Off leaves it alone."));
}
if (m_imgui->bbl_checkbox(_L("Negative"), layer->color_invert))
m_preview_params_dirty = true;
hover_tip(_u8L("Prints the picture inverted, light for dark and each color for its opposite."));
} // Color
ImGui::Separator();
if (!section("bg", _L("Background"), m_image_sec_bg)) {
// Folded away, but still worth saying whether it is doing anything.
ImGui::SameLine();
ImGui::TextDisabled("%s", layer->bg_remove ? _u8L("removed").c_str() : _u8L("kept").c_str());
} else {
if (m_imgui->bbl_checkbox(_L("Remove background"), layer->bg_remove))
m_preview_params_dirty = true;
hover_tip(_u8L("Leaves the background unprinted, so the object's own color shows through instead of "
"a slab of white behind a logo. A picture that is already transparent needs none of "
"this: those parts are left out either way."));
if (layer->bg_remove) {
// Which colour is being removed, shown rather than described: the swatch is also a
// picker, and the dropper beside it takes the colour from the preview above.
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(_L("Color"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
float shown_bg[3] = { layer->bg_color[0], layer->bg_color[1], layer->bg_color[2] };
if (layer->bg_from_corners) {
// Nothing has been picked, so show what the corners actually gave: the same median
// the bake will key on, not a placeholder white.
const DecodedHeightTexture &tex = decode_height_texture(*layer);
if (tex.has_color() && tex.width > 1 && tex.height > 1) {
const size_t n = size_t(tex.width) * size_t(tex.height);
const size_t corner[4] = { 0, size_t(tex.width) - 1,
(size_t(tex.height) - 1) * size_t(tex.width), n - 1 };
for (int c = 0; c < 3; ++c) {
std::array<float, 4> v{};
for (int i = 0; i < 4; ++i)
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
std::sort(v.begin(), v.end());
shown_bg[c] = 0.5f * (v[1] + v[2]) / 255.f;
}
}
}
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX() - icon_sm - gap_s);
if (ImGui::ColorEdit3("##bg_color", shown_bg,
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) {
for (int c = 0; c < 3; ++c)
layer->bg_color[c] = shown_bg[c];
layer->bg_from_corners = false;
m_preview_params_dirty = true;
}
hover_tip(layer->bg_from_corners ?
_u8L("Taken from the four corners of the picture, which is right for a logo on a "
"plain card. Click to choose a color yourself instead.") :
_u8L("The color being removed. Click to change it."));
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(930, "texture_displacement_pipette.svg", m_image_bg_picking, _L("Pick the color"),
_L("Pick the color - then click the background, either in the preview above or on "
"the model itself.")))
m_image_bg_picking = !m_image_bg_picking;
if (!layer->bg_from_corners) {
ImGui::SameLine(0.f, gap_s);
if (m_imgui->button(_u8L("Corners"))) {
layer->bg_from_corners = true;
m_preview_params_dirty = true;
}
hover_tip(_u8L("Back to taking the color from the picture's four corners."));
}
m_preview_params_dirty |= float_row("##bg_tol", _L("Tolerance"), &layer->bg_tolerance, 0.01f, 0.6f, "%.2f", false);
hover_tip(_u8L("How different from that color still counts as background. Raise it when a "
"shadow or a gradient is left behind; lower it when it starts eating the picture."));
}
} // Background
// Outside both sections, because it clears the colour corrections and the background key
// alike, and a reset that lives inside one of them would look like it only reached that far.
ImGui::Separator();
if (m_imgui->button(_u8L("Reset all"))) {
layer->brightness = 0.f;
layer->contrast = 0.f;
layer->saturation = 1.f;
layer->hue = 0.f;
layer->gamma = 1.f;
layer->sharpness = 0.f;
layer->color_count = 0;
layer->bg_remove = false;
layer->bg_tolerance = 0.15f;
layer->bg_from_corners = true;
layer->bg_color[0] = layer->bg_color[1] = layer->bg_color[2] = 1.f;
m_image_bg_picking = false;
layer->color_invert = false;
m_preview_params_dirty = true;
}
hover_tip(_u8L("Puts every correction and the background back to where they started, leaving the "
"picture as it was loaded. Where you placed it and how big it is are not touched."));
}
// The library popup lives in this window's scope, where its open request and the popup itself
// share one ID stack.
{
const ImVec2 win_min = ImGui::GetWindowPos();
const ImVec2 win_sz = ImGui::GetWindowSize();
render_texture_library_popup(win_min, ImVec2(win_min.x + win_sz.x, win_min.y + win_sz.y));
}
// ImGui sliders report "changed" on every frame of a drag, and a preview rebuild is real work,
// so with the mouse still down it waits for the release.
if (m_preview_params_dirty && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
rebuild_preview();
m_preview_params_dirty = false;
}
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
ImGui::EndChild();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
const float footer_top = ImGui::GetCursorScreenPos().y;
ImGui::Separator();
{
const bool painted = placed;
const bool no_colors = mv != nullptr && !any_layer_colors(*mv);
// Colour is the only thing this bake produces, so with a grayscale picture there would be
// nothing to show for it - and it is not a cheap no-op: the bake still refines the painted
// area and replaces the mesh with the result.
const bool can_apply = !busy && mv != nullptr && painted && !no_colors;
const float button_h = std::round(frame_h * 1.25f);
const float button_w = std::max({ ImGui::CalcTextSize(_u8L("Apply").c_str()).x,
ImGui::CalcTextSize(_u8L("Close").c_str()).x,
ImGui::CalcTextSize(_u8L("Stop").c_str()).x,
ImGui::CalcTextSize(_u8L("Working...").c_str()).x }) +
m_imgui->scaled(0.5f);
const float row_y = ImGui::GetCursorPosY();
const float icon_h = 21.f * scale;
const float row_h = std::max(icon_h, button_h);
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
GLGizmoUtils::render_wiki_guide_button(m_parent, scale,
"https://www.orcaslicer.com/wiki/print_prepare/prepare_texture_displacement");
ImGui::PopStyleVar(1);
ImGui::SameLine(panel_w - button_w * 2 - style.ItemSpacing.x);
GLGizmoUtils::push_orca_button_style();
m_imgui->push_bold_font();
m_imgui->disabled_begin(!can_apply);
ImGui::SetCursorPosY(row_y + (row_h - button_h) * .5f);
const std::string apply_label = busy ? _u8L("Working...") : _u8L("Apply");
if (ImGui::Button((apply_label + "##apply").c_str(), ImVec2(button_w, button_h)))
bake_standard();
m_imgui->disabled_end();
m_imgui->pop_bold_font();
GLGizmoUtils::pop_orca_button_style();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
m_imgui->tooltip(!has_picture ? _u8L("Choose a picture first.") :
!painted ? _u8L("Click the model to place the picture first.") :
no_colors ? _u8L("This picture is grayscale, so there is nothing to print. "
"Choose a color picture.") :
_u8L("Prints the picture where you placed it, in the nearest of your "
"loaded filaments. The surface itself is left exactly where it "
"is: this adds color, not relief."),
wrap_w);
ImGui::SameLine();
if (busy) {
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(button_w, button_h)))
wxGetApp().plater()->get_ui_job_worker().cancel_all();
hover_tip(_u8L("Stops it. Whatever it had already finished stays on the model, and can be undone."));
} else {
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(button_w, button_h)))
m_parent.reset_all_gizmos();
hover_tip(_u8L("Closes the tool. Your picture and where you put it stay with the model."));
}
}
m_panel_footer_h = ImGui::GetCursorScreenPos().y - footer_top;
// Both heights feed back into next frame's layout, so the window needs one more frame whenever
// either of them moved.
if (std::abs(m_panel_body_h - previous_body_h) > 0.5f || std::abs(m_panel_footer_h - previous_footer_h) > 0.5f)
m_parent.request_extra_frame();
// Last, on the foreground list, so it covers the panel as well as the view.
render_image_drop_hint();
GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style();
}
} // namespace Slic3r::GUI
+46
View File
@@ -0,0 +1,46 @@
#ifndef slic3r_GLGizmoImage_hpp_
#define slic3r_GLGizmoImage_hpp_
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/I18N.hpp"
#include <string>
#include <wx/string.h>
namespace Slic3r::GUI {
class GLCanvas3D;
// "Put this picture on the model": the texture-displacement gizmo with the relief taken out of it.
//
// There is no second engine here and no copy of one. It is GLGizmoTextureDisplacement with
// image_mode() on, which pins every layer to zero depth and colour on (see
// apply_image_mode_presets()) and leaves out the controls that only mean something once the surface
// moves. Painting, the layer stack, the texture library, the previews and the bake are the very same
// code, reached the very same way - a zero-depth bake refines the painted area and writes the
// colours exactly as a relief bake does, it just displaces by nothing.
//
// Its own toolbar button because the two are different jobs, not two settings of one: a user who
// wants a photo on a part should not have to know what a height map is, or find the one slider that
// has to be zero.
class GLGizmoImage : public GLGizmoTextureDisplacement
{
public:
GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id);
protected:
bool image_mode() const override { return true; }
bool on_init() override;
std::string on_get_name() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
std::string get_gizmo_entering_text() const override { return _u8L("Entering Image painting"); }
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Image painting"); }
std::string get_action_snapshot_name() const override { return _u8L("Image editing"); }
};
} // namespace Slic3r::GUI
#endif // slic3r_GLGizmoImage_hpp_
@@ -409,10 +409,18 @@ std::unique_ptr<GLTexture> upload_color_texture(const DecodedHeightTexture &deco
for (int c = 0; c < 3; ++c)
sum[c] += decoded.rgb[si + size_t(c)];
}
// Coverage rides in the fourth channel, box-filtered with the colour, so the shader can
// leave the surface alone where the picture is not there (see the colour path in
// texture_displacement_shaded.fs).
unsigned int sum_a = 0;
if (decoded.has_alpha())
for (int sy = y0; sy < y1 && sy < decoded.height; ++sy)
for (int sx = x0; sx < x1 && sx < decoded.width; ++sx)
sum_a += decoded.alpha[size_t(sy) * size_t(decoded.width) + size_t(sx)];
const size_t di = (size_t(y) * size_t(w) + size_t(x)) * 4;
for (int c = 0; c < 3; ++c)
rgba[di + size_t(c)] = (n > 0) ? static_cast<unsigned char>(sum[size_t(c)] / n) : 0;
rgba[di + 3] = 255;
rgba[di + 3] = (decoded.has_alpha() && n > 0) ? static_cast<unsigned char>(sum_a / n) : 255;
}
auto texture = std::make_unique<GLTexture>();
@@ -494,17 +502,7 @@ bool GLGizmoTextureDisplacement::on_init()
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
toggle_wireframe_shortcut
};
// Both ways into the same input box on any slider in the panel: ImGui turns a slider into one when
// it is Ctrl+clicked or when it is given the keyboard focus, which is what the edit icon asks for.
m_desc["type_value"] = _L("Type an exact value into a slider");
m_shortcuts_sliders = {
{ctrl + _L("Left mouse button"), m_desc["type_value"]},
{_L("Edit icon at a slider's right end"), m_desc["type_value"]},
};
for (std::vector<std::pair<wxString, wxString>> *list : {&m_shortcuts_brush, &m_shortcuts_bucket_fill, &m_shortcuts_gap_fill})
list->insert(list->end(), m_shortcuts_sliders.begin(), m_shortcuts_sliders.end());
return true;
}
@@ -541,6 +539,7 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_palette_pure_quantizer = nullptr;
m_palette_filaments.clear();
m_palette_images.clear();
m_palette_adjusts.clear();
m_mix_ranking.reset();
m_palette_slots.clear();
m_palette_changed = false;
@@ -566,6 +565,8 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_seam_edit_mode = false;
m_adjust_drag_handle = AdjustHandle::None;
m_adjust_anchor_valid = false;
m_image_handle_armed = false;
m_file_drag_over = false;
// The pane is request-only: closing the gizmo drops the request, so reopening it later doesn't
// silently reopen the pane too.
m_show_uv_editor = false;
@@ -770,8 +771,21 @@ bool GLGizmoTextureDisplacement::on_mouse(const wxMouseEvent &mouse_event)
{
if (m_seam_edit_mode)
return on_mouse_seam(mouse_event);
if (m_adjust_texture_mode)
return on_mouse_adjust_texture(mouse_event);
// The background dropper takes the next click wherever it lands, the model included, so it comes
// before both the handle and the paint.
if (m_image_bg_picking && mouse_event.LeftDown() &&
pick_background_at(Vec2d(mouse_event.GetX(), mouse_event.GetY())))
return true;
if (m_adjust_texture_mode) {
if (on_mouse_adjust_texture(mouse_event))
return true;
// The displacement tool turns this mode on deliberately, so there a click off the handle
// doing nothing is the point. The Image tool has it on from the start, so the same click has
// to fall through and place the picture, or turning it on would disable the tool's one
// gesture.
if (!image_mode())
return false;
}
const bool handled = GLGizmoPainterBase::on_mouse(mouse_event);
// A consumed drag/click is a paint (or erase) event: the base class has already updated the live
@@ -1573,6 +1587,9 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("height_tex", 0);
shader->set_uniform("height_tex_texel", Vec2f(1.f / float(tex->get_width()), 1.f / float(tex->get_height())));
shader->set_uniform("depth_mm", layer->depth_mm);
// Without this the preview wrapped the image and showed a repeating pattern where the bake
// places a single copy.
shader->set_uniform("tile_enabled", layer->tile_enabled);
shader->set_uniform("tiling_scale", layer->tiling_scale);
// Read off the uploaded texture rather than the decoded one: they are the same image, and this is
// the aspect the sampler will actually see.
@@ -3568,7 +3585,6 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
void GLGizmoTextureDisplacement::update_model_object()
{
store_panel_state();
bool updated = false;
ModelObject *mo = m_c->selection_info()->model_object();
int idx = -1;
@@ -3597,6 +3613,11 @@ void GLGizmoTextureDisplacement::update_model_object()
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
rebuild_preview();
// The on-canvas handle is anchored to the paint patch, so a stroke that moved the paint has
// to move it with them. Matters most in the Image tool, where the handle is on from the
// start and a click moves the picture to a different part of the model.
if (m_adjust_texture_mode)
update_adjust_anchor();
}
}
@@ -3604,24 +3625,6 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
{
wxBusyCursor wait;
// Opening the gizmo puts the panel back where it was left: the layer and the mode come off the
// volume, because they are the user's place in this model's stack, and the dock state off the
// application config, because it is about their window. Only on open - this also runs on an
// undo/redo and on a layer switch, where overriding what the user is doing now would be wrong.
if (first_update) {
if (const ModelVolume *mv = texture_volume(); mv != nullptr) {
const TextureDisplacementOptions &opts = mv->texture_displacement_options;
m_panel_mode = std::clamp(opts.panel_mode, 0, 1);
// Only if that slot still holds a layer: the stack can have been edited by anything that
// does not go through the panel, and a selection pointing at nothing shows an empty panel.
const auto &stack = mv->texture_displacement_layers;
if (std::any_of(stack.begin(), stack.end(),
[&opts](const TextureDisplacementLayer &l) { return l.slot == opts.active_slot; }))
m_active_layer_slot = opts.active_slot;
}
m_undocked = wxGetApp().app_config->get_bool(UNDOCKED_CONFIG_KEY);
}
const ModelObject *mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
@@ -3670,14 +3673,6 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
rebuild_preview();
}
void GLGizmoTextureDisplacement::store_panel_state()
{
if (ModelVolume *mv = texture_volume(); mv != nullptr) {
mv->texture_displacement_options.active_slot = m_active_layer_slot;
mv->texture_displacement_options.panel_mode = m_panel_mode;
}
}
void GLGizmoTextureDisplacement::set_active_layer(int slot)
{
if (slot == m_active_layer_slot)
@@ -3686,7 +3681,6 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot)
// reflect - otherwise they would be silently lost.
update_model_object();
m_active_layer_slot = slot;
store_panel_state();
update_from_model_object(false);
// The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync
// instead of leaving it pointing at the previous layer's (now stale) paint patch.
@@ -3706,6 +3700,7 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
// a monochrome ("normal", grey in the current theme) and an original-colour variant.
static const std::vector<std::string> names = {
"texture_displacement_brush.svg", "texture_displacement_face.svg", "texture_displacement_connected_area.svg",
"texture_displacement_pipette.svg", // the Image panel's background dropper
"texture_displacement_real_preview.svg", "texture_displacement_fast_preview.svg",
"texture_displacement_checker.svg", "texture_displacement_distortion.svg",
"texture_displacement_wireframe.svg", "texture_displacement_cross.svg",
@@ -3718,8 +3713,6 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
// The "type a value instead" icon at the right end of every slider.
"edit.svg",
};
std::vector<std::string> paths;
paths.reserve(names.size());
@@ -3766,13 +3759,24 @@ void GLGizmoTextureDisplacement::add_texture_layer()
// Start the layer off on the first library texture rather than on nothing at all: a textureless
// layer looks broken (painting on it appears to do nothing, because there is no height map to
// displace by). The user swaps it for another from the layer's own picker.
const std::vector<TextureLibraryEntry> &library = texture_library();
if (!library.empty())
if (const LibraryTexture *tex = get_library_texture(library.front().path)) {
layer.name = library.front().name;
layer.path = library.front().path;
layer.image_data = tex->image_data;
}
//
// Not in the Image tool: there the layer deliberately starts with no picture at all, so the panel
// stays on its empty frame until the user has chosen one. Picking a shipped height map for them
// would be choosing the wrong thing twice over - it is not their picture, and most of that
// library is grayscale, which an image bake cannot print.
if (image_mode()) {
// One copy, not a repeating pattern: a photo or a logo is placed once. The panel's Tile
// checkbox is there for the cases that do want it, so this is a default and not a pin.
layer.tile_enabled = false;
} else {
const std::vector<TextureLibraryEntry> &library = texture_library();
if (!library.empty())
if (const LibraryTexture *tex = get_library_texture(library.front().path)) {
layer.name = library.front().name;
layer.path = library.front().path;
layer.image_data = tex->image_data;
}
}
mv->texture_displacement_layers.push_back(std::move(layer));
// A new layer opens with every setting showing; there is nothing on it yet to hide settings from.
m_layer_expanded[size_t(free_slot)] = true;
@@ -3967,15 +3971,19 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
return n;
};
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
{
// 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. So the
// Image tool offers only the user's own pictures, and the section is left out entirely rather
// than shown full of things that would all warn when picked.
if (!image_mode()) {
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, false);
end_grid(origin, count);
if (count == 0)
ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
}
ImGui::TextDisabled("%s", _u8L("My textures").c_str());
ImGui::TextDisabled("%s", image_mode() ? _u8L("My images").c_str() : _u8L("My textures").c_str());
{
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, true);
@@ -3983,15 +3991,20 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
import_clicked = true;
if (ImGui::IsItemHovered())
m_imgui->tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
"bakes from and saved to your personal texture folder, kept separate from the "
"textures shipped with OrcaSlicer."),
m_imgui->tooltip(image_mode() ?
_u8L("Import a picture from your computer. Its colors are kept, and it is saved to "
"your personal folder so it is here next time.") :
_u8L("Import your own image as a height map. It is converted to the format the "
"slicer bakes from and saved to your personal texture folder, kept separate "
"from the textures shipped with OrcaSlicer."),
m_imgui->scaled(20.f));
end_grid(origin, count + 1);
}
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::TextDisabled("%s", image_mode() ?
_u8L("PNG, JPG or BMP. Hover one to remove it.").c_str() :
_u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::PopTextWrapPos();
if (chosen || import_clicked)
@@ -4571,15 +4584,22 @@ const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDispl
// The images themselves rather than their addresses, so a freed and reallocated image can never
// pass for the old one. Whether one has colour, and whether its colours are flat, follows from it.
std::vector<std::shared_ptr<std::vector<unsigned char>>> images;
// ... and what each one is corrected by, which changes the colours the mixes are ranked against
// without changing the image it came from.
std::vector<ColorAdjustKey> adjusts;
if (mv != nullptr)
for (const TextureDisplacementLayer &layer : mv->texture_displacement_layers)
if (layer.color_enabled && !layer.empty())
if (layer.color_enabled && !layer.empty()) {
images.push_back(layer.image_data);
adjusts.push_back(ColorAdjustKey::of(layer));
}
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images;
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images ||
adjusts != m_palette_adjusts;
if (ranking_stale) {
m_palette_filaments = std::move(filaments);
m_palette_images = std::move(images);
m_palette_adjusts = std::move(adjusts);
m_mix_ranking.reset();
}
if (mixing && !m_mix_ranking)
@@ -4656,9 +4676,12 @@ std::vector<GLGizmoTextureDisplacement::MixTarget> GLGizmoTextureDisplacement::m
continue;
TextureDisplacementLayer raw = layer;
raw.smoothing = 0.f;
const DecodedHeightTexture tex = decode_height_texture(raw);
DecodedHeightTexture tex = decode_height_texture(raw);
if (!tex.has_color())
continue;
// Corrected first: the mixes are chosen to cover the colours the bake will actually ask for,
// and a darkened or desaturated image asks for different ones.
apply_color_adjustments(tex, layer);
// 16 levels per channel, each bin keeping the mean of the colours that fell in it: a coarse grid
// to gather on, without snapping every colour to a bin corner.
@@ -5364,10 +5387,16 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
{
if (layer.empty() || !layer.color_enabled)
return nullptr;
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing)
const ColorAdjustKey adjust = ColorAdjustKey::of(layer);
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing &&
m_color_tex_adjust == adjust)
return m_color_tex.get();
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
// Corrected here rather than inside the decode, exactly as prepare_sampleable_layers() does for
// the bake, so the fast preview shades the image the bake will print.
DecodedHeightTexture tex = decode_height_texture(layer);
apply_color_adjustments(tex, layer);
std::unique_ptr<GLTexture> texture = upload_color_texture(tex, HEIGHT_TEX_MAX_PX);
if (!texture) {
m_color_tex.reset();
m_color_tex_source = nullptr;
@@ -5376,6 +5405,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
m_color_tex = std::move(texture);
m_color_tex_source = layer.image_data.get();
m_color_tex_smoothing = layer.smoothing;
m_color_tex_adjust = adjust;
return m_color_tex.get();
}
@@ -5452,6 +5482,8 @@ static constexpr float STD_SUBDIV_BORDER_MM = 0.4f;
// what the eye actually lands on - a stepped outline around a printed decal reads as a defect in a
// way a slightly coarse relief transition does not - and it costs triangles along an outline only.
static constexpr float STD_SUBDIV_COLOR_MM = 0.25f;
// The Image tool's triangle budget, in thousands. See apply_image_mode_presets().
static constexpr int IMAGE_BUDGET_K = 1000;
bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
{
@@ -5482,6 +5514,61 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
return changed;
}
// Pinned into the volume rather than applied on the way to the bake, exactly as Standard mode pins
// displace_border and smooth_enabled above: the preview, the bake, the triangle estimate and the
// palette all read the layers straight off the volume, and a value that only some of them saw would
// put the preview and the bake out of step.
//
// The two tools share one layer stack - that is what "not a fork" means here - so opening the Image
// tool on a volume whose layers carry relief flattens them, and they stay flat when the displacement
// tool is reopened. Only that one number changes: the paint, the image, the placement and any relief
// already baked into the mesh are untouched.
bool GLGizmoTextureDisplacement::apply_image_mode_presets(ModelVolume *mv)
{
if (mv == nullptr)
return false;
bool changed = false;
TextureDisplacementOptions &opts = mv->texture_displacement_options;
// A million triangles for the picture. The auto budget is 750 k, chosen for relief, where the
// triangles buy shape; here they buy nothing but the sharpness of the edges between colours, and
// that is the whole output, so it is worth spending more of them on it.
if (opts.v2_max_triangles_k != IMAGE_BUDGET_K) {
opts.v2_max_triangles_k = IMAGE_BUDGET_K;
changed = true;
}
// Resolution is a slider in this panel rather than an Auto checkbox, so it needs a real value to
// start from: the same recommendation Auto would have produced for this model.
if (opts.v2_refine_mm <= 0.f) {
const float edge = v2_recommendation(*mv).edge_mm;
opts.v2_refine_mm = edge > 0.f ? edge : 0.3f;
changed = true;
}
for (TextureDisplacementLayer &layer : mv->texture_displacement_layers) {
// Zero depth is the whole of "no relief": the bake still refines the painted area and still
// writes the colours, every layer just contributes a displacement of 0 (see
// make_combined_displacement_sampler()). Nothing special-cases it.
if (layer.depth_mm != 0.f) {
layer.depth_mm = 0.f;
changed = true;
}
// Colour is the only thing an image bake produces, so it is on wherever there is colour to
// use rather than being a checkbox the user could leave off and wonder at.
const bool wanted = height_texture_has_color(layer);
if (layer.color_enabled != wanted) {
layer.color_enabled = wanted;
changed = true;
}
// Unwrap is the one projection the Image tool does not offer: it is an editor's worth of UI
// (seams, islands, the UV pane) for a case that wants a click and a slider. A layer arriving
// from the displacement tool set to it falls back to the triplanar default.
if (layer.projection_method == TextureProjectionMethod::LSCM) {
layer.projection_method = TextureProjectionMethod::Triplanar;
changed = true;
}
}
return changed;
}
void GLGizmoTextureDisplacement::bake_standard()
{
ModelVolume *mv = texture_volume();
@@ -5790,6 +5877,14 @@ void GLGizmoTextureDisplacement::render_paint_cursor_hint()
void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float bottom_limit)
{
// The Image tool gets its own, much smaller window (see GLGizmoImage.cpp): everything below this
// point is the displacement panel, and it is left exactly as it was rather than being threaded
// with tests for which of the two tools is drawing.
if (image_mode()) {
render_image_input_window(x, y, bottom_limit);
return;
}
ModelObject *mo = m_c->selection_info()->model_object();
if (!mo)
return;
@@ -5959,8 +6054,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding.
// Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on
// any of these types a value in. `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
@@ -5968,107 +6063,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
};
// ---- Typed values on sliders ----
// Every slider in the panel goes through typed_slider(), so all of them take a number the same
// two ways: Ctrl+click on the track, or the edit icon at the right end for the same box without
// the modifier. Both end up in ImGui's own "temp input" mode - the icon only asks ImGui to focus
// the slider, which is what turns it into an input box (see SliderScalar) - so the parsing,
// the clamping and the Enter/click-away behaviour are ImGui's, not a second implementation.
// Retires a click whose row is no longer being drawn - the panel changed mode in between - so that
// it cannot fire later when that row comes back. Before any row is drawn, so a live one still wins.
if (!m_slider_type_request.empty() && ImGui::GetFrameCount() > m_slider_type_request_frame + 1)
m_slider_type_request.clear();
// Exactly as tall as the slider it sits beside, so it lines up with the track without the row
// having to be re-centred around it; icon_button() pads the glyph itself down to about 0.7 of this.
const float edit_icon_w = frame_h;
// What the slider's own width has to give up for the icon beside it.
const float edit_icon_room = edit_icon_w + gap_s;
// One end of a range, through the slider's own format, so the bounds in the tooltip read exactly
// like the value in the track. A parenthesised note in the format - "(auto)" - is dropped: it
// describes the current state, not the bound.
const auto fmt_bound = [](const char *format, auto v) {
char buf[64];
std::snprintf(buf, sizeof(buf), format, v);
std::string out(buf);
const size_t paren = out.find('(');
if (paren != std::string::npos)
out.erase(paren);
while (!out.empty() && out.back() == ' ')
out.pop_back();
return out;
};
const auto range_text = [](const std::string &lo, const std::string &hi) {
return Slic3r::format(_u8L("Range: %1% to %2%"), lo, hi);
};
const auto edit_icon_tip = [&](const std::string &range) {
return _u8L("Type a value instead of dragging. Ctrl+click on the slider does the same.") + "\n\n" + range;
};
// Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right
// edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the
// range go on both. Returns whatever the slider returned.
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range,
const std::function<bool()> &slider) -> bool {
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
// frame, so it has to be issued before the slider it names, not from the icon beside it.
if (m_slider_type_request == id) {
ImGui::SetKeyboardFocusHere();
m_slider_type_request.clear();
m_parent.set_as_dirty(); // the focus only lands on a frame that actually gets drawn
}
ImGui::SetNextItemWidth(item_w);
const bool changed = slider();
// AllowWhenDisabled: several of these rows are drawn inside disabled_begin(), and a greyed
// slider is exactly when the user needs to be told what it is and why it is not moving.
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
wxString full = tip;
if (!full.IsEmpty())
full += "\n\n";
m_imgui->tooltip(full + from_u8(range), wrap_w);
}
// SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was
// drawn at), so an icon of the same height lands on the track wherever the row placed it.
ImGui::SameLine(0.f, gap_s);
if (icon_button(int(ImGui::GetID(id)), "edit.svg", edit_icon_w, _L("Edit"), from_u8(edit_icon_tip(range)))) {
m_slider_type_request = id;
m_slider_type_request_frame = ImGui::GetFrameCount();
m_parent.set_as_dirty(); // the next frame is where the request above is turned into focus
}
return changed;
};
// The two forms every slider in the panel is one of. They derive the range from the bounds they are
// already given, so no row states its bounds or its format twice and the tooltip cannot drift from
// the track. `item_w` is as typed_row() takes it.
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format,
bool log, const wxString &tip) -> bool {
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderFloat(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format,
bool log, const wxString &tip) -> bool {
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderInt(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
// Label column + the slider filling the rest of the row.
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
bool log, float right_inset, const wxString &tip) -> bool {
bool log, float right_inset) -> bool {
slider_label(label);
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
};
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
float right_inset, const wxString &tip) -> bool {
float right_inset) -> bool {
slider_label(label);
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
};
const auto layer_name = [](const TextureDisplacementLayer &l) {
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
@@ -6116,7 +6121,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"not what you wanted and you know why."));
if (clicked && !on) {
m_panel_mode = m;
store_panel_state();
if (!pro_mode()) {
// Leaving the subdivision preview open would strand a wireframe whose controls just
// disappeared, so close it as part of the switch.
@@ -6139,7 +6143,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
"the toolbar.")))
m_undocked = !m_undocked;
wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked);
}
ImGui::Separator();
@@ -6292,10 +6295,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); };
vsep(icon_md);
if (is_brush_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm)));
centre_on_row(frame_h);
typed_slider("##cursor_radius",
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"));
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
ImGuiSliderFlags_AlwaysClamp);
hover_tip(m_desc.at("cursor_size"));
vsep(icon_sm);
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
centre_on_row(icon_sm);
@@ -6308,10 +6312,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Sphere - paints only within a ball around the point under the cursor")))
m_cursor_type = TriangleSelector::CursorType::SPHERE;
} else if (is_area_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX()));
centre_on_row(frame_h);
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false,
_L("Angle threshold - the fill stops at edges sharper than this"));
ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°",
ImGuiSliderFlags_AlwaysClamp);
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"));
} else {
centre_on_row(frame_h);
ImGui::AlignTextToFramePadding();
@@ -6581,16 +6586,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// The three controls nearly every layer needs; everything else waits behind "More settings".
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad,
_L("Height of the relief: how far white in the texture lifts the surface, in "
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad,
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad);
hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in "
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad);
hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad);
hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
if (m_layer_expanded[size_t(layer.slot)]) {
{
@@ -6605,22 +6610,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
// at 0.5 mid-grey is neutral and darker texels cut inwards.
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad,
_L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
ImGui::PushTextWrapPos(content_rx - card_pad);
m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
ImGui::PopTextWrapPos();
}
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad,
_L("Blurs the image before it is used, which rounds off hard steps and removes "
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
"costs fine detail."));
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad);
hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes "
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
"costs fine detail."));
// Edge fade: the relief flattens toward the boundary of the painted area.
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
@@ -6628,11 +6633,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"blends into the bare surface instead of stopping at a step."));
ImGui::SameLine();
m_imgui->disabled_begin(!layer.edge_smoothing);
m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room),
&layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", false,
_L("How far in the fade reaches, as a share of the painted area. Small "
"values soften a narrow band at the edge; 1 flattens almost all of it."));
ImGui::SetNextItemWidth(-card_pad);
m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f",
ImGuiSliderFlags_AlwaysClamp);
m_imgui->disabled_end();
hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values "
"soften a narrow band at the edge; 1 flattens almost all of it."));
// Invert and Colours share a row.
{
@@ -6677,19 +6683,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Every mix can become a filament slot, and there are only so many beside the
// physical filaments.
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
_L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created.")))
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created."));
// After the slider, so a change shows in the same frame.
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
_L("Cleans up single stray triangles of the wrong color, which detail finer "
"than the mesh leaves behind. Raise it if the result looks speckled, "
"lower it if small features are being swallowed.")))
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer "
"than the mesh leaves behind. Raise it if the result looks speckled, "
"lower it if small features are being swallowed."));
}
}
@@ -6794,10 +6800,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"gets the texture, and its border becomes the edge of the projection."));
if (projector_open) {
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad,
_L("How solid the frame window looks while you place it. Lower to see the "
"model through it; it does not affect the result.")))
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad))
m_projector_frame->set_opacity(m_projector_opacity);
hover_tip(_u8L("How solid the frame window looks while you place it. Lower to see the "
"model through it; it does not affect the result."));
if (m_imgui->button(_u8L("Apply projection frame"))) {
const int painted_count = apply_projection_frame();
@@ -7098,57 +7104,57 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// pattern can sample four points that all happen to land at similar heights, report no error,
// and stall before refinement ever starts.
if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm,
0.001f, 20.f, "%.3f mm", true, 0.f,
m_subdivide_feature ?
_L("No triangle in the painted area stays larger than this, even where the texture "
"is flat. Keep it near the size of the smallest feature you want to come out.") :
_L("Triangles in the painted area are split until none is larger than this. Smaller "
"means finer detail and, fast, a lot more triangles.")))
0.001f, 20.f, "%.3f mm", true, 0.f))
preview_live();
hover_tip(m_subdivide_feature ?
_u8L("No triangle in the painted area stays larger than this, even where the texture "
"is flat. Keep it near the size of the smallest feature you want to come out.") :
_u8L("Triangles in the painted area are split until none is larger than this. Smaller "
"means finer detail and, fast, a lot more triangles."));
if (m_subdivide_feature) {
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f,
_L("How far the mesh may sit from the shape the texture describes, in millimetres. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features.")))
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f))
preview_live();
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f,
_L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges.")))
hover_tip(_u8L("How far the mesh may sit from the shape the texture describes, in millimetres. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features."));
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f))
preview_live();
hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges."));
}
// Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which
// exists whether or not "Follow texture detail" is on. 0 turns the band off.
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
_L("Triangle size along the outline of the painted area, where the relief drops back to the "
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
"outline. 0 turns it off.")))
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
preview_live();
hover_tip(_u8L("Triangle size along the outline of the painted area, where the relief drops back to the "
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
"outline. 0 turns it off."));
// Only worth showing when a layer is actually colouring: with no colour there is no boundary for it
// to refine and the control would do nothing whatever it is set to.
if (mv != nullptr && any_layer_colors(*mv)) {
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
_L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off.")))
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
preview_live();
hover_tip(_u8L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off."));
}
ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str());
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
} else {
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f,
_L("Splits every triangle of the model into four, this many times over. Each step "
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
"only the painted area unless you need the whole model finer."))) {
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f)) {
if (m_subdivide_editing)
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
m_parent.set_as_dirty();
}
hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step "
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
"only the painted area unless you need the whole model finer."));
}
{
@@ -7210,9 +7216,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f;
}
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f,
_L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
"even mesh; this is what makes one out of an uneven import."));
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f);
hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
"even mesh; this is what makes one out of an uneven import."));
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
@@ -7220,10 +7226,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"a cube comes back with wobbly edges."));
ImGui::SameLine();
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false,
_L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
ImGui::SetNextItemWidth(-1.f);
ImGui::SliderFloat("##remesh_sharp_angle", &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", ImGuiSliderFlags_AlwaysClamp);
m_imgui->disabled_end();
hover_tip(_u8L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
m_imgui->disabled_begin(mv == nullptr || busy);
if (ImGui::Button(_u8L("Remesh").c_str()))
@@ -7251,16 +7258,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"on a layer is a different thing: it blurs the image before it is used."));
if (opts.smooth_enabled) {
float percent = opts.smooth_strength * 100.f;
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f,
_L("How far each pass pulls a vertex towards its neighbours. High values round the "
"relief off quickly; low values need more passes but keep more of the detail."))) {
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
m_preview_params_dirty = true;
}
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f,
_L("How many smoothing passes to run. More passes spread the smoothing further across "
"the surface; Strength decides how much each one moves.")))
hover_tip(_u8L("How far each pass pulls a vertex towards its neighbours. High values round the "
"relief off quickly; low values need more passes but keep more of the detail."));
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f))
m_preview_params_dirty = true;
hover_tip(_u8L("How many smoothing passes to run. More passes spread the smoothing further across "
"the surface; Strength decides how much each one moves."));
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Ignore outer ring"), opts.smooth_skip_border);
hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours "
@@ -7303,16 +7310,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k;
m_imgui->disabled_begin(auto_budget);
// TRN Slider value: %d is the triangle budget in thousands
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f,
_L("How many thousand triangles this bake may spend on the area you painted. "
"Relief already baked into the rest of the model is kept on top of it, so a "
"second bake somewhere else gets the same budget as the first. Raise it if the "
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
"the refinement produced, however many that is."))) {
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f)) {
opts.v2_max_triangles_k = shown_k;
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
hover_tip(_u8L("How many thousand triangles this bake may spend on the area you painted. "
"Relief already baked into the rest of the model is kept on top of it, so a "
"second bake somewhere else gets the same budget as the first. Raise it if the "
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
"the refinement produced, however many that is."));
}
if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) {
// Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines
@@ -7419,28 +7426,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"default. Untick to set both yourself - worth doing for a texture much finer or "
"much coarser than the part it sits on."));
ImGui::SameLine();
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
// Held in a local: the format outlives the call that reads it, and the slider and the
// range in its tooltip are then formatted the same way.
const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm");
const wxString v2edge_tip =
auto_res && rec.edge_mm > 0.f ?
from_u8(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
"budget %2% k. Untick Auto to set them yourself."),
rec.edge_mm, rec.budget_k)) :
_L("How fine the mesh is made under the paint, in millimetres. It has to be "
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
"four times as many, and once they no longer fit the budget the bake simplifies "
"back down and the detail goes with it.");
m_imgui->disabled_begin(busy || auto_res);
if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f,
v2edge_format.c_str(), true, v2edge_tip)) {
if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? _u8L("%.2f mm (auto)").c_str() : "%.2f mm",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
opts.v2_refine_mm = shown;
m_preview_params_dirty = true;
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
if (auto_res && rec.edge_mm > 0.f)
hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
"budget %2% k. Untick Auto to set them yourself."),
rec.edge_mm, rec.budget_k));
else
hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be "
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
"four times as many, and once they no longer fit the budget the bake simplifies "
"back down and the detail goes with it."));
// What this resolution costs over what is painted, against what the budget allows. Refining
// past the budget is not an error - the bake simplifies back down to it - but the result
@@ -7472,17 +7477,18 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->text(_L("Triangles"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
m_imgui->disabled_begin(busy);
if (typed_slider_int("##subdivbudget", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room,
&m_subdivide_budget_k, 10, 2000, "+%d k", true,
_L("How many thousand triangles the refinement may add to the model. The triangle "
"that fits worst is always split first, so even a run that spends the lot has "
"spent it where it shows most. Raise it if the relief still looks coarse."))) {
if (ImGui::SliderInt("##subdivbudget", &m_subdivide_budget_k, 10, 2000, "+%d k",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
if (m_subdivide_editing)
rebuild_subdivide_preview();
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle "
"that fits worst is always split first, so even a run that spends the lot has "
"spent it where it shows most. Raise it if the relief still looks coarse."));
}
const float button_h = std::round(frame_h * 1.25f);
@@ -7501,7 +7507,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
: m_shortcuts_sliders; // the sliders are there whether or not a painting tool is
: std::vector<std::pair<wxString, wxString>>{};
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
@@ -154,6 +154,33 @@ public:
// What the cache was built from.
std::vector<ColorRGBA> m_palette_filaments;
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
// One layer's colour correction, as a value that can be compared and cached on. It belongs to
// every cache keyed on "what the image looks like", because a correction slider changes the
// pixels while leaving the image_data pointer alone.
struct ColorAdjustKey
{
float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f, hue = 0.f;
float sharpness = 0.f, bg_tolerance = 0.15f;
float bg_r = 1.f, bg_g = 1.f, bg_b = 1.f;
int color_count = 0;
bool invert = false, bg_remove = false, bg_from_corners = true;
bool operator==(const ColorAdjustKey &o) const
{
return brightness == o.brightness && contrast == o.contrast && saturation == o.saturation &&
gamma == o.gamma && hue == o.hue && sharpness == o.sharpness &&
bg_tolerance == o.bg_tolerance && bg_r == o.bg_r && bg_g == o.bg_g && bg_b == o.bg_b &&
color_count == o.color_count && invert == o.invert && bg_remove == o.bg_remove &&
bg_from_corners == o.bg_from_corners;
}
bool operator!=(const ColorAdjustKey &o) const { return !(*this == o); }
static ColorAdjustKey of(const TextureDisplacementLayer &l)
{
return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue,
l.sharpness, l.bg_tolerance, l.bg_color[0], l.bg_color[1], l.bg_color[2],
l.color_count, l.color_invert, l.bg_remove, l.bg_from_corners };
}
};
std::vector<ColorAdjustKey> m_palette_adjusts; // alongside m_palette_images, one per entry
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
bool m_palette_mixing = false;
int m_palette_mix_count = 0;
@@ -203,7 +230,25 @@ public:
// rotation handles instead of painting); otherwise forwards to the normal painting handling.
bool on_mouse(const wxMouseEvent &mouse_event) override;
// Loads `path` as the Image tool's picture, importing it into the user's library on the way so
// it is there next time. For a drop onto the 3D view; see PlaterDropTarget::OnDropFiles(). False
// when this is not the Image tool, or the file could not be read.
bool drop_image_file(const std::string &path);
// Called while a file is being dragged over the 3D view, so the Image tool can show that
// dropping it there will do something. Without it the drag has no feedback at all and the only
// way to find out is to let go.
void set_file_drag_over(bool over);
protected:
// True in the Image tool (see GLGizmoImage): this same gizmo restricted to the one case where
// the surface does not move at all - a colour picture laid on the model. Everything behind the
// panel is shared and unchanged: the same layers, the same paint masks, the same bake. This flag
// only tells the panel which of the two tools it is drawing, and makes it pin the values the
// restricted case fixes (see apply_image_mode_presets()).
virtual bool image_mode() const { return false; }
bool on_init() override;
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
@@ -221,8 +266,11 @@ protected:
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
// Paint / Erase, from the panel. See get_left_button_state_type(). Protected because a subclass
// overriding handle_snapshot_action_name() has to read the same flag to name the undo step.
bool m_erase_mode = false;
private:
bool on_init() override;
void update_model_object() override;
void update_from_model_object(bool first_update) override;
void on_opening() override {}
@@ -262,15 +310,42 @@ private:
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
// changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv);
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
// can carry it. Cheap and idempotent - called wherever either of the two changes.
void store_panel_state();
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
// The order matters and is the whole reason this is one button - a height map can only move
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
// the paint if it were not carried across (see prepare_mesh()).
void bake_standard();
// Image mode's fixed values, on top of Standard's and pinned the same way (every frame, so the
// preview cannot disagree with the bake): zero depth on every layer, and colour on wherever the
// picture has any. Those two are what turn the displacement bake into "put this picture on the
// model" without a second code path. Returns true if it actually changed something.
bool apply_image_mode_presets(ModelVolume *mv);
// The Image tool's whole window: load a picture, click the model to place it, correct it, Apply.
// Defined in GLGizmoImage.cpp, beside the class it exists for, rather than here - it is a member
// only so that it can reach the same private machinery the displacement panel uses, and keeping
// it out of this file leaves that panel's 1700 lines alone.
void render_image_input_window(float x, float y, float bottom_limit);
// Set once the Image tool has switched its placement handle on for the current picture, so that
// turning the handle back off stays off instead of being re-armed on the next frame.
bool m_image_handle_armed = false;
// Set between a drag entering the 3D view and leaving or dropping on it; drives the drop zone
// render_image_input_window() draws over the canvas.
bool m_file_drag_over = false;
void render_image_drop_hint();
// Set while the panel's background dropper is armed: the next click on the picture preview takes
// the colour under it instead of opening the library.
bool m_image_bg_picking = false;
// Takes the background colour from the point of the model under `mouse_pos`: the picture's own
// colour there, read through the same projection the bake uses, not the lit pixel on screen.
// False when the ray misses or the layer does not colour that point, leaving the dropper armed.
bool pick_background_at(const Vec2d &mouse_pos);
// Which of the Image panel's sections are unfolded. Panel state for the session, deliberately
// not saved with the project.
bool m_image_sec_place = true;
bool m_image_sec_color = true;
bool m_image_sec_bg = true;
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
// Bake differ only in which stages `params` enables and in what happens afterwards:
@@ -402,17 +477,9 @@ private:
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
// Paint / Erase, from the panel. See get_left_button_state_type().
bool m_erase_mode = false;
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
@@ -943,6 +1010,7 @@ private:
std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f;
ColorAdjustKey m_color_tex_adjust;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
@@ -980,12 +1048,9 @@ private:
PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
// config rather than in the project: it describes the user's window, not the model, so opening
// someone else's project must not move their panel.
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// open/close within a session, so the choice sticks while working.
bool m_undocked = false;
// The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
@@ -1028,9 +1093,6 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
+13
View File
@@ -22,6 +22,7 @@
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoImage.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
@@ -201,6 +202,11 @@ void GLGizmosManager::switch_gizmos_icon_filename()
// light/dark switch.
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
break;
case(EType::Image):
// Drawn flat in Orca's menu-icon palette, which reads on either background, so one file
// serves both themes here too.
gizmo->set_icon_filename("toolbar_image.svg");
break;
case(EType::MeshBoolean):
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
break;
@@ -252,6 +258,8 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
// The same gizmo restricted to the zero-displacement case; see GLGizmoImage.
m_gizmos.emplace_back(new GLGizmoImage(m_parent, "toolbar_image.svg", EType::Image));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
@@ -585,6 +593,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == TextureDisplacement)
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == Image)
return dynamic_cast<GLGizmoImage*>(m_gizmos[Image].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == MeshBoolean)
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == BrimEars)
@@ -599,6 +609,7 @@ bool GLGizmosManager::is_paint_gizmo()
m_current == EType::MmSegmentation ||
m_current == EType::FuzzySkin ||
m_current == EType::TextureDisplacement ||
m_current == EType::Image ||
m_current == EType::Seam;
}
@@ -1550,6 +1561,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
return "Fuzzy Skin Painting";
case GLGizmosManager::EType::TextureDisplacement:
return "Texture Displacement";
case GLGizmosManager::EType::Image:
return "Image";
default:
return "";
}
@@ -96,6 +96,7 @@ public:
FuzzySkin,
MmSegmentation,
TextureDisplacement,
Image,
Emboss,
Svg,
Measure,
@@ -1,96 +0,0 @@
#include "LegacyPrinterMigrationDialog.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Utils.hpp"
#include "I18N.hpp"
#include "MainFrame.hpp"
#include "format.hpp"
#include "libslic3r/Preset.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/Label.hpp"
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/sizer.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/toplevel.h>
#include <string>
#include <vector>
namespace Slic3r { namespace GUI {
LegacyPrinterMigrationDialog::LegacyPrinterMigrationDialog(wxWindow *parent, const std::vector<LegacyPrinterMigration> &migrations,
bool offer_dont_remind)
: DPIDialog(parent ? parent : static_cast<wxWindow *>(wxGetApp().mainframe), wxID_ANY, _L("Migrate Filament Presets"),
wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
{
SetBackgroundColour(*wxWHITE);
SetFont(Label::Body_14);
auto *sizer = new wxBoxSizer(wxVERTICAL);
auto *message = new Label(this,
_L("Since OrcaSlicer 2.5.0, filament presets have been restructured to support more features. Some of your "
"presets need to be migrated to the new structure, or they will no longer be available for some of your "
"printers.\n\n"
"Would you like to migrate them now? Your presets will be backed up first, and this only needs to be done "
"once. You can also do it later from Help > Migrate Filament Presets."),
0);
message->Wrap(FromDIP(520));
sizer->Add(message, 0, wxEXPAND | wxALL, FromDIP(15));
sizer->Add(new Label(this, _L("Affected presets:")), 0, wxLEFT | wxRIGHT, FromDIP(15));
sizer->AddSpacer(FromDIP(5));
wxString details;
for (const LegacyPrinterMigration &migration : migrations) {
details += format_wxstr(_L("%1% (based on %2%)"), migration.preset, migration.parent) + "\n";
for (const std::string &printer : migration.printers)
details += " " + from_u8(printer) + "\n";
}
auto *list = new wxTextCtrl(this, wxID_ANY, details, wxDefaultPosition, wxSize(-1, FromDIP(220)),
wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP);
sizer->Add(list, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(15));
if (offer_dont_remind) {
auto *dont_remind_sizer = new wxBoxSizer(wxHORIZONTAL);
m_dont_remind = new ::CheckBox(this);
auto *dont_remind_label = new Label(this, _L("Don't remind me again"));
dont_remind_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &e) {
m_dont_remind->SetValue(!m_dont_remind->GetValue());
e.Skip();
});
dont_remind_sizer->Add(m_dont_remind, 0, wxALIGN_CENTER_VERTICAL);
dont_remind_sizer->Add(dont_remind_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6));
sizer->Add(dont_remind_sizer, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(15));
}
auto *buttons = new DialogButtons(this, {"Yes", "No"}, "Yes");
buttons->GetYES()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_YES); });
buttons->GetNO()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_NO); });
sizer->Add(buttons, 0, wxEXPAND);
SetSizerAndFit(sizer);
CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
}
bool LegacyPrinterMigrationDialog::dont_remind() const
{
return m_dont_remind != nullptr && m_dont_remind->GetValue();
}
void LegacyPrinterMigrationDialog::on_dpi_changed(const wxRect &)
{
if (m_dont_remind != nullptr)
m_dont_remind->Rescale();
GetSizer()->SetSizeHints(this);
Refresh();
}
}} // namespace Slic3r::GUI
@@ -1,34 +0,0 @@
#pragma once
#include "GUI_Utils.hpp"
#include <vector>
class CheckBox;
class wxWindow;
class wxRect;
namespace Slic3r {
struct LegacyPrinterMigration;
namespace GUI {
// Lists the user filament presets that no longer show on printers their parent listed in an older
// release, and asks whether to migrate them. ShowModal() returns wxID_YES to migrate. With
// offer_dont_remind it also offers to stop the startup reminder for good.
class LegacyPrinterMigrationDialog : public DPIDialog
{
public:
LegacyPrinterMigrationDialog(wxWindow *parent, const std::vector<LegacyPrinterMigration> &migrations, bool offer_dont_remind);
bool dont_remind() const;
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
::CheckBox *m_dont_remind{nullptr};
};
}} // namespace Slic3r::GUI
-4
View File
@@ -2827,10 +2827,6 @@ wxMenu* MainFrame::generate_help_menu()
append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"),
[](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); });
append_menu_item(helpMenu, wxID_ANY, _L("Migrate Filament Presets"),
_L("Bring back filament presets that a profile update removed from printers they were made for"),
[](wxCommandEvent&) { wxGetApp().check_legacy_printer_migration(true); });
helpMenu->AppendSeparator();
// Troubleshoot center
+6
View File
@@ -0,0 +1,6 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
+11
View File
@@ -0,0 +1,11 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
-4
View File
@@ -589,10 +589,6 @@ std::vector<NativeCommand> build_command_catalog()
Slic3r::GUI::desktop_open_datadir_folder();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_migrate_filament_presets", L("Migrate Filament Presets"), L("Help"), [](const std::string&) {
wxGetApp().check_legacy_printer_migration(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) {
wxGetApp().troubleshoot();
return AppActionRunResult{AppActionRunResult::Level::Success};
+59
View File
@@ -67,6 +67,7 @@
#include "slic3r/GUI/Field.hpp"
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include <set>
#include <wx/filename.h>
#include "slic3r/GUI/Widgets/StaticLine.hpp"
#include "slic3r/GUI/GUI_ObjectSettings.hpp"
#include "slic3r/GUI/ColorDecomposeDialog.hpp"
@@ -246,6 +247,7 @@
#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles
#include "ProjectDirtyStateManager.hpp"
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
#include "Gizmos/GLGizmoImage.hpp" // Drop an image onto the Image tool
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
#include "Gizmos/GizmoObjectManipulation.hpp"
@@ -7110,6 +7112,9 @@ void Sidebar::can_search()
p->can_search();
}
// Defined with the other drop helpers below; forward declared because the drop target uses it.
void set_image_drag_over(Plater &plater, bool over);
class PlaterDropTarget : public wxFileDropTarget
{
public:
@@ -7119,12 +7124,53 @@ public:
virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames);
// A drag has no feedback of its own, so the Image tool is told when one is over the view and
// draws a drop zone for as long as it is. wxWidgets cannot hand us the file names before the
// drop on GTK, so this says nothing about what is being dragged: the zone invites a picture,
// and anything else still falls through to the model loader.
virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def)
{
set_image_drag_over(m_plater, true);
return wxFileDropTarget::OnEnter(x, y, def);
}
virtual void OnLeave()
{
set_image_drag_over(m_plater, false);
wxFileDropTarget::OnLeave();
}
private:
MainFrame& m_mainframe;
Plater& m_plater;
};
namespace {
// The Image gizmo, but only while it is the open one. Null otherwise, which is what keeps a drag or
// a drop from meaning anything different with the tool closed.
GLGizmoImage *open_image_gizmo(Plater &plater)
{
GLCanvas3D *canvas = plater.canvas3D();
if (canvas == nullptr)
return nullptr;
GLGizmosManager &gizmos = canvas->get_gizmos_manager();
if (gizmos.get_current_type() != GLGizmosManager::Image)
return nullptr;
return dynamic_cast<GLGizmoImage *>(gizmos.get_gizmo(GLGizmosManager::Image));
}
// True when `file` is a picture and the Image gizmo is the one currently open, in which case it has
// taken the file. Mirrors emboss_svg() below, which does the same for an SVG and the text tool.
bool drop_image_on_gizmo(Plater &plater, const wxString &file)
{
static const std::set<wxString> image_ext = { "png", "jpg", "jpeg", "bmp" };
const wxString ext = wxFileName(file).GetExt().Lower();
if (image_ext.count(ext) == 0)
return false;
GLGizmoImage *image = open_image_gizmo(plater);
return image != nullptr && image->drop_image_file(into_u8(file));
}
bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position)
{
std::string svg_file_str = into_u8(svg_file);
@@ -7783,6 +7829,12 @@ const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::ica
const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase);
const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase);
void set_image_drag_over(Plater &plater, bool over)
{
if (GLGizmoImage *image = open_image_gizmo(plater))
image->set_file_drag_over(over);
}
bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames)
{
#ifdef WIN32
@@ -7790,11 +7842,18 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
this->MSWUpdateDragImageOnLeave();
#endif // WIN32
set_image_drag_over(m_plater, false); // the drag is over, whoever ends up taking the file
m_mainframe.Raise();
m_mainframe.select_tab(TAB_ID_PREPARE);
if (wxGetApp().is_editor())
m_plater.select_view_3D("3D");
// A picture dropped while the Image tool is open is that tool's input, not a model to load. Only
// then: with the tool closed a dropped PNG goes on doing whatever it did before.
if (filenames.size() == 1 && drop_image_on_gizmo(m_plater, filenames.Last()))
return true;
// When only one .svg file is dropped on scene
if (filenames.size() == 1) {
const wxString &filename = filenames.Last();
+3
View File
@@ -114,6 +114,9 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }),
// Unbound by default: every single letter this context can reach is already taken, and a default
// that shadows one of them is worse than none. The shortcuts dialog lists it for binding.
SHORTCUT(GizmoImage, "gizmo_image", L("Gizmo image painting"), PLATER, KeyChord{}),
// Sliders
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
+1 -1
View File
@@ -38,7 +38,7 @@ enum class Shortcut : uint8_t {
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
// Gizmos
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, GizmoImage,
// Sliders
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
// Painting tools
+232
View File
@@ -0,0 +1,232 @@
#include "SysInfoDialog.hpp"
#include "I18N.hpp"
#include "3DScene.hpp"
#include "GUI.hpp"
#include "../Utils/UndoRedo.hpp"
#include "Plater.hpp"
#include <string>
#include <Eigen/Core>
#include <wx/clipbrd.h>
#include <wx/platinfo.h>
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "wxExtensions.hpp"
#include "libslic3r/BlacklistedLibraryCheck.hpp"
#include "libslic3r/Color.hpp"
#include "format.hpp"
#ifdef _WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <psapi.h>
#endif /* _WIN32 */
namespace Slic3r {
namespace GUI {
std::string get_main_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
if (!format_as_html)
out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end;
out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end;
out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end;
out << line_end;
out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end;
out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end;
out << b_start <<
#if defined _WIN32
"Windows Version: "
#else
// Hopefully some kind of unix / linux.
"System Version: "
#endif
<< b_end << wxPlatformInfo::Get().GetOperatingSystemDescription() << line_end;
out << b_start << "Total RAM size [MB]: " << b_end << Slic3r::format_memsize_MB(Slic3r::total_physical_memory());
return out.str();
}
std::string get_mem_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
std::string mem_info_str = log_memory_info(true);
std::istringstream mem_info(mem_info_str);
std::string value;
while (std::getline(mem_info, value, ':')) {
out << b_start << (value+": ") << b_end;
std::getline(mem_info, value, ';');
out << value << line_end;
}
const Slic3r::UndoRedo::Stack &stack = wxGetApp().plater()->undo_redo_stack_main();
out << b_start << "RAM size reserved for the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.get_memory_limit()) << line_end;
out << b_start << "RAM size occupied by the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.memsize()) << line_end << line_end;
return out.str();
}
SysInfoDialog::SysInfoDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_FULL_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
wxColour bgr_clr = wxGetApp().get_window_default_clr();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
SetBackgroundColour(bgr_clr);
SetFont(wxGetApp().normal_font());
wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->SetMinSize(wxSize(50 * wxGetApp().em_unit(), -1));
auto main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10);
// logo
m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192);
m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp());
hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL);
wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL);
hsizer->Add(vsizer, 1, wxEXPAND|wxLEFT, 20);
// title
{
wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize);
wxFont title_font = wxGetApp().bold_font();
title_font.SetFamily(wxFONTFAMILY_ROMAN);
title_font.SetPointSize(22);
title->SetFont(title_font);
vsizer->Add(title, 0, wxEXPAND | wxTOP, wxGetApp().em_unit()/*50*/);
}
// main_info_text
wxFont font = get_default_font(this);
const auto text_clr = wxGetApp().get_label_clr_default();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue()));
auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()));
const int fs = font.GetPointSize() - 1;
int size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER);
{
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_html->SetBorders(2);
const auto text = wxString::Format(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
get_main_info(true));
m_html->SetPage(text);
vsizer->Add(m_html, 1, wxEXPAND | wxBOTTOM, wxGetApp().em_unit());
}
// opengl_info
m_opengl_info_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO);
{
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit()));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_opengl_info_html->SetBorders(10);
wxString blacklisted_libraries_message;
#ifdef WIN32
std::wstring blacklisted_libraries = BlacklistedLibraryCheck::get_instance().get_blacklisted_string().c_str();
if (! blacklisted_libraries.empty())
blacklisted_libraries_message = wxString("<br><b>") + _L("Blacklisted libraries loaded into OrcaSlicer process:") + "</b><br>" + blacklisted_libraries;
#endif // WIN32
const auto text = GUI::format_wxstr(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s<br>%s<br>%s<br>%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
blacklisted_libraries_message,
get_mem_info(true), wxGetApp().get_gl_info(false),
"<b>" + _L("SIMD is supported:") + "</b> " + Eigen::SimdInstructionSetsInUse());
m_opengl_info_html->SetPage(text);
main_sizer->Add(m_opengl_info_html, 1, wxEXPAND | wxBOTTOM, 15);
}
wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK);
m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize);
buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5);
m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this);
this->SetEscapeId(wxID_OK);
this->Bind(wxEVT_BUTTON, &SysInfoDialog::onCloseDialog, this, wxID_OK);
main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3);
wxGetApp().UpdateDlgDarkUI(this, true);
// this->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
// logo->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
SetSizer(main_sizer);
main_sizer->SetSizeHints(this);
}
void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect)
{
m_logo_bmp.msw_rescale();
m_logo->SetBitmap(m_logo_bmp.bmp());
wxFont font = get_default_font(this);
const int fs = font.GetPointSize() - 1;
int font_size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_html->Refresh();
const int& em = em_unit();
msw_buttons_rescale(this, em, { wxID_OK, m_btn_copy_to_clipboard->GetId() });
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * em));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_opengl_info_html->Refresh();
const wxSize& size = wxSize(65 * em, 55 * em);
SetMinSize(size);
Fit();
Refresh();
}
void SysInfoDialog::onCopyToClipboard(wxEvent &)
{
wxTheClipboard->Open();
const auto text = get_main_info(false) + "\n" + wxGetApp().get_gl_info(true);
wxTheClipboard->SetData(new wxTextDataObject(text));
wxTheClipboard->Close();
}
void SysInfoDialog::onCloseDialog(wxEvent &)
{
this->EndModal(wxID_CLOSE);
}
} // namespace GUI
} // namespace Slic3r
+36
View File
@@ -0,0 +1,36 @@
#ifndef slic3r_GUI_SysInfoDialog_hpp_
#define slic3r_GUI_SysInfoDialog_hpp_
#include <wx/event.h>
#include <wx/wx.h>
#include <wx/html/htmlwin.h>
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
class SysInfoDialog : public DPIDialog
{
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_logo;
wxHtmlWindow* m_opengl_info_html;
wxHtmlWindow* m_html;
wxButton* m_btn_copy_to_clipboard;
public:
SysInfoDialog();
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void onCopyToClipboard(wxEvent &);
void onCloseDialog(wxEvent &);
};
} // namespace GUI
} // namespace Slic3r
#endif
View File
View File
+9 -29
View File
@@ -82,11 +82,6 @@ ComboBox::ComboBox(wxWindow *parent,
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
drop.Bind(EVT_DISMISS, [this](auto &) {
drop_down = false;
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
@@ -256,7 +251,7 @@ int ComboBox::Append(const wxString &text,
int style)
{
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
item.style = style;
items.push_back(item);
SetClientDataType(wxClientData_Void);
@@ -302,12 +297,6 @@ void ComboBox::ForceDropdownOpen()
}
}
void ComboBox::CloseDropdown()
{
if (drop.IsShown())
drop.DismissAndNotify();
}
unsigned int ComboBox::GetCount() const { return items.size(); }
void ComboBox::set_replace_text(wxString text, wxString image_name)
@@ -361,13 +350,6 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
drop.Invalidate();
}
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
{
if (n >= items.size()) return;
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
drop.Invalidate();
}
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
unsigned int pos,
void ** clientData,
@@ -375,7 +357,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
{
if (pos > this->items.size()) return -1;
for (int i = 0; i < items.GetCount(); ++i) {
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
this->items.insert(this->items.begin() + pos, item);
++pos;
}
@@ -437,18 +419,16 @@ void ComboBox::keyDown(wxKeyEvent& event)
case WXK_UP:
case WXK_DOWN:
case WXK_LEFT:
case WXK_RIGHT: {
// Step over disabled rows: the mouse cannot select them either.
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
int n = GetSelection() + step;
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
n += step;
if (n < 0 || n >= int(items.size()))
case WXK_RIGHT:
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
SetSelection(GetSelection() - 1);
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
SetSelection(GetSelection() + 1);
} else {
break;
SetSelection(n);
}
sendComboBoxEvent();
break;
}
case WXK_TAB:
HandleAsNavigationKey(event);
break;
-2
View File
@@ -96,12 +96,10 @@ public:
wxBitmap GetItemBitmap(unsigned int n);
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
bool is_drop_down(){return drop_down;}
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
void ForceDropdownOpen();
void CloseDropdown();
protected:
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
+4 -64
View File
@@ -30,7 +30,6 @@
#include "slic3r/GUI/wxExtensions.hpp"
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
@@ -243,19 +242,6 @@ static wxSize GetBmpSize(wxBitmap & bmp)
#endif
}
// The x-range [first, second) of the column at the right of the rows that holds the items' action
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
// clicks against it, so the drawn icon and its clickable area always match.
std::pair<int, int> DropDown::actionColumn() const
{
if (actionIconWidth <= 0)
return {0, 0};
int right = rowSize.x;
if (rowSize.y * count > GetSize().y) // position bar
right -= 6;
return {right - actionIconWidth - FromDIP(16), right};
}
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
{
// save dc
@@ -385,10 +371,6 @@ void DropDown::render(wxDC &dc)
rcContent.width -= szBmp.x + 5;
}
const auto action_column = actionColumn();
if (action_column.second > action_column.first)
rcContent.width = action_column.first - rcContent.x;
std::set<wxString> groups;
// draw texts & icons
int index = 0;
@@ -452,24 +434,13 @@ void DropDown::render(wxDC &dc)
auto text = group.IsEmpty()
? (item.group_key.IsEmpty() ? item.text : item.group_label)
: item.text;
if (item.action_icon.IsOk()) {
auto action_size = GetBmpSize(item.action_icon);
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
rcContent.y + (rcContent.height - action_size.y) / 2);
dc.DrawBitmap(item.action_icon, action_pt);
}
if (!text_off && !text.IsEmpty()) {
wxSize tSize = dc.GetMultiLineTextExtent(text);
if (pt.x + tSize.x > rcContent.GetRight()) {
int max_text_width = rcContent.GetRight() - pt.x;
if (max_text_width <= 0) {
text.clear();
} else {
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
}
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
rcContent.GetRight() - pt.x);
}
pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont());
@@ -558,7 +529,6 @@ void DropDown::messureSize()
if (!need_sync) return;
textSize = wxSize();
iconSize = wxSize();
actionIconWidth = 0;
count = 0;
wxClientDC dc(GetParent() ? GetParent() : this);
dc.SetFont(GetFont());
@@ -595,13 +565,8 @@ void DropDown::messureSize()
size1.x += size2.x + (text_off ? 0 : 5);
}
}
if (item.action_icon.IsOk())
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
if (size1.x > textSize.x) textSize = size1;
}
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
if (actionIconWidth > 0)
textSize.x += actionIconWidth + FromDIP(16);
if (!align_icon) iconSize.x = 0;
wxSize szContent = textSize;
if (szContent.x < FromDIP(120) && !use_content_width)
@@ -671,11 +636,6 @@ void DropDown::messureSize()
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
}
need_sync = false;
}
@@ -754,15 +714,6 @@ void DropDown::mouseReleased(wxMouseEvent& event)
if (hover_item < 0)
return;
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
int index = hoverIndex();
if (index >= 0 && items[index].action_icon.IsOk()) {
const auto action_column = actionColumn();
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
sendDropDownActionEvent(index);
paintNow();
return;
}
}
sendDropDownEvent();
if (mainDropDown)
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
@@ -879,17 +830,6 @@ void DropDown::sendDropDownEvent()
GetEventHandler()->ProcessEvent(event);
}
void DropDown::sendDropDownActionEvent(int index)
{
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
return;
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
event.SetEventObject(this);
event.SetInt(index);
event.SetString(items[index].text);
GetEventHandler()->ProcessEvent(event);
}
void DropDown::Dismiss()
{
if (subDropDown && subDropDown->IsShown())
-6
View File
@@ -5,7 +5,6 @@
#include <wx/event.h>
#include <vector>
#include <cstddef>
#include <utility>
#include <wx/gdicmn.h>
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <boost/date_time/posix_time/ptime.hpp>
@@ -28,7 +27,6 @@ class wxWindow;
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
class DropDown : public PopupWindow
{
@@ -39,7 +37,6 @@ public:
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
wxBitmap icon;
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
wxBitmap action_icon;
void * data{nullptr};
wxString group_key{};
wxString group_label{};
@@ -68,7 +65,6 @@ private:
wxSize textSize;
wxSize iconSize;
int actionIconWidth = 0;
wxSize rowSize;
StateHandler state_handler;
@@ -139,7 +135,6 @@ private:
int hoverIndex();
int selectedItem();
std::pair<int, int> actionColumn() const;
friend class ComboBox;
void messureSize();
@@ -153,7 +148,6 @@ private:
void mouseWheelMoved(wxMouseEvent &event);
void sendDropDownEvent();
void sendDropDownActionEvent(int index);
DECLARE_EVENT_TABLE()
+15
View File
@@ -0,0 +1,15 @@
#ifndef slic3r_wxinit_hpp_
#define slic3r_wxinit_hpp_
#include <wx/wx.h>
#include <wx/intl.h>
#include <wx/html/htmlwin.h>
// Perl redefines a _ macro, so we undef this one
#undef _
// We do want to use translation however, so define it as __ so we can do a find/replace
// later when we no longer need to undef _
#define __(s) wxGetTranslation((s))
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef slic3r_GUI_Profile_hpp_
#define slic3r_GUI_Profile_hpp_
// Profiling support using the Shiny intrusive profiler
//#define SLIC3R_PROFILE_GUI
#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI)
#include <Shiny/Shiny.h>
#define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x)
#else
#define SLIC3R_GUI_PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x)
#endif
#endif // slic3r_GUI_Profile_hpp_
+13 -215
View File
@@ -28,7 +28,6 @@
#include <miniz.h>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include <memory>
#include <cstdint>
#include <ios>
@@ -39,7 +38,6 @@
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <functional>
#include <initializer_list>
#include <catch2/catch_tostring.hpp>
#include <Eigen/Core>
@@ -351,24 +349,6 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf
return replaced;
}
// Stores `model` as a one-plate project.
static void store_project(Model& model, const std::string& path, SaveStrategy strategy)
{
ScopedTemporaryDir backup_dir("orca_paint_src");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig cfg;
PlateData plate;
plate.plate_index = 0;
StoreParams sp;
sp.path = path.c_str();
sp.model = &model;
sp.config = &cfg;
sp.strategy = strategy;
sp.plate_data_list.push_back(&plate);
REQUIRE(store_bbs_3mf(sp));
}
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
static void store_painted_cube(const std::string& path)
@@ -383,7 +363,19 @@ static void store_painted_cube(const std::string& path)
selector.set_facet(1, EnforcerBlockerType::Extruder3);
REQUIRE(volume->mmu_segmentation_facets.set(selector));
}
store_project(model, path, SaveStrategy::Zip64 | SaveStrategy::Silence);
ScopedTemporaryDir backup_dir("orca_paint_src");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig cfg;
PlateData plate;
plate.plate_index = 0;
StoreParams sp;
sp.path = path.c_str();
sp.model = &model;
sp.config = &cfg;
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
sp.plate_data_list.push_back(&plate);
REQUIRE(store_bbs_3mf(sp));
}
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
@@ -509,163 +501,6 @@ TEST_CASE("A project with malformed paint data loads without the damaged facet",
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
}
// Paints `triangles` of `volume` on texture displacement layer slot `slot`.
static void paint_texture_slot(ModelVolume& volume, int slot, std::initializer_list<int> triangles)
{
TriangleSelector selector(volume.mesh());
for (const int triangle : triangles)
selector.set_facet(triangle, EnforcerBlockerType::ENFORCER);
REQUIRE(volume.texture_displacement_facet(slot).set(selector));
}
// Every triangle's texture paint on every slot, in the form the exporter writes it, so two volumes
// compare equal only when the same triangles are painted on the same slots.
static std::vector<std::string> texture_paint(const ModelVolume& volume)
{
std::vector<std::string> paint;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
for (size_t i = 0; i < volume.mesh().its.indices.size(); ++i)
paint.push_back(volume.texture_displacement_facet(slot).get_triangle_as_string(int(i)));
return paint;
}
// A cube with one texture displacement layer in slot 2 and texture paint on gapped triangles of two
// slots. The layer's image bytes need not decode: the archive stores them as they are.
static ModelVolume& add_texture_displaced_cube(Model& model)
{
ModelObject* object = model.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 2;
layer.name = "Bark";
layer.depth_mm = 1.25f;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::vector<unsigned char>{ 0x89, 'P', 'N', 'G', 1, 2, 3 });
volume->texture_displacement_layers.push_back(layer);
volume->texture_displacement_options.active_slot = 2;
paint_texture_slot(*volume, 2, { 3, 7 });
paint_texture_slot(*volume, 5, { 10 });
return *volume;
}
TEST_CASE("Texture displacement layers, textures and paint survive a project round trip", "[3mf]")
{
// A single model file, and one file per object (read by a separate importer).
const SaveStrategy strategy = GENERATE(SaveStrategy::Zip64 | SaveStrategy::Silence,
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel);
INFO("save strategy " << int(strategy));
ScopedTemporaryFile temp(".3mf");
Model source;
const ModelVolume& painted = add_texture_displaced_cube(source);
const std::vector<std::string> paint = texture_paint(painted);
REQUIRE(std::count_if(paint.begin(), paint.end(), [](const std::string& s) { return !s.empty(); }) == 3);
store_project(source, temp.string(), strategy);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const ModelVolume& volume = *model.objects.front()->volumes.front();
REQUIRE(volume.texture_displacement_layers.size() == 1);
const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front();
CHECK(layer.slot == 2);
CHECK(layer.name == "Bark");
CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(1.25f, 1e-6f));
REQUIRE(layer.image_data);
CHECK(*layer.image_data == *painted.texture_displacement_layers.front().image_data);
CHECK(volume.texture_displacement_options.active_slot == 2);
CHECK(texture_paint(volume) == paint);
}
TEST_CASE("Volumes sharing a mesh keep their own texture displacement paint", "[3mf]")
{
Model source;
ModelObject* first_object = source.add_object();
ModelVolume* first = first_object->add_volume(make_cube(10., 10., 10.));
first_object->add_instance();
ModelVolume* second = source.add_object(*first_object)->volumes.front();
REQUIRE(second->mesh_ptr() == first->mesh_ptr());
paint_texture_slot(*first, 0, { 3 });
paint_texture_slot(*second, 0, { 7 });
// What a project save from the app uses: one file per object, an identical mesh written once.
ScopedTemporaryFile temp(".3mf");
store_project(source, temp.string(),
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::ShareMesh);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 2);
CHECK(texture_paint(*model.objects[0]->volumes.front()) == texture_paint(*first));
CHECK(texture_paint(*model.objects[1]->volumes.front()) == texture_paint(*second));
}
TEST_CASE("A texture displacement layer never loads its image from outside the project", "[3mf]")
{
// A file the layer names, on the machine opening the project, that the project did not carry.
ScopedTemporaryFile texture(".png");
{
boost::nowide::ofstream file(texture.string(), std::ios::binary);
file << "not part of the project";
}
REQUIRE(boost::filesystem::file_size(texture.path()) > 0);
Model source;
ModelObject* object = source.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 0;
layer.name = "Elsewhere";
layer.path = texture.string();
layer.depth_mm = 0.75f;
volume->texture_displacement_layers.push_back(layer);
ScopedTemporaryFile temp(".3mf");
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const std::vector<TextureDisplacementLayer>& layers = model.objects.front()->volumes.front()->texture_displacement_layers;
REQUIRE(layers.size() == 1);
CHECK(layers.front().name == "Elsewhere");
CHECK_THAT(layers.front().depth_mm, Catch::Matchers::WithinAbs(0.75f, 1e-6f));
CHECK_FALSE(layers.front().image_data);
}
TEST_CASE("A project with a malformed texture displacement value still loads", "[3mf][Regression]")
{
const auto [from, to] = GENERATE(table<std::string, std::string>({
{ "\"islands\":[]", "\"islands\":[[null,0,0,1]]" }, // a number that is null
{ "\"slot\":2", "\"slot\":8" }, // a slot past the last
}));
INFO(from << " -> " << to);
ScopedTemporaryFile temp(".3mf");
Model source;
const ModelVolume& painted = add_texture_displaced_cube(source);
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
REQUIRE(replace_in_3mf_entry(temp.string(), ".json", from, to));
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
bool loaded = false;
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
REQUIRE(loaded);
REQUIRE(model.objects.size() == 1);
const ModelVolume& volume = *model.objects.front()->volumes.front();
// The layer stack is dropped as a whole; the paint, stored apart from it, still loads.
CHECK(volume.texture_displacement_layers.empty());
CHECK(texture_paint(volume) == texture_paint(painted));
}
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
// store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for
// files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves:
@@ -2208,40 +2043,3 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
CHECK_FALSE(parser.check_3mf_from_prusa(path));
}
}
TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]")
{
Model model;
REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model));
model.add_default_instances();
ScopedTemporaryDir backup_dir("orca_pick");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
auto plate = std::make_unique<PlateData>();
plate->plate_index = 0;
plate->pick_file = (backup_dir.path() / "pick.png").string();
plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string();
ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string();
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &model;
store_params.config = &config;
store_params.plate_data_list.push_back(plate.get());
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
REQUIRE(store_bbs_3mf(store_params));
boost::nowide::ifstream in(test_file, std::ios::binary);
Model dst_model;
DynamicPrintConfig dst_config;
PlateDataPtrs dst_plates;
Semver file_version;
dst_model.set_backup_path(backup_dir.string());
REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version));
REQUIRE(dst_plates.size() == 1);
CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png");
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
release_PlateData_list(dst_plates);
}
@@ -37,7 +37,6 @@
#include <utility>
#include "libslic3r/Config.hpp"
#include <map>
#include <regex>
#include "libslic3r/libslic3r.h"
#include "libslic3r/PublishSettings.hpp"
#include <set>
@@ -7162,126 +7161,3 @@ TEST_CASE("A user filament whose derived compatible printer cannot be written ba
CHECK(read_file(file) == before);
}
#endif
TEST_CASE("User filaments that lost printers their narrowed system parent listed in an older release are found", "[Preset][Bundle]")
{
PresetBundle bundle;
PresetCollection &filaments = bundle.filaments;
const std::string parent = "Generic PETG @Creality K1-all";
// load_preset inserts in sorted order, so presets are looked up again rather than held by reference.
auto set_printers = [&filaments](const std::string &name, std::vector<std::string> printers) {
filaments.find_preset(name)->config.option<ConfigOptionStrings>("compatible_printers", true)->values = std::move(printers);
};
add_inmemory_preset(filaments, parent).is_system = true;
add_inmemory_preset(filaments, "My PETG", parent);
add_inmemory_preset(filaments, "My PETG own list", parent);
set_printers(parent, { "Creality K1C 0.6 nozzle" });
set_printers("My PETG", { "Creality K1C 0.6 nozzle" });
set_printers("My PETG own list", { "Creality K1C 0.6 nozzle", "Creality K1C 0.8 nozzle" });
// A printer the parent still lists is not reported again. The system preset and a user preset
// with its own list are left alone.
std::vector<LegacyPrinterMigration> migrations =
filaments.legacy_printer_migrations({ { parent, { "Creality K1C 0.4 nozzle", "Creality K1C 0.6 nozzle" } } });
REQUIRE(migrations.size() == 1);
CHECK(migrations[0].preset == "My PETG");
CHECK(migrations[0].parent == parent);
CHECK(migrations[0].printers == std::vector<std::string>{ "Creality K1C 0.4 nozzle" });
// A parent renamed after the map was written is still found under its old name.
add_inmemory_preset(filaments, "Generic PETG @Creality K1 renamed").is_system = true;
add_inmemory_preset(filaments, "My renamed PETG", "Generic PETG @Creality K1 renamed");
set_renamed_from(filaments, "Generic PETG @Creality K1 renamed", { parent });
set_printers("Generic PETG @Creality K1 renamed", { "Creality K1C 0.6 nozzle" });
set_printers("My renamed PETG", { "Creality K1C 0.6 nozzle" });
migrations = filaments.legacy_printer_migrations({ { parent, { "Creality K1C 0.4 nozzle" } } });
REQUIRE(migrations.size() == 2);
CHECK(migrations[1].preset == "My renamed PETG");
CHECK(migrations[1].parent == "Generic PETG @Creality K1 renamed");
}
TEST_CASE("Migrating a user filament backs up the user folder to a zip and writes the legacy printers to its file", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path());
const std::string parent = "Generic PETG @Creality K1-all";
auto with_parent = [&parent](PresetBundle &bundle) {
add_inmemory_preset(bundle.filaments, parent).is_system = true;
bundle.filaments.find_preset(parent)->config.option<ConfigOptionStrings>("compatible_printers", true)->values = { "Creality K1C 0.6 nozzle" };
};
// Like a real user preset, the file states only its diff, so the printer list comes from the parent.
const fs::path user_dir = temp_dir.path() / PRESET_USER_DIR / "default";
const fs::path file = user_dir / PRESET_FILAMENT_NAME / "My PETG.json";
fs::create_directories(file.parent_path());
const std::string original = std::string(R"({"type":"filament","name":"My PETG","from":"User","version":"1.0.0","inherits":")") + parent + R"("})";
std::ofstream(file.string()) << original;
auto load = [&user_dir](PresetCollection &filaments) {
PresetsConfigSubstitutions substitutions;
filaments.load_presets(user_dir.string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::EnableSilent);
};
PresetBundle bundle;
PresetCollection &filaments = bundle.filaments;
with_parent(bundle);
load(filaments);
const std::map<std::string, std::vector<std::string>> map{ { parent, { "Creality K1C 0.4 nozzle" } } };
const std::vector<LegacyPrinterMigration> migrations = filaments.legacy_printer_migrations(map);
REQUIRE(migrations.size() == 1);
std::vector<std::string> failed;
const std::string backup = bundle.migrate_legacy_printers(migrations, failed);
REQUIRE_FALSE(backup.empty());
CHECK(std::regex_match(fs::path(backup).filename().string(), std::regex("filament_preset_migration_\\d{8}T\\d{6}Z_backup\\.zip")));
CHECK(failed.empty());
const fs::path extracted = temp_dir.path() / "extracted";
REQUIRE(extract_archive_confined(backup, extracted.string()));
CHECK(read_file(extracted / PRESET_USER_DIR / "default" / PRESET_FILAMENT_NAME / "My PETG.json") == original);
// Reloaded from disk, the preset carries both printers itself and needs no further migration.
PresetBundle reloaded;
with_parent(reloaded);
load(reloaded.filaments);
const std::vector<std::string> expected{ "Creality K1C 0.6 nozzle", "Creality K1C 0.4 nozzle" };
CHECK(reloaded.filaments.find_preset("My PETG")->config.option<ConfigOptionStrings>("compatible_printers")->values == expected);
CHECK(reloaded.filaments.legacy_printer_migrations(map).empty());
CHECK_FALSE(reloaded.filaments.apply_legacy_printer_migration(migrations[0]));
}
namespace {
// A filament collection that exposes the rename map update, which PresetBundle runs after loading system presets.
struct RenameTestFilaments : public PresetCollection
{
RenameTestFilaments()
: PresetCollection(Preset::TYPE_FILAMENT, Preset::filament_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()))
{}
using PresetCollection::update_map_system_profile_renamed;
};
} // namespace
TEST_CASE("Migrating a user filament keeps the old name of its renamed parent", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
RenameTestFilaments filaments;
const std::string parent = "Generic PETG @Creality K1-all";
const std::string old_parent = "Creality Generic PETG @K1-all";
add_inmemory_preset(filaments, parent).is_system = true;
filaments.find_preset(parent)->config.option<ConfigOptionStrings>("compatible_printers", true)->values = { "Creality K1C 0.6 nozzle" };
set_renamed_from(filaments, parent, { old_parent });
filaments.update_map_system_profile_renamed();
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PETG.json";
fs::create_directories(file.parent_path());
std::ofstream(file.string()) << R"({"type":"filament","name":"My PETG","from":"User","version":"1.0.0","inherits":")" << old_parent << R"("})";
PresetsConfigSubstitutions substitutions;
filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::EnableSilent);
const std::vector<LegacyPrinterMigration> migrations = filaments.legacy_printer_migrations({ { parent, { "Creality K1C 0.4 nozzle" } } });
REQUIRE(migrations.size() == 1);
REQUIRE(filaments.apply_legacy_printer_migration(migrations[0]));
// An older release knows the parent only by its old name.
const std::string saved = read_file(file);
CHECK(saved.find("\"inherits\": \"" + old_parent + "\"") != std::string::npos);
CHECK(saved.find("Creality K1C 0.4 nozzle") != std::string::npos);
}
@@ -32,7 +32,6 @@
#include <fstream>
#include <limits>
#include <set>
#include <string>
#include <boost/filesystem.hpp>
#include "libslic3r/AABBMesh.hpp"
@@ -2158,177 +2157,6 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
CHECK(second <= single * 5 / 4);
}
// The .3mf stores the layer stack as JSON (see texture_displacement_layers_to_json). Two properties
// matter: everything the UV editor and the panel can set has to survive a round trip, and a document
// written by a build that knew fewer fields has to keep loading, with the missing ones left at their
// defaults rather than zeroed.
TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisplacement]")
{
std::vector<TextureDisplacementLayer> layers(2);
TextureDisplacementLayer &a = layers[0];
a.slot = 0;
a.name = "Bark";
a.path = "/textures/bark.png";
a.depth_mm = 1.25f;
a.tiling_scale = 7.5f;
a.rotation_deg = 30.f;
a.offset = Vec2f(0.25f, -0.5f);
a.invert = true;
a.midlevel = 0.125f;
a.smoothing = 0.75f;
a.edge_smoothing = true;
a.edge_smoothing_amount = 0.25f;
a.auto_connect_islands = false;
a.tile_enabled = false;
a.tile_method = TextureTileMethod::MirroredRepeat;
a.projection_method = TextureProjectionMethod::LSCM;
a.blend_mode = TextureBlendMode::Subtract;
a.color_enabled = true;
a.lscm_seam_angle_deg = 45.f;
a.island_padding_mm = 0.5f;
a.lscm_seam_edges = { { 1, 2 }, { 3, 5 } };
a.islands = { TextureIsland{ Vec2f(1.f, 2.f), 15.f, 1.5f }, TextureIsland{} };
a.island_groups = { 0, 0 };
a.lscm_uv_overrides = { { -4, Vec2f(0.5f, 0.75f) } };
TextureDisplacementLayer &b = layers[1];
b.slot = 1;
b.projection_method = TextureProjectionMethod::ViewProjected;
b.view_project_right = Vec3f(0.f, 1.f, 0.f);
b.view_project_up = Vec3f(0.f, 0.f, 1.f);
b.view_project_projective = true;
for (size_t i = 0; i < b.view_project_matrix.size(); ++i)
b.view_project_matrix[i] = float(i) + 0.5f;
TextureDisplacementOptions options;
options.displace_border = false;
options.smooth_enabled = true;
options.smooth_strength = 0.6f;
options.smooth_iterations = 5;
options.pipeline_v2 = false;
options.v2_max_triangles_k = 250;
options.color_mix_count = 3;
options.color_despeckle = 4;
options.active_slot = 3;
options.panel_mode = 1;
const std::string json = texture_displacement_layers_to_json(layers, options);
std::vector<TextureDisplacementLayer> read_layers;
TextureDisplacementOptions read_options;
REQUIRE(texture_displacement_layers_from_json(json, read_layers, read_options));
REQUIRE(read_layers.size() == layers.size());
const TextureDisplacementLayer &ra = read_layers[0];
CHECK(ra.name == a.name);
CHECK(ra.path == a.path);
CHECK_THAT(ra.depth_mm, WithinAbs(a.depth_mm, 1e-6f));
CHECK_THAT(ra.tiling_scale, WithinAbs(a.tiling_scale, 1e-6f));
CHECK_THAT(ra.rotation_deg, WithinAbs(a.rotation_deg, 1e-6f));
CHECK(ra.offset.isApprox(a.offset));
CHECK(ra.invert == a.invert);
CHECK_THAT(ra.midlevel, WithinAbs(a.midlevel, 1e-6f));
CHECK_THAT(ra.smoothing, WithinAbs(a.smoothing, 1e-6f));
CHECK(ra.edge_smoothing == a.edge_smoothing);
CHECK_THAT(ra.edge_smoothing_amount, WithinAbs(a.edge_smoothing_amount, 1e-6f));
CHECK(ra.auto_connect_islands == a.auto_connect_islands);
CHECK(ra.tile_enabled == a.tile_enabled);
CHECK(ra.tile_method == a.tile_method);
CHECK(ra.projection_method == a.projection_method);
CHECK(ra.blend_mode == a.blend_mode);
CHECK(ra.color_enabled == a.color_enabled);
CHECK_THAT(ra.lscm_seam_angle_deg, WithinAbs(a.lscm_seam_angle_deg, 1e-6f));
CHECK_THAT(ra.island_padding_mm, WithinAbs(a.island_padding_mm, 1e-6f));
CHECK(ra.lscm_seam_edges == a.lscm_seam_edges);
REQUIRE(ra.islands.size() == a.islands.size());
CHECK(ra.islands[0].offset.isApprox(a.islands[0].offset));
CHECK_THAT(ra.islands[0].rotation_deg, WithinAbs(a.islands[0].rotation_deg, 1e-6f));
CHECK_THAT(ra.islands[0].scale, WithinAbs(a.islands[0].scale, 1e-6f));
CHECK(ra.island_groups == a.island_groups);
REQUIRE(ra.lscm_uv_overrides.size() == 1);
CHECK(ra.lscm_uv_overrides[0].first == -4);
CHECK(ra.lscm_uv_overrides[0].second.isApprox(Vec2f(0.5f, 0.75f)));
const TextureDisplacementLayer &rb = read_layers[1];
CHECK(rb.projection_method == TextureProjectionMethod::ViewProjected);
CHECK(rb.view_project_right.isApprox(b.view_project_right));
CHECK(rb.view_project_up.isApprox(b.view_project_up));
CHECK(rb.view_project_projective);
CHECK(rb.view_project_matrix == b.view_project_matrix);
CHECK(read_options.displace_border == options.displace_border);
CHECK(read_options.smooth_enabled == options.smooth_enabled);
CHECK_THAT(read_options.smooth_strength, WithinAbs(options.smooth_strength, 1e-6f));
CHECK(read_options.smooth_iterations == options.smooth_iterations);
CHECK(read_options.pipeline_v2 == options.pipeline_v2);
CHECK(read_options.v2_max_triangles_k == options.v2_max_triangles_k);
CHECK(read_options.color_mix_count == options.color_mix_count);
CHECK(read_options.color_despeckle == options.color_despeckle);
// Where the panel was left comes back with the rest, so a reopened project is on the same layer.
CHECK(read_options.active_slot == options.active_slot);
CHECK(read_options.panel_mode == options.panel_mode);
}
TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", "[TextureDisplacement]")
{
// What a build that knew fewer fields would have written: one layer, almost nothing set.
const std::string sparse = R"({"version":1,"layers":[{"slot":2,"name":"Old","depth_mm":0.75}]})";
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementOptions options;
REQUIRE(texture_displacement_layers_from_json(sparse, layers, options));
REQUIRE(layers.size() == 1);
const TextureDisplacementLayer &l = layers[0];
const TextureDisplacementLayer fresh;
const TextureDisplacementOptions defaults;
CHECK(l.slot == 2);
CHECK(l.name == "Old");
CHECK_THAT(l.depth_mm, WithinAbs(0.75f, 1e-6f));
// Everything absent keeps the struct's own default rather than becoming zero.
CHECK_THAT(l.tiling_scale, WithinAbs(fresh.tiling_scale, 1e-6f));
CHECK(l.auto_connect_islands == fresh.auto_connect_islands);
CHECK(l.tile_enabled == fresh.tile_enabled);
CHECK(l.projection_method == fresh.projection_method);
CHECK_THAT(l.lscm_seam_angle_deg, WithinAbs(fresh.lscm_seam_angle_deg, 1e-6f));
CHECK(options.color_mix_count == defaults.color_mix_count);
CHECK(options.pipeline_v2 == defaults.pipeline_v2);
CHECK(options.active_slot == defaults.active_slot);
CHECK(options.panel_mode == defaults.panel_mode);
std::vector<TextureDisplacementLayer> unused_layers;
TextureDisplacementOptions unused_options;
CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options));
}
TEST_CASE("Texture displacement JSON with a wrong-typed element or an out-of-range slot is rejected", "[TextureDisplacement]")
{
const std::string layer = GENERATE(as<std::string>{},
R"({"slot":0,"offset":["x",0]})",
R"({"slot":0,"view_project_right":[null,0,0]})",
R"({"slot":0,"view_project_matrix":[0,0,0,0,0,0,0,0,0,0,0,"x"]})",
R"({"slot":0,"lscm_seam_edges":[1,null]})",
R"({"slot":0,"islands":[[0,0,"x",1]]})",
R"({"slot":0,"lscm_uv_overrides":[[null,0,0]]})",
R"({"slot":8})",
R"({"slot":-1})",
R"({"slot":"x"})",
R"({"name":"No slot"})");
INFO(layer);
std::vector<TextureDisplacementLayer> layers(1);
layers.front().name = "Kept";
TextureDisplacementOptions options;
options.color_mix_count = 7;
bool parsed = true;
REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(R"({"version":1,"layers":[)" + layer + "]}", layers, options));
CHECK_FALSE(parsed);
REQUIRE(layers.size() == 1);
CHECK(layers.front().name == "Kept");
CHECK(options.color_mix_count == 7);
}
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
{
TextureDisplacementLayer gray;
+1 -1
View File
@@ -255,7 +255,7 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands"
const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts");
REQUIRE(first != nullptr);
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_migrate_filament_presets", "help_troubleshoot", "help_network_test",
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test",
"help_benchmark_3d_scene", "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki",
"open_youtube"}) {
const Slic3r::GUI::NativeCommand* c = find(key);