Compare commits

..
Author SHA1 Message Date
ExPikaPaka fbaed8a512 Project the texture in the model's own orientation, and stop the preview tiling
The bake frame took its orientation from the world, so turning a part on the
plate slid the decoration across its surface. Turning a part is a print-time
decision - clearance, supports, warping - and it should not be a design one.
The frame now takes its orientation from the model and keeps world scale, so a
tile is still tiling_scale printed millimetres whatever the instance's scale.

The rotation is separated out by polar decomposition rather than by assuming
the linear part is a rotation times a scale, since a sheared or mirrored
placement is legal here. A reflection is deliberately left with the scale, so a
mirrored instance still makes the frame's determinant negative and the bake
still flips the normals it derives.

The preview shaders rebuild that frame themselves from a world position, so
they take the same rotation through a uniform; without it the fast preview and
the bake would disagree, which is the class of bug this is fixing. The gizmo's
own way back out to world carries the rotation too, which it did not need to
when the frame was world-aligned.

Tiling in the fast preview:

- the sampler wrap mode was set on the height texture but not on the colour
  texture, which sits on its own unit and is sampled at the same uv. So with
  Tile off the relief stopped at one placement while the colour carried on
  repeating around it.
- outside a single placement the colour then came out black, because a zero
  texture border cannot say "no colour here", only "the colour is black here".
  The shader now tests the uv the way sample_layer_color() does and leaves the
  surface its own colour.

The placement gizmo is a tile of a planar projection, so it is offered only for
the mappings that have one. An unwrap cuts the surface into islands and packs
them, and a model's own UV channel is whatever its author drew: one tile lands
there as an arbitrary curved patch with no rectangle to take hold of. Those two
say so and point at the UV editor instead.
2026-10-15 15:45:47 +02:00
ExPikaPaka 597b2abac4 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-15 13:13:26 +02:00
ExPikaPaka ba4de8c1fc Measure the placement gizmo in the frame the texture is projected in
The gizmo worked in mesh-local millimetres while the texture is projected in
the bake frame - world orientation and scale, anchored at the volume's origin.
On an unscaled object the two agree, which is why this went unnoticed; on a
scaled one the drawn tile differed from the rendered one by exactly the
object's scale, and a rotated one would have skewed its axes as well.

The anchor is carried into that frame at the point of use rather than stored
converted, since the object can be moved, turned or scaled without leaving the
mode. Getting from the frame to the screen is then a translation and nothing
more, which is the point: rotation and scale are what a tile's millimetres must
not be measured through.

Also in this commit: a pale casing outside the dark halo, because a dark halo
alone disappears against a dark backdrop and a photographic texture offers both
within one tile; one rotate ring instead of four, moved to whichever corner the
cursor is nearest and held there by a margin so it does not flit between two;
one constant for the casing's width, after the move grip's dots turned out to
carry nearly twice the casing of everything around them; a request for one more
frame when a panel edit changes a preview parameter, since the panel is drawn
after the 3D view and the edit was otherwise a frame late; and an animated
illustration for the square-tile lock.
2026-10-15 13:13:26 +02:00
ExPikaPaka 7bce53a773 Merge branch 'main' into texture-placement-gizmo
Three files conflicted, all in the texture displacement subsystem that main
has been working on in parallel.

apply_uv_transform and UVTransform gained main's normalized_uv parameter for
TextureProjectionMethod::ModelUV. A normalized uv has no millimetre tile to
divide by, so the extents are skipped for it, but the mirroring still applies:
a mirror is a placement choice and means the same thing on a model's own UV
layout as on a projected one. The aspect correction stays skipped, as main has
it.

adjust_tangent_basis now calls main's box_projection_axes instead of restating
the dominant-axis choice, which also picks up main's fix to the texture's
orientation. The gizmo derives its whole frame from that basis, so it follows
the new conventions without needing to know about them.

The basic settings rows took main's float_row, which carries the tooltip and
its media as arguments, and its disabling of tile size under ModelUV. The
square lock and the second extent join that disabled block, being the same
millimetres from another angle.

Layers now persist to the .3mf as JSON, which they did not when this branch
started, so tiling_scale_v, mirror_u and mirror_v are written and read there
too. The reader keeps the default for a missing key, so a project written
before the tile could be rectangular or mirrored still loads as a square,
unmirrored one.
2026-10-15 09:54:52 +02:00
ExPikaPaka e65113af6d Add edge and corner resize, mirroring and a square lock to the gizmo
Follows the first pass on the placement gizmo.

Handles and hit radii are twice the size, and the minimum frame with them,
so the corner zones cannot bury the move area on a small tile.

Each edge carries a bar that sets one tile extent, so width and height can
be set on the model rather than only in the panel. The four axis arrows
stay, further out on the same rays, kept apart from the bars by distance.

Dragging a width or height handle back through the tile's middle mirrors
the texture on that axis instead of sticking at the minimum extent. The
new mirror_u and mirror_v ride as a sign on the same division as the tile
extents, in apply_uv_transform and in all four shaders, and the shaded
shaders' normal gradient carries the sign too.

The frame was centred on uv 0, which is a corner of the image, so it
straddled the four tiles meeting there and rotation pivoted on that
corner. It is now centred on uv (0.5, 0.5): the frame bounds one whole
tile and rotating or scaling turns and resizes the image about its middle.

A scale drag read the extents straight off the cursor, so grabbing a
handle anywhere inside its hit radius jumped the tile by that much. The
offset of the grab from the handle is now subtracted.

The square-tile lock is an icon on the Tile size row instead of a text
link, reusing lock_closed.svg and lock_open.svg.
2026-10-12 09:31:32 +02:00
ExPikaPaka 91ab04e823 Redesign the texture placement gizmo and give the tile two extents
The on-canvas control for a texture layer's placement was a translucent
orange quad with two hairline GL arrows. It moved the texture and nothing
else, had no hover state, took no notice of the theme, and sized itself
from the volume's bounding box, which left it tiny on small parts and
huge on large ones. Rotation and tile size were panel-only numbers.

The gizmo is now one tile of the texture lying in the paint patch's
tangent plane, so it draws the three values it edits: where it sits is
the offset, how it is turned is the rotation, how big it is is the tile
extents. It carries four chevron arms for an axis-constrained move (the
silhouette of the panel's own Adjust placement icon), a six-dot move grip
taken from the drag icon, a corner grip on each corner for the extents,
and a ring on a short stem for the rotation. Colours come from
ImGuiWrapper's COL_ORCA and the icon grey rather than raw orange and
yellow.

It is drawn through ImGui's foreground draw list instead of the flat
shader, which is what allows antialiased sub-pixel strokes, rounded
joins, a contrast halo under every stroke and a numeric readout by the
cursor. That halo matters because a volume stays the same neutral grey
whatever the theme, so the accent alone cannot carry contrast; the halo
strength and the handle fill do. Handle sizes and hit radii are in screen
pixels, so the control is equally usable at any zoom or model size, and a
tile that projects below 44px is drawn at that floor and dashed to say it
is not to scale. Picking is a real point-in-quad test rather than a
circle through the corners, so a move can no longer start from empty
space beside the tile.

Rotating or resizing changes where a given offset places the tile, so
every drag recomputes the offset from the centre it means to keep. That
is what makes rotate and scale act in place instead of swinging the tile
around the anchor, and it leaves one place that writes offset.

Tile size gains a second extent. tiling_scale_v holds the v extent in mm
and anything <= 0 means "same as u", which is both the default and what
every layer written before this meant, so existing layers and older undo
snapshots still read back as the square tiles they were. The new
layer_tile_mm() states that fallback once and every consumer goes through
it: the bake, both preview shaders, the UV pane and the gizmo. The panel
shows one slider until the lock is opened, and the lock state is the data
rather than a separate flag that could fall out of step with it.

The extents now divide after the rotation in apply_uv_transform() and in
all four shaders. Dividing two different extents before it would shear
the tile rather than rotate it; with a square tile both orders are
algebraically identical, so no existing layer changes. The inverse in
UVEditorCanvas::uv_delta_to_unwrap() and the island matrix in
uv_editor_island_transforms() follow the same reordering, and the texel
size used for refinement and step cutting now takes the finer extent.
2026-10-12 08:04:35 +02:00
59 changed files with 7191 additions and 12190 deletions
@@ -569,18 +569,12 @@ on the **first** call. So:
#endif #endif
``` ```
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA **Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44, when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and (non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
Wayland app inherits both). It calls `XInitThreads()`
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions. `references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
**wx's own detectors**, for reference: **wx's own detectors**, for reference:
@@ -61,8 +61,7 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that 11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview) needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview) 12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces 13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions) make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and 14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested `WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
@@ -568,69 +567,33 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions ## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises) **Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`: branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and - **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets `WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42 non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not `DISPLAY` is set.
identify it, because a native Wayland app inherits both. `XInitThreads()` runs only when `DISPLAY` is
set.
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when - **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable `/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages. compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()` Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
(see `references/platforms.md`), not these environment variables. (see `references/platforms.md`), not these environment variables.
**Pitfall.** **Pitfall.**
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before - **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only initialises; never unconditionally. Keep JS-side fallbacks so a page never
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
process, before the first WebView.
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
```cpp
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
// Right: keep compositing, skip only the hardware DMABUF path
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
}
```
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path, in the branch where wx
lacks EGL and Orca forces `GDK_BACKEND=x11`, non-replacing (`setenv(..., false)`, so a value the user
already exported wins), before GTK
initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
waits forever for a C++→JS call. waits forever for a C++→JS call.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving **Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling, blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the acceleration; without the JS timeout the user is stuck when the signal is lost.
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
```cpp ```cpp
// Wrong: unconditional, kills HW acceleration everywhere // Wrong: unconditional, kills HW acceleration everywhere
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42, // Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
// and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the if (display && *display && wayland && *wayland)
// case that runs through XWayland
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
``` ```
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit` Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
+1 -15
View File
@@ -22,7 +22,7 @@ function usage() {
echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner" echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
echo " -h: prints this help text" echo " -h: prints this help text"
echo " -i: build the Orca Slicer AppImage (optional)" echo " -i: build the Orca Slicer AppImage (optional)"
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses" echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
echo " -r: skip RAM and disk checks (low RAM compiling)" echo " -r: skip RAM and disk checks (low RAM compiling)"
echo " -s: build the Orca Slicer (optional)" echo " -s: build the Orca Slicer (optional)"
echo " -t: build tests (optional), requires -s flag" echo " -t: build tests (optional), requires -s flag"
@@ -549,20 +549,6 @@ fi
if [ -n "${CMAKE_CCACHE}" ] ; then if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..." echo "${CMAKE_CCACHE} found, enabling compiler caching..."
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}") export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds." echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
fi fi
+17 -2
View File
@@ -1,7 +1,22 @@
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
if (JPEG_VERSION STREQUAL "6")
message("Using Jpeg Lib 62")
set(jpeg_flag "")
elseif (JPEG_VERSION STREQUAL "7")
message("Using Jpeg Lib 70")
set(jpeg_flag "-DWITH_JPEG7=ON")
else ()
message("Using Jpeg Lib 80")
set(jpeg_flag "-DWITH_JPEG8=ON")
endif ()
endif()
orcaslicer_add_cmake_project(JPEG orcaslicer_add_cmake_project(JPEG
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63 URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
DEPENDS ${ZLIB_PKG}
CMAKE_ARGS CMAKE_ARGS
-DENABLE_SHARED=OFF -DENABLE_SHARED=OFF
-DENABLE_STATIC=ON -DENABLE_STATIC=ON
${jpeg_flag}
) )
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 366 B

@@ -36,6 +36,11 @@ uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex; uniform bool has_color_tex;
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
// instead, which is what sample_layer_color() does on the CPU side.
uniform bool tile_enabled;
uniform bool volume_mirrored; uniform bool volume_mirrored;
uniform mat4 view_model_matrix; uniform mat4 view_model_matrix;
@@ -45,6 +50,14 @@ uniform sampler2D height_tex;
uniform vec2 height_tex_texel; uniform vec2 height_tex_texel;
uniform float depth_mm; uniform float depth_mm;
uniform float tiling_scale; uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
// CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
uniform bool mirror_u;
uniform bool mirror_v;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions // Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform(). // instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect; uniform float tex_aspect;
@@ -54,6 +67,12 @@ uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step 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 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 uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
// The placement's rotation and its inverse, so this shader turns a world position into the texture
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
// behaviour, which is what an unset uniform gives.
uniform mat3 tex_rot; // world -> texture frame
uniform mat3 tex_rot_inv; // texture frame -> world
uniform bool use_vertex_uv; uniform bool use_vertex_uv;
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the // 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
// 140 variant. Identity when nothing is dragged. // 140 variant. Identity when nothing is dragged.
@@ -162,13 +181,29 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
} }
} }
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n) vec2 project_uv(vec3 p, vec3 n)
{ {
vec2 planar = projection_raw(p, n); vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
// After the rotation, so the rotation stays a rotation rather than becoming a shear. // After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect; r.y *= tex_aspect;
return r + uv_offset; return r + uv_offset;
@@ -237,9 +272,12 @@ void main()
// World millimetres throughout, like the bake - see the 140 variant. // World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
if (volume_mirrored) if (volume_mirrored)
triangle_normal = -triangle_normal; triangle_normal = -triangle_normal;
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
// the end of this shader needs it there.
vec3 tex_normal = tex_rot * triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's // Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour // *height* came from - including the parallax-marched one on the triplanar path - and the colour
@@ -270,14 +308,14 @@ void main()
} else if (weight > 0.0) { } else if (weight > 0.0) {
vec3 t, b; vec3 t, b;
vec2 units_per_mm; vec2 units_per_mm;
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm); projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the // 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). // 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); float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos); vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal); float v_dot_n = dot(view_dir, tex_normal);
vec2 uv = project_uv(tex_pos, triangle_normal); vec2 uv = project_uv(tex_pos, tex_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from // 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, // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -289,18 +327,18 @@ void main()
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half // How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common // a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface. // case of looking straight down at a surface.
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) { if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the // A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface // undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees. // by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n; float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS)); float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal); vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) { for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds; s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal); vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel); float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) { if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it // Crossed between the last two samples - interpolating the hit is what stops it
@@ -322,18 +360,23 @@ void main()
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r; float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y)); 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; vec2 tile = tile_mm();
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 * clamp(weight, 0.0, 1.0);
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v // One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
// component of the gradient carries the extra factor before being rotated back into t/b. // the uv-space gradient carries its own factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect); // Each component carries its own extent and its own mirror sign, both of which project_uv()
// applied on the way in; without the sign a mirrored layer would light as though it were not.
vec2 ms = mirror_sign();
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant. // ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm; vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
vec3 gradient = slope.x * t + slope.y * b; // t and b came out of the texture frame, so the gradient is in it too and has to come back
// to world before it perturbs the normal the lighting uses.
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
gradient -= triangle_normal * dot(triangle_normal, gradient); gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient); triangle_normal = normalize(triangle_normal - gradient);
} }
@@ -353,7 +396,9 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // 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. // the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb; vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) bool uv_covered = tile_enabled ||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)]; albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
} }
@@ -16,7 +16,16 @@ uniform bool volume_mirrored;
uniform int mode; uniform int mode;
uniform float checker_freq; uniform float checker_freq;
uniform float tiling_scale; uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
uniform bool mirror_u;
uniform bool mirror_v;
uniform vec3 tex_anchor; // the volume's origin in world space uniform vec3 tex_anchor; // the volume's origin in world space
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
// is what an unset uniform gives, leaves the old world-aligned behaviour.
uniform mat3 tex_rot; // world -> texture frame
uniform float rotation_rad; uniform float rotation_rad;
uniform vec2 uv_offset; uniform vec2 uv_offset;
uniform bool use_vertex_uv; uniform bool use_vertex_uv;
@@ -79,13 +88,30 @@ vec2 projection_raw(vec3 p, vec3 n)
return vec2(box_face_sign(n.z) * p.x, -p.y); return vec2(box_face_sign(n.z) * p.x, -p.y);
} }
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n) vec2 project_uv(vec3 p, vec3 n)
{ {
vec2 planar = projection_raw(p, n); vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
return r + uv_offset; return r + uv_offset;
} }
@@ -105,7 +131,7 @@ void main()
// World space anchored at the volume's origin, like the bake and the shaded preview. // World space anchored at the volume's origin, like the bake and the shaded preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
if (volume_mirrored) if (volume_mirrored)
triangle_normal = -triangle_normal; triangle_normal = -triangle_normal;
@@ -113,7 +139,7 @@ void main()
if (mode == 1) { if (mode == 1) {
base = heatmap(distortion); base = heatmap(distortion);
} else { } else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal); vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
vec2 c = floor(uv * checker_freq); vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0); float check = mod(c.x + c.y, 2.0);
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86); base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
@@ -95,6 +95,11 @@ uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex; uniform bool has_color_tex;
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
// instead, which is what sample_layer_color() does on the CPU side.
uniform bool tile_enabled;
uniform bool volume_mirrored; uniform bool volume_mirrored;
uniform mat4 view_model_matrix; uniform mat4 view_model_matrix;
@@ -104,6 +109,14 @@ uniform sampler2D height_tex;
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
uniform float depth_mm; uniform float depth_mm;
uniform float tiling_scale; uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
// CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
uniform bool mirror_u;
uniform bool mirror_v;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions // Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform(). // instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect; uniform float tex_aspect;
@@ -113,6 +126,12 @@ uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step 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 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 uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
// The placement's rotation and its inverse, so this shader turns a world position into the texture
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
// behaviour, which is what an unset uniform gives.
uniform mat3 tex_rot; // world -> texture frame
uniform mat3 tex_rot_inv; // texture frame -> world
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM) uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of // A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is // the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
@@ -228,13 +247,29 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
} }
} }
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n) vec2 project_uv(vec3 p, vec3 n)
{ {
vec2 planar = projection_raw(p, n); vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
// After the rotation, so the rotation stays a rotation rather than becoming a shear. // After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect; r.y *= tex_aspect;
return r + uv_offset; return r + uv_offset;
@@ -305,9 +340,12 @@ void main()
// printed part whatever the instance's scale or rotation, so the preview has to project from the // printed part whatever the instance's scale or rotation, so the preview has to project from the
// world position and perturb the world normal. // world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
if (volume_mirrored) if (volume_mirrored)
triangle_normal = -triangle_normal; triangle_normal = -triangle_normal;
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
// the end of this shader needs it there.
vec3 tex_normal = tex_rot * triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's // Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour // *height* came from - including the parallax-marched one on the triplanar path - and the colour
@@ -351,14 +389,14 @@ void main()
// closed-form uv here, unlike the LSCM case. // closed-form uv here, unlike the LSCM case.
vec3 t, b; vec3 t, b;
vec2 units_per_mm; vec2 units_per_mm;
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm); projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the // 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). // 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); float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos); vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal); float v_dot_n = dot(view_dir, tex_normal);
vec2 uv = project_uv(tex_pos, triangle_normal); vec2 uv = project_uv(tex_pos, tex_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from // 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, // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -370,18 +408,18 @@ void main()
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half // How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common // a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface. // case of looking straight down at a surface.
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) { if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the // A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface // undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees. // by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n; float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS)); float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal); vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) { for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds; s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal); vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel); float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) { if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it // Crossed between the last two samples - interpolating the hit is what stops it
@@ -405,23 +443,28 @@ void main()
// Central difference, per uv unit (not per texel). // Central difference, per uv unit (not per texel).
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y)); vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space // uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
// gradient into a real surface slope. // turns the uv-space gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; vec2 tile = tile_mm();
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 * clamp(weight, 0.0, 1.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the // uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame. // gradient back into the axes' frame.
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v // One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
// component of the gradient carries the extra factor before being rotated back into t/b. // the uv-space gradient carries its own factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect); // Each component carries its own extent and its own mirror sign, both of which project_uv()
// applied on the way in; without the sign a mirrored layer would light as though it were not.
vec2 ms = mirror_sign();
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a // ...and back out of raw-projection units into millimetres of travel along t / b, which is a
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation, // no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
// because units_per_mm is expressed in the t/b frame rather than in uv. // because units_per_mm is expressed in the t/b frame rather than in uv.
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm; vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
vec3 gradient = slope.x * t + slope.y * b; // t and b came out of the texture frame, so the gradient is in it too and has to come back
// to world before it perturbs the normal the lighting uses.
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
gradient -= triangle_normal * dot(triangle_normal, gradient); gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient); triangle_normal = normalize(triangle_normal - gradient);
} }
@@ -441,7 +484,9 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // 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. // the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb; vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) bool uv_covered = tile_enabled ||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)]; albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
} }
@@ -25,7 +25,16 @@ uniform bool volume_mirrored;
uniform int mode; // 0 checker, 1 distortion uniform int mode; // 0 checker, 1 distortion
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile) uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
uniform float tiling_scale; uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
uniform bool mirror_u;
uniform bool mirror_v;
uniform vec3 tex_anchor; // the volume's origin in world space uniform vec3 tex_anchor; // the volume's origin in world space
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
// is what an unset uniform gives, leaves the old world-aligned behaviour.
uniform mat3 tex_rot; // world -> texture frame
uniform float rotation_rad; uniform float rotation_rad;
uniform vec2 uv_offset; uniform vec2 uv_offset;
uniform bool use_vertex_uv; uniform bool use_vertex_uv;
@@ -90,13 +99,30 @@ vec2 projection_raw(vec3 p, vec3 n)
return vec2(box_face_sign(n.z) * p.x, -p.y); return vec2(box_face_sign(n.z) * p.x, -p.y);
} }
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n) vec2 project_uv(vec3 p, vec3 n)
{ {
vec2 planar = projection_raw(p, n); vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs); vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
return r + uv_offset; return r + uv_offset;
} }
@@ -117,7 +143,7 @@ void main()
// World space anchored at the volume's origin, like the bake and the shaded preview. // World space anchored at the volume's origin, like the bake and the shaded preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
if (volume_mirrored) if (volume_mirrored)
triangle_normal = -triangle_normal; triangle_normal = -triangle_normal;
@@ -125,7 +151,7 @@ void main()
if (mode == 1) { if (mode == 1) {
base = heatmap(distortion); base = heatmap(distortion);
} else { } else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal); vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
vec2 c = floor(uv * checker_freq); vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0); float check = mod(c.x + c.y, 2.0);
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance. // Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
@@ -18,7 +18,7 @@
<script src="../js/plugin-config-ui.js"></script> <script src="../js/plugin-config-ui.js"></script>
<script src="./index.js"></script> <script src="./index.js"></script>
<script src="./plugin-sort.js"></script> <script src="./plugin-sort.js"></script>
<script src="../../js/fuzzy-search.js"></script> <script src="../js/fuzzy-search.js"></script>
<script src="./plugin-search.js"></script> <script src="./plugin-search.js"></script>
</head> </head>
<body onLoad="OnInit()"> <body onLoad="OnInit()">
+2 -6
View File
@@ -156,16 +156,12 @@ function SetCloudMode( bOn )
m_CloudMode=bOn; m_CloudMode=bOn;
$("body").toggleClass("cloud-mode", bOn); $("body").toggleClass("cloud-mode", bOn);
if( bOn )
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
}
} }
function GotoCloud() function GotoCloud()
{ {
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
SetCloudMode(true); SetCloudMode(true);
SendSimpleCommand("homepage_open_cloud"); SendSimpleCommand("homepage_open_cloud");
} }
@@ -336,10 +336,10 @@ modules:
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
dest: external-packages/PNG dest: external-packages/PNG
# libjpeg-turbo 3.2.0 # libjpeg-turbo 3.0.1
- type: file - type: file
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63 sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
dest: external-packages/JPEG dest: external-packages/JPEG
# Freetype 2.12.1 # Freetype 2.12.1
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the # Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp). grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
# Work-around https://github.com/bambulab/BambuStudio/issues/3440 # Work-around https://github.com/bambulab/BambuStudio/issues/3440
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues # Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
+15 -21
View File
@@ -163,7 +163,6 @@ using json = nlohmann::json;
#include <X11/Xlib.h> #include <X11/Xlib.h>
#endif #endif
#include <unistd.h> #include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif #endif
#ifdef SLIC3R_GUI #ifdef SLIC3R_GUI
@@ -1157,10 +1156,8 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it // opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it // worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup // unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path applies it only where it // Wizard / login / store. The default path still applies it on
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really // XWayland sessions as a conservative fallback for older WebKit.
// runs through XWayland), and only when the linked WebKitGTK predates
// the fix (< 2.42), as a conservative fallback for older WebKit.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
{ {
const char* gdk_backend = ::getenv("GDK_BACKEND"); const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1168,14 +1165,6 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list). // honours the first backend in the comma-separated list).
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11"); const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
// Force the shared-memory transport, which keeps compositing and threaded
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
}
if (x11_opt_in) { if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) ===== // ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds."; BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -1215,18 +1204,23 @@ int CLI::run(int argc, char **argv)
if (wayland_env && *wayland_env) { if (wayland_env && *wayland_env) {
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend."; BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
::setenv("GDK_BACKEND", "x11", true); ::setenv("GDK_BACKEND", "x11", true);
// GTK will use X11 in a Wayland session here, which is the
// one default-path case that really runs through XWayland.
// WebKit2GTK accelerated compositing could fail silently
// there on releases before 2.42, so force it off. Disabling
// it also turns off threaded scrolling, hence the gate.
if (WebView::NeedsCompositingWorkaround())
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
} }
} }
#endif #endif
// WebKit2GTK compositing can fail under XWayland on older
// WebKit releases. Disable it only when both DISPLAY and
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
// use). On pure X11 or native Wayland, compositing is left
// enabled so WebKit retains HW acceleration.
{
const char* display_env_wk = ::getenv("DISPLAY");
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
// XInitThreads is needed before GStreamer may use Xlib. On // XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink // native Wayland without DISPLAY, GStreamer uses waylandsink
// (no Xlib involved), so the call is skipped. // (no Xlib involved), so the call is skipped.
@@ -429,8 +429,7 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export MESA_LOADER_DRIVER_OVERRIDE=zink export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink export GALLIUM_DRIVER=zink
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself, export WEBKIT_DISABLE_DMABUF_RENDERER=1
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
fi fi
fi fi
fi fi
+95 -26
View File
@@ -1,6 +1,7 @@
#include "TextureDisplacement.hpp" #include "TextureDisplacement.hpp"
#include <Eigen/Core> #include <Eigen/Core>
#include <Eigen/SVD>
#include <Eigen/Geometry> #include <Eigen/Geometry>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -722,7 +723,10 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
const DecodedHeightTexture &tex = decode_height_texture(layer); const DecodedHeightTexture &tex = decode_height_texture(layer);
if (tex.width <= 0) if (tex.width <= 0)
continue; continue;
const float texel = layer.tiling_scale / float(tex.width); // Texels are square in both axes (the aspect correction is what keeps them so), so the finer
// of the two tile extents sets the finest texel the refinement has to resolve.
const Vec2f tile = layer_tile_mm(layer);
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
if (out.texel_mm <= 0.f || texel < out.texel_mm) { if (out.texel_mm <= 0.f || texel < out.texel_mm) {
out.texel_mm = texel; out.texel_mm = texel;
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge; out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
@@ -2164,7 +2168,7 @@ namespace {
// of them was most of the per-sample arithmetic. // of them was most of the per-sample arithmetic.
struct UVTransform struct UVTransform
{ {
float scale, cs, sn, aspect; float inv_u, inv_v, cs, sn, aspect;
Vec2f offset; Vec2f offset;
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the // `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
@@ -2172,7 +2176,18 @@ struct UVTransform
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it. // rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false) UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
{ {
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale); // The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
// nothing to do per sample beyond carrying the sign here. A normalized uv has no millimetre
// tiling to divide by, but a mirror is still a mirror, so the sign applies either way.
const Vec2f sign = layer_mirror_sign(layer);
if (normalized_uv) {
inv_u = sign.x();
inv_v = sign.y();
} else {
const Vec2f tile = layer_tile_mm(layer);
inv_u = sign.x() / tile.x();
inv_v = sign.y() / tile.y();
}
const float rad = layer.rotation_deg * float(M_PI) / 180.f; const float rad = layer.rotation_deg * float(M_PI) / 180.f;
cs = std::cos(rad); cs = std::cos(rad);
sn = std::sin(rad); sn = std::sin(rad);
@@ -2181,9 +2196,11 @@ struct UVTransform
} }
Vec2f operator()(const Vec2f &planar) const Vec2f operator()(const Vec2f &planar) const
{ {
const Vec2f scaled = planar * scale; // See apply_uv_transform() for why both the tile extents and the aspect correction follow
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs); // the rotation rather than preceding it.
// See apply_uv_transform() for why the aspect correction follows the rotation. Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
rotated.x() *= inv_u;
rotated.y() *= inv_v;
if (aspect > 0.f && aspect != 1.f) if (aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect; rotated.y() *= aspect;
return rotated + offset; return rotated + offset;
@@ -2203,26 +2220,43 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv) Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
{ {
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
// see the header. UVTransform does the same, and the two must stay in step.
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const Vec2f scaled = planar * scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f; const float rad = layer.rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad); const float cs = std::cos(rad);
const float sn = std::sin(rad); const float sn = std::sin(rad);
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs); Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
// coordinate by different amounts would be a shear, so the rotation has to come first and the
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
// R being linear - which is why this is not a change in behaviour for any existing layer.
//
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
// it mirrors along the tile's axes rather than the world's, and because the offset is what
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
// writes alongside it, not of anything here.
// A uv that is already normalized has no millimetre scale to divide by - see the header - but it
// can still be mirrored, so the sign is applied in both cases and only the extents are skipped.
// UVTransform does the same, and the two must stay in step.
const Vec2f sign = layer_mirror_sign(layer);
if (normalized_uv) {
rotated.x() *= sign.x();
rotated.y() *= sign.y();
} else {
const Vec2f tile = layer_tile_mm(layer);
rotated.x() *= sign.x() / tile.x();
rotated.y() *= sign.y() / tile.y();
}
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its // Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out // proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is // as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so // is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
// dividing v by that extent is the same as multiplying it by width / height. A square texture has // it by width / height. A square texture has aspect 1 and is untouched, which is why this
// aspect 1 and is untouched, which is why this changes nothing for the shipped library. // changes nothing for the shipped library.
// //
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a // Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of // already-rotated coordinate before turning it would be a shear.
// turning it.
if (!normalized_uv && aspect > 0.f && aspect != 1.f) if (!normalized_uv && aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect; rotated.y() *= aspect;
@@ -3566,13 +3600,36 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ(); (an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
} }
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world)
{
// Separated out by polar decomposition rather than by assuming the linear part is a clean
// rotation times a scale: a sheared or mirrored placement is legal here. A reflection is left
// behind with the scale on purpose, so a mirrored instance still makes the bake frame's
// determinant negative and the bake still knows to flip the normals it derives.
const Matrix3d linear = volume_to_world.linear();
Eigen::JacobiSVD<Matrix3d> svd(linear, Eigen::ComputeFullU | Eigen::ComputeFullV);
Matrix3d rotation = svd.matrixU() * svd.matrixV().transpose();
if (rotation.determinant() < 0.0) {
Matrix3d u = svd.matrixU();
u.col(2) *= -1.0;
rotation = u * svd.matrixV().transpose();
}
return rotation.transpose(); // world -> texture frame
}
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world) Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
{ {
// World orientation and scale, but the origin moved to where the volume's own origin sits: the // The model's own orientation, world scale, and the origin moved to where the volume's origin
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp // sits. So the texture rides with the model however it is moved *and* however it is turned,
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale` // while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a
// printed millimetres whatever the instance's scale. // single untiled stamp starts on the model rather than at the plate's corner.
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world; //
// Orientation used to come from the world too, which meant turning a part on the plate slid the
// decoration across its surface. Turning a part is a print-time decision - clearance, supports,
// warping - and it should not be a design one.
Transform3d unrotate = Transform3d::Identity();
unrotate.linear() = texture_displacement_frame_rotation(volume_to_world);
return unrotate * Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
} }
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume) Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
@@ -5783,7 +5840,13 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
j["depth_mm"] = l.depth_mm; j["depth_mm"] = l.depth_mm;
j["tiling_scale"] = l.tiling_scale; j["tiling_scale"] = l.tiling_scale;
j["rotation_deg"] = l.rotation_deg; // The v extent and the mirroring. Written unconditionally, including the <= 0 "same as u"
// sentinel, so a project says plainly that the tile was square rather than leaving a reader
// to infer it from a missing key.
j["tiling_scale_v"] = l.tiling_scale_v;
j["mirror_u"] = l.mirror_u;
j["mirror_v"] = l.mirror_v;
j["rotation_deg"] = l.rotation_deg;
j["offset"] = vec2_to_json(l.offset); j["offset"] = vec2_to_json(l.offset);
j["invert"] = l.invert; j["invert"] = l.invert;
j["midlevel"] = l.midlevel; j["midlevel"] = l.midlevel;
@@ -5891,6 +5954,12 @@ bool texture_displacement_layers_from_json(const std::string
read(j, "depth_mm", l.depth_mm); read(j, "depth_mm", l.depth_mm);
read(j, "tiling_scale", l.tiling_scale); read(j, "tiling_scale", l.tiling_scale);
// Absent in a project written before the tile could be rectangular or mirrored, and
// then each keeps its default: -1 for the v extent, which layer_tile_mm() reads as
// "square", and false for the mirrors. So an older project loads as what it was.
read(j, "tiling_scale_v", l.tiling_scale_v);
read(j, "mirror_u", l.mirror_u);
read(j, "mirror_v", l.mirror_v);
read(j, "rotation_deg", l.rotation_deg); read(j, "rotation_deg", l.rotation_deg);
if (const auto o = j.find("offset"); o != j.end()) if (const auto o = j.find("offset"); o != j.end())
l.offset = vec2_from_json(*o, l.offset); l.offset = vec2_from_json(*o, l.offset);
+47 -8
View File
@@ -225,7 +225,17 @@ struct TextureDisplacementLayer
std::shared_ptr<std::vector<unsigned char>> image_data; std::shared_ptr<std::vector<unsigned char>> image_data;
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
float tiling_scale = 12.5f; // size of one texture tile, in mm float tiling_scale = 12.5f; // size of one texture tile along u, in mm
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
// the default and what a layer created before the tile could be non-square meant - so an older
// undo snapshot still comes back as the square tile it was.
float tiling_scale_v = -1.f;
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
// layer written before this meant.
bool mirror_u = false;
bool mirror_v = false;
float rotation_deg = 0.f; float rotation_deg = 0.f;
Vec2f offset = Vec2f::Zero(); Vec2f offset = Vec2f::Zero();
bool invert = false; bool invert = false;
@@ -362,7 +372,8 @@ struct TextureDisplacementLayer
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands, 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, 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, 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, tiling_scale_v,
mirror_u, mirror_v);
} }
template<class Archive> void load(Archive &ar) template<class Archive> void load(Archive &ar)
{ {
@@ -374,7 +385,7 @@ struct TextureDisplacementLayer
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel, 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, 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, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled); view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end()); image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int); tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int); projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
@@ -383,6 +394,25 @@ struct TextureDisplacementLayer
}; };
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
// v extent means.
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
{
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
}
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
// and letting one of them go negative would mean every one of those places had to remember to take
// the magnitude.
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
{
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
}
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next // Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
// to texture_displacement_layers and consumed by build_texture_displacement(). // to texture_displacement_layers and consumed by build_texture_displacement().
struct TextureDisplacementOptions struct TextureDisplacementOptions
@@ -668,14 +698,19 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// projection method, without going through project_texture_displacement_uv()'s own dispatch // projection method, without going through project_texture_displacement_uv()'s own dispatch
// (which only knows how to compute the *analytic* methods from a single vertex + normal). // (which only knows how to compute the *analytic* methods from a single vertex + normal).
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not // `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is // squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the // keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
// square case and leaves the coordinate exactly as it always was. // always was.
//
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
// rectangle on the surface instead of a sheared parallelogram. The layer's mirroring is a sign on
// that same division.
// //
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the // `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm // model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tile extents (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for // per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation and offset are left. // the image's proportions) applies, and only the rotation, the mirroring and the offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f, Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false); bool normalized_uv = false);
@@ -1047,6 +1082,10 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume); Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation // The frame the bake and the previews project the texture in: `volume_to_world` with its translation
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement(). // removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
// The rotation that carries a world direction into the texture's frame, i.e. the inverse of the
// placement's own rotation. Exposed because the preview shaders rebuild the frame themselves from a
// world position and have to turn it the same way the bake does, or the two disagree.
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world);
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world); Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along. // Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
-5
View File
@@ -689,8 +689,6 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostUi.hpp plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp plugin/host/PluginPages.hpp
plugin/host/PluginResourceTextures.cpp
plugin/host/PluginResourceTextures.hpp
plugin/CloudPluginService.cpp plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp plugin/PluginFsUtils.cpp
@@ -714,9 +712,6 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/pages/PagesPluginCapability.hpp plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+72 -260
View File
@@ -85,96 +85,6 @@ static std::string float_to_string_with_precision(float value, int precision = 3
return stream.str(); return stream.str();
} }
static std::string colour_to_ams_string(const wxColour& color)
{
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
return col_buf;
}
static wxColour mix_colour(const wxColour& left, const wxColour& right, double ratio)
{
ratio = std::max(0.0, std::min(1.0, ratio));
auto mix_channel = [ratio](unsigned char l, unsigned char r) {
return static_cast<unsigned char>(std::round(l + (r - l) * ratio));
};
return wxColour(mix_channel(left.Red(), right.Red()),
mix_channel(left.Green(), right.Green()),
mix_channel(left.Blue(), right.Blue()),
mix_channel(left.Alpha(), right.Alpha()));
}
static std::vector<ColorPickerPopup::ColorItem> collect_ams_color_items(DevFilaSystem* fila_system)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (!fila_system) return items;
for (const auto& ams_pair : fila_system->GetAmsList()) {
DevAms* ams = ams_pair.second;
if (!ams) continue;
for (const auto& tray_pair : ams->GetTrays()) {
DevAmsTray* tray = tray_pair.second;
if (!tray || !tray->is_tray_info_ready()) continue;
ColorPickerPopup::ColorItem item;
for (const std::string& col : tray->cols) {
item.colors.emplace_back(DevAmsTray::decode_color(col));
}
if (item.colors.empty() && !tray->color.empty()) {
item.colors.emplace_back(DevAmsTray::decode_color(tray->color));
}
if (item.colors.empty()) continue;
item.ctype = item.colors.size() > 1 ? tray->ctype : 2;
items.emplace_back(std::move(item));
}
}
return items;
}
static std::vector<ColorPickerPopup::ColorItem> collect_preset_color_items(const std::string& filament_id)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (filament_id.empty()) return items;
auto* clr_query = GUI::wxGetApp().get_filament_color_code_query();
if (!clr_query) return items;
auto* agent = wxGetApp().getAgent();
const std::string catalog_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id;
FilamentColorCodes* color_codes = clr_query->GetFilaInfoMap(wxString::FromUTF8(catalog_id));
if (!color_codes || !color_codes->GetFilamentColor2CodeMap()) return items;
std::set<std::string> seen;
for (const auto& color_pair : *color_codes->GetFilamentColor2CodeMap()) {
const FilamentColor& fila_color = color_pair.first;
FilamentColorCode* color_code = color_pair.second;
if (!color_code || fila_color.GetColors().empty()) continue;
ColorPickerPopup::ColorItem item;
item.ctype = fila_color.ColorCount() < 2 ? 2 :
(fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR ? 0 : 1);
item.name = color_code->GetFilaColorName();
std::string key = std::to_string(item.ctype);
for (const wxColour& color : fila_color.GetColors()) {
item.colors.emplace_back(color);
char rgba[9];
std::snprintf(rgba, sizeof(rgba), "%02X%02X%02X%02X",
static_cast<unsigned>(color.Red()), static_cast<unsigned>(color.Green()),
static_cast<unsigned>(color.Blue()), static_cast<unsigned>(color.Alpha()));
key += "|" + std::string(rgba);
}
if (item.colors.empty() || !seen.insert(key).second) continue;
items.emplace_back(std::move(item));
}
return items;
}
AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id) AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id)
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) : DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_color_picker_popup(ColorPickerPopup(this)) , m_color_picker_popup(ColorPickerPopup(this))
@@ -830,19 +740,8 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
nozzle_temp_min.ToLong(&nozzle_temp_min_int); nozzle_temp_min.ToLong(&nozzle_temp_min_int);
nozzle_temp_max.ToLong(&nozzle_temp_max_int); nozzle_temp_max.ToLong(&nozzle_temp_max_int);
wxColour color = m_clr_picker->m_colour; wxColour color = m_clr_picker->m_colour;
std::string tray_color = colour_to_ams_string(color); char col_buf[10];
std::vector<std::string> tray_colors; sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
int tray_ctype = 2;
// Only firmware advertising fun2[23] receives the manual multi-color fields.
if (obj->is_support_filament_manual_multi_color) {
for (const wxColour& selected_color : m_clr_picker->m_cols) {
tray_colors.emplace_back(colour_to_ams_string(selected_color));
}
if (tray_colors.empty()) {
tray_colors.emplace_back(tray_color);
}
tray_ctype = tray_colors.size() > 1 ? m_clr_picker->ctype : 2;
}
if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) { if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) {
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id"; BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
@@ -861,33 +760,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
<< ", tray_info_idx (filament_id) = " << ams_filament_id << ", tray_info_idx (filament_id) = " << ams_filament_id
<< ", setting_id = " << ams_setting_id << ", setting_id = " << ams_setting_id
<< ", tray_type = " << m_filament_type << ", tray_type = " << m_filament_type
<< ", tray_color = " << tray_color << ", tray_color = " << col_buf
<< ", nozzle_temp_min = " << nozzle_temp_min_int << ", nozzle_temp_min = " << nozzle_temp_min_int
<< ", nozzle_temp_max = " << nozzle_temp_max_int; << ", nozzle_temp_max = " << nozzle_temp_max_int;
// set filament // set filament
if (m_is_third) { if (m_is_third) {
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, tray_color, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int, obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int);
tray_colors, tray_ctype);
// Refresh the edited tray immediately while its MQTT acknowledgement is pending.
DevAmsTray* tray = nullptr;
if (is_virtual_tray()) {
for (auto& virtual_tray : obj->vt_slot) {
if (virtual_tray.id == std::to_string(ams_id)) {
tray = &virtual_tray;
break;
}
}
} else {
tray = obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
}
if (tray) {
tray->color = tray_color;
tray->cols = tray_colors;
tray->ctype = tray_ctype;
tray->set_hold_count();
}
} }
//reset param //reset param
@@ -1044,15 +923,8 @@ void AMSMaterialsSetting::set_ctype(int ctype)
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event) void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
{ {
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours(); unsigned int color_num = event.GetInt();
if (colors.empty()) { set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
unsigned int color_num = event.GetInt();
colors.emplace_back(color_num >> 24 & 0xFF, color_num >> 16 & 0xFF, color_num >> 8 & 0xFF, color_num & 0xFF);
}
set_ctype(m_color_picker_popup.get_selected_ctype());
set_color(colors.front());
set_colors(colors);
} }
void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event) void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
@@ -1067,12 +939,14 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
} }
} }
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get())); std::vector<wxColour> ams_colors;
m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id)); obj->GetFilaSystem()->CollectAmsColors(ams_colors);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour, m_clr_picker->m_cols, m_clr_picker->ctype);
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0)); wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65)); wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
m_color_picker_popup.Position(popup_pos, wxSize(0, 0)); m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
m_color_picker_popup.set_ams_colours(ams_colors);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
m_color_picker_popup.Popup(); m_color_picker_popup.Popup();
} }
@@ -1861,46 +1735,7 @@ void ColorPicker::doRender(wxDC& dc)
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2; auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
if (m_selected) radius -= FromDIP(1); if (m_selected) radius -= FromDIP(1);
if (m_show_full && m_cols.size() > 1) { if (alpha == 0) {
if (ctype == 0) {
const double center_x = size.x / 2.0;
const double center_y = size.y / 2.0;
const double draw_radius = radius;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int x = 0; x < size.x; ++x) {
const double dx = x + 0.5 - center_x;
if (std::abs(dx) > draw_radius) continue;
const double half_height = std::sqrt(std::max(0.0, draw_radius * draw_radius - dx * dx));
const int top = std::max(0, static_cast<int>(std::ceil(center_y - half_height)));
const int bottom = std::min(size.y - 1, static_cast<int>(std::floor(center_y + half_height)));
const double pos = size.x > 1 ? static_cast<double>(x) / (size.x - 1) : 0.0;
const double scaled = pos * (m_cols.size() - 1);
const size_t idx = std::min(static_cast<size_t>(scaled), m_cols.size() - 2);
const double ratio = scaled - idx;
if (top <= bottom) {
dc.SetBrush(wxBrush(mix_colour(m_cols[idx], m_cols[idx + 1], ratio)));
dc.DrawRectangle(x, top, 1, bottom - top + 1);
}
}
}
else {
float ev_angle = 360.0 / m_cols.size();
const float overlap = 2.0f;
wxPoint center(size.x / 2, size.y / 2);
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_cols.front()));
dc.DrawCircle(center.x, center.y, radius);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
float startAngle = 270.0f + i * ev_angle;
float endAngle = startAngle + ev_angle;
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle - overlap, endAngle + overlap);
}
}
}
else if (alpha == 0) {
wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize(); wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize();
int center_x = (size.x - bmp_size.x) / 2; int center_x = (size.x - bmp_size.x) / 2;
int center_y = (size.y - bmp_size.y) / 2; int center_y = (size.y - bmp_size.y) / 2;
@@ -1942,6 +1777,52 @@ void ColorPicker::doRender(wxDC& dc)
dc.SetPen(wxPen(wxColour("#6B6B6B"))); dc.SetPen(wxPen(wxColour("#6B6B6B")));
dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(size.x / 2, size.y / 2, radius); dc.DrawCircle(size.x / 2, size.y / 2, radius);
if (m_cols.size() > 1) {
if (ctype == 0) {
int left = FromDIP(0);
float total_width = size.x;
int gwidth = std::round(total_width / (m_cols.size() - 1));
for (int i = 0; i < m_cols.size() - 1; i++) {
if ((left + gwidth) > (size.x)) {
gwidth = size.x - left;
}
auto rect = wxRect(left, 0, gwidth, size.y);
dc.GradientFillLinear(rect, m_cols[i], m_cols[i + 1], wxEAST);
left += gwidth;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
else {
float ev_angle = 360.0 / m_cols.size();
float startAngle = 270.0;
float endAngle = 270.0;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
endAngle += ev_angle;
endAngle = endAngle > 360.0 ? endAngle - 360.0 : endAngle;
wxPoint center(size.x / 2, size.y / 2);
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle, endAngle);
startAngle += ev_angle;
startAngle = startAngle > 360.0 ? startAngle - 360.0 : startAngle;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
}
} }
if (m_is_empty) { if (m_is_empty) {
@@ -2010,21 +1891,19 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
//other //other
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0); wxFlexGridSizer* fg_sizer;
m_other_fg_sizer->SetFlexibleDirection(wxBOTH); fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); fg_sizer->SetFlexibleDirection(wxBOTH);
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
for (wxColour col : m_def_colors) { for (wxColour col : m_def_colors) {
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize); auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col); cp->set_color(col);
cp->set_colors({col});
cp->ctype = 2;
cp->set_selected(false); cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238))); cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp); m_color_pickers.push_back(cp);
m_default_color_pickers.push_back(cp); fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) { cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour); set_def_colour(cp->m_colour);
@@ -2091,7 +1970,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_other_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10)); m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16)); m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10)); m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
@@ -2102,7 +1981,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_def_color_box->Fit(); m_def_color_box->Fit();
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10); m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
SetSizerAndFit(m_sizer_main); SetSizer(m_sizer_main);
Layout(); Layout();
Fit(); Fit();
@@ -2140,15 +2019,13 @@ void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
wxPostEvent(GetParent(), evt); wxPostEvent(GetParent(), evt);
} }
void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams) void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
{ {
m_ams_fg_sizer->Clear(false);
if (m_ams_color_pickers.size() > 0) { if (m_ams_color_pickers.size() > 0) {
for (ColorPicker* col_pick:m_ams_color_pickers) { for (ColorPicker* col_pick:m_ams_color_pickers) {
std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick); std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) { if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy(); col_pick->Destroy();
m_color_pickers.erase(iter); m_color_pickers.erase(iter);
} }
@@ -2158,22 +2035,17 @@ void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
} }
m_ams_color_items = ams; m_ams_colors = ams;
for (const ColorItem& item : m_ams_color_items) { for (wxColour col : m_ams_colors) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize); auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(item.colors.front()); cp->set_color(col);
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false); cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238))); cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp); m_color_pickers.push_back(cp);
m_ams_color_pickers.push_back(cp); m_ams_color_pickers.push_back(cp);
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3)); m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) { cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype); set_def_colour(cp->m_colour);
wxCommandEvent evt(EVT_SELECTED_COLOR); wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff); unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
@@ -2186,68 +2058,9 @@ void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
Fit(); Fit();
} }
void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors) void ColorPickerPopup::set_def_colour(wxColour col)
{
m_other_fg_sizer->Clear(false);
if (!m_preset_color_pickers.empty()) {
for (ColorPicker* col_pick : m_preset_color_pickers) {
auto iter = std::find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy();
m_color_pickers.erase(iter);
}
}
m_preset_color_pickers.clear();
}
for (const ColorItem& item : preset_colors) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
if (!item.name.empty()) {
cp->SetToolTip(item.name);
}
m_color_pickers.push_back(cp);
m_preset_color_pickers.push_back(cp);
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
evt.SetInt(g_col);
wxPostEvent(GetParent(), evt);
});
}
const bool show_default_colours = m_preset_color_pickers.empty();
for (ColorPicker* cp : m_default_color_pickers) {
cp->Show(show_default_colours);
}
for (ColorPicker* cp : (show_default_colours ? m_default_color_pickers : m_preset_color_pickers)) {
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
}
m_other_fg_sizer->Layout();
Layout();
Fit();
}
void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols, int ctype)
{ {
m_def_col = col; m_def_col = col;
if (cols.empty()) {
cols.push_back(col);
}
m_def_cols = cols;
m_def_ctype = m_def_cols.size() > 1 ? ctype : 2;
for (ColorPicker* cp : m_color_pickers) { for (ColorPicker* cp : m_color_pickers) {
if (cp->m_selected) { if (cp->m_selected) {
@@ -2256,8 +2069,7 @@ void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols,
} }
for (ColorPicker* cp : m_color_pickers) { for (ColorPicker* cp : m_color_pickers) {
if (!cp->IsShown()) continue; if (cp->m_colour == m_def_col) {
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
cp->set_selected(true); cp->set_selected(true);
break; break;
} }
+4 -19
View File
@@ -82,22 +82,14 @@ public:
class ColorPickerPopup : public PopupWindow class ColorPickerPopup : public PopupWindow
{ {
public: public:
struct ColorItem
{
std::vector<wxColour> colors;
int ctype = 2;
wxString name;
};
ScalableBitmap m_ts_bitmap_custom; ScalableBitmap m_ts_bitmap_custom;
wxStaticBitmap* m_ts_stbitmap_custom; wxStaticBitmap* m_ts_stbitmap_custom;
StaticBox* m_custom_cp; StaticBox* m_custom_cp;
StaticBox* m_def_color_box; StaticBox* m_def_color_box;
wxFlexGridSizer* m_ams_fg_sizer; wxFlexGridSizer* m_ams_fg_sizer;
wxFlexGridSizer* m_other_fg_sizer;
wxColour m_def_col; wxColour m_def_col;
std::vector<wxColour> m_def_colors; std::vector<wxColour> m_def_colors;
std::vector<ColorItem> m_ams_color_items; std::vector<wxColour> m_ams_colors;
std::vector<ColorPicker*> m_color_pickers; std::vector<ColorPicker*> m_color_pickers;
std::vector<ColorPicker*> m_ams_color_pickers; std::vector<ColorPicker*> m_ams_color_pickers;
@@ -105,20 +97,13 @@ public:
ColorPickerPopup(wxWindow* parent); ColorPickerPopup(wxWindow* parent);
~ColorPickerPopup() {}; ~ColorPickerPopup() {};
void on_custom_clr_picker(wxMouseEvent& event); void on_custom_clr_picker(wxMouseEvent& event);
void set_ams_colours(const std::vector<ColorItem>& ams); void set_ams_colours(std::vector<wxColour> ams);
void set_preset_colours(const std::vector<ColorItem>& preset_colors); void set_def_colour(wxColour col);
void set_def_colour(wxColour col, std::vector<wxColour> cols = {}, int ctype = 2);
const std::vector<wxColour>& get_selected_colours() const { return m_def_cols; }
int get_selected_ctype() const { return m_def_ctype; }
void paintEvent(wxPaintEvent& evt); void paintEvent(wxPaintEvent& evt);
virtual void OnDismiss() wxOVERRIDE; virtual void OnDismiss() wxOVERRIDE;
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE; virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
private: public:
std::vector<ColorPicker*> m_default_color_pickers;
std::vector<ColorPicker*> m_preset_color_pickers;
std::vector<wxColour> m_def_cols;
int m_def_ctype = 2;
}; };
+1 -3
View File
@@ -747,9 +747,7 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
// Orca: prevent resetting under gcode viewer mode // Orca: prevent resetting under gcode viewer mode
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) { if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
// Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply() const auto plater = GUI::wxGetApp().mainframe->m_plater;
// (loading presets, config or input files, e.g. from the CLI) therefore dereferences a null `mainframe` and crashes.
const auto plater = GUI::wxGetApp().plater();
if (plater && plater->only_gcode_mode()) { if (plater && plater->only_gcode_mode()) {
invalidated = PrintBase::APPLY_STATUS_UNCHANGED; invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
} }
+1 -7
View File
@@ -1798,8 +1798,7 @@ int MachineObject::command_ams_calibrate(int ams_id)
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors, int tray_ctype)
{ {
int tag_tray_id = 0; int tag_tray_id = 0;
int tag_ams_id = ams_id; int tag_ams_id = ams_id;
@@ -1828,10 +1827,6 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
j["print"]["nozzle_temp_min"] = nozzle_temp_min; j["print"]["nozzle_temp_min"] = nozzle_temp_min;
j["print"]["nozzle_temp_max"] = nozzle_temp_max; j["print"]["nozzle_temp_max"] = nozzle_temp_max;
j["print"]["tray_type"] = tray_type; j["print"]["tray_type"] = tray_type;
if (!tray_colors.empty()) {
j["print"]["cols"] = tray_colors;
j["print"]["ctype"] = tray_ctype;
}
return this->publish_json(j); return this->publish_json(j);
} }
@@ -5483,7 +5478,6 @@ void MachineObject::parse_new_info(json print)
is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1); is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1);
is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1); is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1; is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
is_support_filament_manual_multi_color = (get_flag_bits_no_border(fun2, 23) == 1);
if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) { if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) {
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1; m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
+1 -2
View File
@@ -674,7 +674,6 @@ public:
bool is_support_remote_dry = false; bool is_support_remote_dry = false;
bool is_support_check_track_switch_match_slice_printer{false}; bool is_support_check_track_switch_match_slice_printer{false};
bool is_support_pa_mode{false}; bool is_support_pa_mode{false};
bool is_support_filament_manual_multi_color{false};
std::optional<bool> m_firmware_support_print_tpu_left; std::optional<bool> m_firmware_support_print_tpu_left;
bool installed_upgrade_kit{false}; bool installed_upgrade_kit{false};
@@ -805,7 +804,7 @@ public:
int command_ams_switch_filament(bool switch_filament); int command_ams_switch_filament(bool switch_filament);
int command_ams_air_print_detect(bool air_print_detect); int command_ams_air_print_detect(bool air_print_detect);
int command_ams_calibrate(int ams_id); int command_ams_calibrate(int ams_id);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors = {}, int tray_ctype = 2); int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max);
int command_ams_select_tray(std::string tray_id); int command_ams_select_tray(std::string tray_id);
int command_ams_refresh_rfid(int ams_id, int slot_id); int command_ams_refresh_rfid(int ams_id, int slot_id);
int command_ams_control(std::string action); int command_ams_control(std::string action);
+1 -1
View File
@@ -96,7 +96,7 @@ void FilamentColorCodeQuery::LoadFromLocal()
const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>(); const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>();
for (const auto& color_str : fila_color_strs) { for (const auto& color_str : fila_color_strs) {
if (color_str.size() > 3) /* Skip the value like "#0"*/{ if (color_str.size() > 3) /* Skip the value like "#0"*/{
fila_color.AddColor(wxColour(color_str)); fila_color.m_colors.emplace(wxColour(color_str));
} }
} }
} }
-11
View File
@@ -87,17 +87,6 @@ struct FilamentColor
public: public:
size_t ColorCount() const noexcept { return m_colors.size(); } size_t ColorCount() const noexcept { return m_colors.size(); }
// Preserve catalog endpoint order for display while retaining set-based lookup identity.
const std::vector<wxColour>& GetColors() const noexcept { return m_color_list; }
void AddColor(const wxColour& color)
{
if (m_colors.insert(color).second) m_color_list.push_back(color);
}
private:
std::vector<wxColour> m_color_list;
public:
void EndSet(int ctype) void EndSet(int ctype)
{ {
+15 -108
View File
@@ -252,11 +252,9 @@
#include "slic3r/GUI/ParamsDialog.hpp" #include "slic3r/GUI/ParamsDialog.hpp"
#include "slic3r/GUI/ParamsPanel.hpp" #include "slic3r/GUI/ParamsPanel.hpp"
#include "slic3r/GUI/PartPlate.hpp" #include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/host/PluginPages.hpp" #include "slic3r/plugin/host/PluginPages.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include <wx/defs.h> #include <wx/defs.h>
#include "slic3r/GUI/Widgets/WebView.hpp" #include "slic3r/GUI/Widgets/WebView.hpp"
#include <cwchar> #include <cwchar>
@@ -335,23 +333,6 @@ static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent*
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr; return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
} }
// Full {web} page URL for a same-site OrcaCloud path, or empty when it cannot be built. An already
// open cloud pane is navigated straight to it, without re-running the session handoff.
static wxString cloud_page_url(NetworkAgent* agent, const std::string& path)
{
if (path.empty())
return {};
auto orca = get_orca_cloud_agent(agent);
if (!orca)
return {};
std::string base = orca->get_cloud_base_url();
while (!base.empty() && base.back() == '/')
base.pop_back();
if (base.empty())
return {};
return from_u8(base + path);
}
void start_ping_test() void start_ping_test()
{ {
return; return;
@@ -3082,37 +3063,6 @@ void GUI_App::init_plugin_gui_wiring()
refresh_plugins_dialog(); refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key); switch_printer_agent_after_unload(capability.plugin_key);
}); });
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_register(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_register(capability);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_deregister(capability);
Slic3r::GUI::bump_texture_library_generation();
});
// Packs already loaded before this wiring, or in tests where no callback fires.
PluginResourceTextures::instance().initial_sweep();
Slic3r::GUI::bump_texture_library_generation();
} }
bool GUI_App::on_init_inner() bool GUI_App::on_init_inner()
@@ -5484,7 +5434,12 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({}); this->request_open_project({});
} }
else if (command_str.compare("homepage_open_cloud") == 0) { else if (command_str.compare("homepage_open_cloud") == 0) {
this->open_orca_cloud(); // Deferred: set_cloud_mode() may create a second native webview, which must
// not run inside this webview's script-message callback.
CallAfter([this]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(true, cloud_webview_handoff());
});
} }
else if (command_str.compare("homepage_close_cloud") == 0) { else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() { CallAfter([]() {
@@ -5633,22 +5588,6 @@ std::string GUI_App::handle_web_request(std::string cmd)
return ""; return "";
} }
void GUI_App::open_orca_cloud(const std::string& next)
{
// Remembered so a login while the pane is open re-seeds the same page rather than the default.
m_cloud_open_path = next;
// Deferred: set_cloud_mode() may create the second native webview, so this must not run inside
// the caller's frame (a webview script-message callback or an ImGui frame).
CallAfter([this]() {
if (mainframe != nullptr)
mainframe->select_tab(TAB_ID_HOME);
// The Home page is built lazily, so the tab switch may not have created it yet: ensure() builds
// it if needed, matching what showing the tab does, rather than dropping the open request.
if (WebViewPanel* home = WebViewPanel::ensure())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path), cloud_page_url(m_agent, m_cloud_open_path));
});
}
void GUI_App::handle_script_message(std::string msg, const std::string& provider) void GUI_App::handle_script_message(std::string msg, const std::string& provider)
{ {
try { try {
@@ -5926,7 +5865,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/ }*/
} }
wxString GUI_App::cloud_webview_handoff(const std::string& next) wxString GUI_App::cloud_webview_handoff()
{ {
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session // The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will // and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5934,7 +5873,7 @@ wxString GUI_App::cloud_webview_handoff(const std::string& next)
auto orca = get_orca_cloud_agent(m_agent); auto orca = get_orca_cloud_agent(m_agent);
if (!orca) if (!orca)
return {}; return {};
return from_u8(orca->build_web_session_start_url(next)); return from_u8(orca->build_web_session_start_url());
} }
void GUI_App::request_cloud_web_session(const std::string& state) void GUI_App::request_cloud_web_session(const std::string& state)
@@ -6077,7 +6016,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear(); m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built()) if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active()) if (home->cloud_mode_active())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path)); home->set_cloud_mode(true, cloud_webview_handoff());
} }
@@ -6924,7 +6863,7 @@ void GUI_App::push_notification(const MachineObject* obj, wxString msg, wxStrin
} }
} }
bool GUI_App::reload_settings() void GUI_App::reload_settings()
{ {
if (preset_bundle && m_agent) { if (preset_bundle && m_agent) {
// Load user's personal presets // Load user's personal presets
@@ -6932,7 +6871,7 @@ bool GUI_App::reload_settings()
int result = m_agent->get_user_presets(&user_presets); int result = m_agent->get_user_presets(&user_presets);
if (result != 0) { if (result != 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync";
return false; return;
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable { auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
@@ -7007,30 +6946,13 @@ bool GUI_App::reload_settings()
refresh_synced_ui(); refresh_synced_ui();
else else
CallAfter(refresh_synced_ui); CallAfter(refresh_synced_ui);
return true;
} }
return false;
} }
void GUI_App::check_legacy_user_filament_presets(bool by_user) void GUI_App::check_legacy_user_filament_presets(bool by_user)
{ {
// One check at a time: the startup check can come due while the manual one is open, and on macOS the menu bar
// stays usable while a modal dialog is up.
if (m_legacy_filament_check_running)
return;
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind")) if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return; return;
// The local copies may be older than the cloud ones another machine changed: migrating them would push changes
// made against stale copies and conflict with the cloud. The startup check waits for the presets to be pulled.
if (!by_user && m_user_preset_pull_pending) {
m_legacy_filament_check_due = true;
return;
}
// This check also answers a startup check that is still waiting.
m_legacy_filament_check_due = false;
m_legacy_filament_check_running = true;
ScopeGuard running_guard([this] { m_legacy_filament_check_running = false; });
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets(); const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) { if (legacy_presets.empty()) {
if (by_user) if (by_user)
@@ -7075,9 +6997,6 @@ void GUI_App::check_legacy_user_filament_presets(bool by_user)
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup)); show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return; return;
} }
// Ask the sync loop to push on its next pass instead of its next 60 s round, which shortens the time the migrated
// presets stay unpushed.
m_sync_user_presets_now.store(true, std::memory_order_release);
preset_bundle->update_compatible(PresetSelectCompatibleType::Never); preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui(); mainframe->update_side_preset_ui();
for (auto tab : tabs_list) { for (auto tab : tabs_list) {
@@ -7844,7 +7763,6 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
BOOST_LOG_TRIVIAL(info) << "start_sync_service..."; BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
// BBS // BBS
m_user_sync_token.reset(new int(0)); m_user_sync_token.reset(new int(0));
m_user_preset_pull_pending = true;
if (with_progress_dlg) { if (with_progress_dlg) {
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores // Mark a manual progress dialog as active so restart_sync_user_preset() ignores
// repeat triggers while it is on screen (prevents stacking modal dialogs). // repeat triggers while it is on screen (prevents stacking modal dialogs).
@@ -7918,16 +7836,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
finishFn(ret == 0); finishFn(ret == 0);
// Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded. if (ret == 0 && m_agent && !t.expired())
if (ret == 0 && m_agent && !t.expired() && reload_settings()) reload_settings();
CallAfter([this, t] {
// A stopped or restarted sync: the next sync, if one starts, pulls the presets again.
if (t.expired() || is_closing())
return;
m_user_preset_pull_pending = false;
if (m_legacy_filament_check_due)
check_legacy_user_filament_presets(false);
});
// For orca specific syncing // For orca specific syncing
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()); auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
@@ -7942,11 +7852,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds. // Sync once immediately, then every 60 seconds.
while (!t.expired()) { while (!t.expired()) {
++tick_tock; ++tick_tock;
// Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked // Sync once immediately, then every 60s, or right away when a force-push asked for it.
// for it. The request is consumed on every tick, so one made before a scheduled pass is served by it if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
// instead of running a second pass straight after.
const bool push_now = m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel);
if (tick_tock % 120 == 0 || push_now) {
tick_tock = 0; tick_tock = 0;
if (m_agent) { if (m_agent) {
if (!m_agent->is_user_login()) { if (!m_agent->is_user_login()) {
+3 -13
View File
@@ -355,9 +355,6 @@ private:
std::atomic<bool> m_restart_sync_pending {false}; std::atomic<bool> m_restart_sync_pending {false};
std::atomic<bool> m_sync_user_preset_dlg_active {false}; // a manual "Sync Presets" progress dialog is on screen (see restart_sync_user_preset) std::atomic<bool> m_sync_user_preset_dlg_active {false}; // a manual "Sync Presets" progress dialog is on screen (see restart_sync_user_preset)
std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick
bool m_user_preset_pull_pending {false}; // set when a sync thread starts, cleared once its pull is applied; a failed pull or a stopped sync leaves it set
bool m_legacy_filament_check_due {false}; // the startup check waits for that pull
bool m_legacy_filament_check_running {false}; // a check is in progress, its dialog may be open
std::atomic<bool> m_migration_retry_pending {false}; std::atomic<bool> m_migration_retry_pending {false};
bool m_is_dark_mode{ false }; bool m_is_dark_mode{ false };
bool m_adding_script_handler { false }; bool m_adding_script_handler { false };
@@ -368,8 +365,6 @@ private:
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the // and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
// newest request counts"). // newest request counts").
std::string m_cloud_handoff_state; std::string m_cloud_handoff_state;
// The OrcaCloud path the open requests target, remembered so a login re-seed keeps the same page.
std::string m_cloud_open_path;
uint64_t m_cloud_handoff_gen { 0 }; uint64_t m_cloud_handoff_gen { 0 };
bool m_show_error_msgdlg{false}; bool m_show_error_msgdlg{false};
wxString m_info_dialog_content; wxString m_info_dialog_content;
@@ -471,9 +466,6 @@ public:
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
void init_webview_runtime(); void init_webview_runtime();
#endif #endif
// Switches to the Home tab and opens the OrcaCloud pane beside the homepage on `next`, a
// same-site OrcaCloud path (empty = the page's default). Work is deferred internally.
void open_orca_cloud(const std::string& next = std::string());
static unsigned get_colour_approx_luma(const wxColour& colour); static unsigned get_colour_approx_luma(const wxColour& colour);
static bool dark_mode(); static bool dark_mode();
const wxColour get_label_default_clr_system(); const wxColour get_label_default_clr_system();
@@ -580,7 +572,7 @@ public:
// called from the pane when the page asks for a session; it opens the desktop login dialog if // called from the pane when the page asks for a session; it opens the desktop login dialog if
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL // signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// in the same webview. // in the same webview.
wxString cloud_webview_handoff(const std::string& next = std::string()); wxString cloud_webview_handoff();
void request_cloud_web_session(const std::string& state); void request_cloud_web_session(const std::string& state);
void mint_cloud_web_session(const std::string& state, uint64_t gen); void mint_cloud_web_session(const std::string& state, uint64_t gen);
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash); void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
@@ -597,8 +589,7 @@ public:
void check_new_version_sf(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 made against an older release that lost printers their parent // Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be // listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; while the user presets sync with the cloud, it waits until they have been pulled. by_user also // reminded; by_user also reports when there is nothing to migrate.
// reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user); void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg); bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user); void request_new_version(int by_user);
@@ -609,8 +600,7 @@ public:
std::string format_IP(const std::string& ip); std::string format_IP(const std::string& ip);
void show_dialog(wxString msg); void show_dialog(wxString msg);
void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL); void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL);
// Returns false when the cloud presets could not be fetched, so nothing was reloaded. void reload_settings();
bool reload_settings();
void remove_user_presets(); void remove_user_presets();
bool maybe_migrate_user_presets_on_login(); bool maybe_migrate_user_presets_on_login();
File diff suppressed because it is too large Load Diff
@@ -428,10 +428,6 @@ private:
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max); void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
int m_picker_slot = -1; int m_picker_slot = -1;
bool m_picker_open_request = false; bool m_picker_open_request = false;
// Set when the popup opens, so the next layout scrolls the layer's current texture into view.
bool m_picker_reveal_selected = false;
float m_picker_scroll_target = 0.f; // see ease_wheel_scroll()
float m_picker_scroll_applied = -1.f;
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry); void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer); void import_custom_texture(TextureDisplacementLayer &layer);
@@ -512,25 +508,120 @@ private:
// is where the live adjustment is applied. Called whenever the panel moves or reverts it. // is where the live adjustment is applied. Called whenever the panel moves or reverts it.
void invalidate_adjust_textures(); void invalidate_adjust_textures();
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active // "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane // on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's // where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to // tile extents - so each handle is one of the values it edits rather than a stand-in for it.
// the centroid/average-normal of the active layer's current paint patch (see // Anchored to the centroid/average-normal of the active layer's paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet. // libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
// //
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way // Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
// the texture should appear to move as the handle is dragged - it could not be visually // it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
// confirmed while writing it (no way to render/see pixels in this environment), so it may // cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
// need a one-line sign flip once actually tested. // frame (see on_render_input_window()), not from on_render() with the rest of the GL drawing.
// Move is the whole interior, free in both axes. AxisMove is the four arrows, each holding a
// move to one of the tile's axes. EdgeScale is the four edge bars, one tile extent each, and
// CornerScale the four corner grips, which take both extents at once.
//
// The arrows and the bars share the same four rays out of the tile, so they are kept apart by
// distance rather than direction: the bar hugs its edge, then a clear gap, then the arrow well
// outside it. adjust_pick() leans on that - see the order it tests them in.
enum class AdjustHandle { None, Move, AxisMove, EdgeScale, CornerScale, Rotate };
// The gizmo's geometry for one frame, in screen pixels. Worked out once and shared by the
// drawing and the hit test, so the two cannot disagree about where a handle is. Everything
// except the frame itself is a fixed number of pixels, which is what keeps the control equally
// usable on a 5 mm part and a 500 mm one - the old handle scaled with the volume's bounding box
// and so was unusable at both ends.
struct AdjustFrame
{
bool valid = false;
Vec2d center = Vec2d::Zero();
// +u+v, +u-v, -u-v, -u+v. A scale grip sits on each.
std::array<Vec2d, 4> corner{};
// +u, -u, +v, -v: the middle of each edge, and the two ends of the bar that sits just
// outside it. The bar's ends are kept rather than recomputed so the drawing and the hit test
// cannot end up with it in two different places.
std::array<Vec2d, 4> edge_mid{};
std::array<Vec2d, 4> bar_a{};
std::array<Vec2d, 4> bar_b{};
// The axis arrow further out on the same ray: where its shaft starts and where its chevron
// ends. Kept, like the bar's ends, so the drawing and the hit test cannot disagree.
std::array<Vec2d, 4> arm_base{};
std::array<Vec2d, 4> arm_tip{};
std::array<Vec2d, 4> arm_dir{}; // unit, pointing outward from the centre
// A ring is placed off every corner, but only one is drawn and only that one can be picked:
// four marks for a single operation is clutter, and whichever corner the cursor is near is
// the one worth offering. `rotate_shown` says which, and see update_rotate_corner() for why
// it does not simply track the nearest.
std::array<Vec2d, 4> rotate_at{};
int rotate_shown = 0;
// The tile projects to less than ADJUST_MIN_TILE_PX across, so the frame was drawn at that
// floor instead. It is dashed to say so, and the readout still gives the true millimetres.
bool clamped = false;
// What the frame was built from, in mesh-local mm: the tile's own (rotated) edge directions
// and its extents. The drag arithmetic reuses these rather than recomputing them, so it
// cannot end up working against a different tile than the one on screen.
Vec3f e_u = Vec3f::UnitX(), e_v = Vec3f::UnitY();
Vec2f tile_mm = Vec2f::Ones(); // (u, v) as stored on the layer
Vec2f half_planar = Vec2f::Ones(); // half extents in planar mm, v already aspect-corrected
Vec2f center_planar = Vec2f::Zero();
};
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
// The selected instance's volume-to-world transform, which the bake frame is derived from.
bool adjust_volume_to_world(Transform3d &out) const;
// The anchor, carried from mesh-local into the bake frame. Converted on every use rather than
// stored converted: the object can be moved, rotated or scaled while this mode is open, and a
// stored copy would quietly describe the shape it used to be. The expensive part - finding the
// paint patch's centroid - stays cached in the local members.
bool adjust_anchor_in_frame(Vec3f &pos, Vec3f &normal) const;
// Drops every scrap of live gizmo state. Called from each of the several ways out of the mode -
// the panel's toggle, collapsing the card, switching to seam marking, closing the gizmo - so a
// stale hover highlight or a half-finished drag cannot survive into the next time it is opened.
void end_adjust_texture_mode();
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event); bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
void render_adjust_texture_gizmo(); void render_adjust_texture_gizmo();
// This frame's gizmo geometry, or an invalid frame when there is nothing to draw.
AdjustFrame adjust_frame() const;
// Which handle is under `mouse`, with `index` set to the arm or corner it belongs to. Tested
// outermost first; see the hit-area board of the design for the order and the radii.
AdjustHandle adjust_pick(const Vec2d &mouse, const AdjustFrame &f, int &index) const;
// Chooses which corner carries the rotate ring. The nearest to the cursor, but it only moves once
// the newcomer is clearly nearer, because without that margin the ring flits between two corners
// every time the cursor wanders along the line equidistant from both. Never moves during a drag.
void update_rotate_corner(const AdjustFrame &f, const Vec2d &mouse);
// Where the tile's centre sits, in planar mm relative to the anchor, for a layer's placement.
// Not the patch centroid: the gizmo *is* the texture's placement, so it has to travel as the
// offset changes - pinning it to the centroid is what made the old handle look broken, the
// texture sliding while the handle stayed put.
static Vec2f adjust_handle_planar(const TextureDisplacementLayer &layer, float aspect);
// The exact inverse: the offset that puts the tile's centre at `planar`. Rotating or resizing
// the tile changes where a given offset places it, so every drag recomputes the offset from the
// centre it wants to keep - which is what makes rotate and scale act *in place* rather than
// swinging the tile around the anchor.
static Vec2f adjust_offset_for_planar(const Vec2f &planar, float rot_deg, const Vec2f &tile_mm,
const Vec2f &mirror_sign, float aspect);
// The mesh-local-to-world transform of the selected instance, the (unrotated) tangent basis and
// the lifted anchor the whole gizmo is built on. False when there is nothing to build on - which
// is the single place that decides the gizmo is not drawable, so the drawing, the hit test and
// the drag cannot disagree about it.
bool adjust_basis(Transform3d &trafo, Vec3f &u_axis, Vec3f &v_axis, Vec3f &base) const;
// Screen-space quad of a tile with the given placement. Used for the pre-drag ghost, which is
// drawn from the captured start placement rather than from the layer, the layer having already
// been moved by then.
bool adjust_quad_for(const Vec2f &center_planar, float rot_deg, const Vec2f &tile_mm,
std::array<Vec2d, 4> &out) const;
// Where the cursor meets the drag plane, in planar mm relative to the anchor. The plane is
// deliberately the *fixed* anchor plane rather than the tile's current position: measuring a
// drag against a plane the drag itself moves is a feedback loop.
bool adjust_planar_under(const Vec2d &mouse, Vec2f &planar) const;
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is // Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's // obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame. // foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint(); void render_paint_cursor_hint();
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging // Bake-frame tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
// on-canvas maps consistently onto offset whichever face the anchor is on. // on-canvas maps consistently onto offset whichever face the anchor is on.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const; void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
@@ -1066,8 +1157,6 @@ private:
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures; std::map<std::string, LibraryTexture> m_library_textures;
// Generation of the library the above were cached against; a change clears them.
uint64_t m_library_generation = 0;
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on // Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change // every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
@@ -1109,15 +1198,12 @@ private:
// The config key behind m_undocked. // The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked"; static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for a child window, called inside it: the panel body (everything between the header // Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// and the pinned Bake footer) and the texture library's grid. ImGui jumps a fixed number of lines per // ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
// wheel notch, which on tall layer cards reads as a hard jolt rather than a scroll. The child sets // jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
// ImGuiWindowFlags_NoScrollWithMouse, and the wheel moves `target` instead; the real scroll is eased // moves a *target* offset instead; the real scroll is eased toward it over the following frames.
// toward it over the following frames. `applied` is what the easing wrote last frame, <0 until the
// first one.
void ease_wheel_scroll(float &target, float &applied);
float m_panel_scroll_target = 0.f; float m_panel_scroll_target = 0.f;
float m_panel_scroll_applied = -1.f; float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
// Last frame's body content height and footer height. The body is a child window that has to be // Last frame's body content height and footer height. The body is a child window that has to be
// given its height before its content is laid out, so it is sized from the previous frame: as tall // given its height before its content is laid out, so it is sized from the previous frame: as tall
// as its content, capped so the footer still fits above the bottom of the canvas. // as its content, capped so the footer still fits above the bottom of the canvas.
@@ -1127,23 +1213,47 @@ private:
// See the "Adjust Texture" block of private methods above. // See the "Adjust Texture" block of private methods above.
bool m_adjust_texture_mode = false; bool m_adjust_texture_mode = false;
bool m_adjust_anchor_valid = false; bool m_adjust_anchor_valid = false;
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local // Mesh-local, as compute_layer_paint_anchor() produces them. Everything else about the gizmo
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local // works in the bake frame - world orientation and scale, origin at the volume's own, which is
// where the texture is projected (texture_displacement_bake_frame()) - so these go through
// adjust_anchor_in_frame() at the point of use. A tile is so many *printed* millimetres whatever
// the instance's scale, which is why measuring the gizmo in mesh-local millimetres made its tile
// differ from the rendered one by exactly that scale.
Vec3f m_adjust_anchor_pos = Vec3f::Zero();
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ();
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag // What the cursor is over, and what is being dragged. `index` picks the arm or the corner
// the corresponding arrow, moves offset along only that one axis. // (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
enum class AdjustHandle { None, Pan, AxisU, AxisV }; // Only ever one of each is set, so two handles can never both look live.
AdjustHandle m_adjust_drag_handle = AdjustHandle::None; // Which corner currently carries the rotate ring. Persists between frames, which is what gives
Vec2f m_adjust_drag_start_offset = Vec2f::Zero(); // update_rotate_corner() something to apply its margin against.
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the int m_adjust_rotate_corner = 0;
// moment the current drag started; every subsequent frame's delta is measured against this, AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
// rather than accumulated frame-to-frame, to avoid drift. int m_adjust_hover_index = -1;
Vec2f m_adjust_drag_start_planar = Vec2f::Zero(); AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
int m_adjust_drag_index = -1;
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for // Everything the live drag is measured against, captured once on mouse-down. Every frame's
// both the U and V axis arrows), transformed into place at render time. // delta is taken from these rather than accumulated frame to frame, so a long drag cannot
GLModel m_adjust_panel_glmodel; // drift away from the cursor.
GLModel m_adjust_arrow_glmodel; Vec2f m_adjust_drag_start_center = Vec2f::Zero(); // tile centre, planar mm
Vec2f m_adjust_drag_start_grab = Vec2f::Zero(); // the grabbed point, planar mm
// How far the grab landed from the handle's own position, in planar mm. A scale drag reads the
// extents straight off the cursor, so without subtracting this the tile would jump by however
// far inside the generous hit radius the click happened to fall.
Vec2f m_adjust_drag_grab_bias = Vec2f::Zero();
float m_adjust_drag_start_rot = 0.f;
Vec2f m_adjust_drag_start_tile = Vec2f::Ones(); // (u, v) extents as stored
float m_adjust_drag_start_angle = 0.f; // planar angle of the grab, radians
// The mirroring the layer had when the drag began. A width or height handle dragged back through
// the tile's middle toggles its axis against this, rather than against whatever the last frame
// left behind, so dragging back and forth across the middle cannot ratchet the flag.
bool m_adjust_drag_start_mirror_u = false;
bool m_adjust_drag_start_mirror_v = false;
// The live value to put in the readout pill, built by the drag and drawn by the renderer in the
// same frame. Empty when no drag is running, which is also when no pill is drawn.
std::string m_adjust_readout;
std::map<std::string, wxString> m_desc; std::map<std::string, wxString> m_desc;
+4 -6
View File
@@ -83,13 +83,11 @@ namespace Slic3r::GUI::GLGizmoUtils {
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale); bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
if (url && *url && clicked) if (url && *url && clicked)
wxLaunchDefaultBrowser(wxString::FromUTF8(url)); wxLaunchDefaultBrowser(wxString::FromUTF8(url));
// ImGuiWrapper::tooltip keeps the text readable on the dark tooltip background whatever text colour the
// host window pushed; a negative wrap position keeps the URL on one line.
if (ImGui::IsItemHovered()){ if (ImGui::IsItemHovered()){
if(url && *url) if(url && *url)
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide") + "\n" + url, -1.f); ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url);
else else
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide"), -1.f); ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str());
} }
return clicked; // for dynamically generated links return clicked; // for dynamically generated links
} }
@@ -103,9 +101,9 @@ namespace Slic3r::GUI::GLGizmoUtils {
wxLaunchDefaultBrowser(wxString::FromUTF8(url)); wxLaunchDefaultBrowser(wxString::FromUTF8(url));
if (ImGui::IsItemHovered()){ if (ImGui::IsItemHovered()){
if(url && *url) if(url && *url)
wxGetApp().imgui()->tooltip(_u8L("Video Guide") + "\n" + url, -1.f); ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url);
else else
wxGetApp().imgui()->tooltip(_u8L("Video Guide"), -1.f); ImGui::SetTooltip("%s", _u8L("Video Guide").c_str());
} }
return clicked; // for dynamically generated links return clicked; // for dynamically generated links
} }
@@ -857,12 +857,11 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) { if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
combox_changed = true; combox_changed = true;
} }
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
auto tooltip_str = _L("Coordinate system used for transform actions."); auto tooltip_str = _L("Coordinate system used for transform actions.");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
} }
ImGuiWrapper::pop_combo_style();
// ORCA use TextColored to match axes color // ORCA use TextColored to match axes color
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2; float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
@@ -965,12 +964,11 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width)) if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width))
m_align_to_parent = align_mode_idx + 1 == align_modes.size(); m_align_to_parent = align_mode_idx + 1 == align_modes.size();
ImGui::PopID(); ImGui::PopID();
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
wxString tooltip_str = _L("Reference for alignment"); wxString tooltip_str = _L("Reference for alignment");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
} }
ImGuiWrapper::pop_combo_style();
const bool dark_mode = wxGetApp().dark_mode(); const bool dark_mode = wxGetApp().dark_mode();
int current_axis = -1; int current_axis = -1;
@@ -1379,12 +1377,11 @@ void GizmoObjectManipulation::do_render_scale_input_window(ImGuiWrapper* imgui_w
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) { if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
combox_changed = true; combox_changed = true;
} }
ImGuiWrapper::pop_combo_style();
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
auto tooltip_str = _L("Coordinate system used for transform actions."); auto tooltip_str = _L("Coordinate system used for transform actions.");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
} }
ImGuiWrapper::pop_combo_style();
// ORCA use TextColored to match axes color // ORCA use TextColored to match axes color
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2; float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
+3 -5
View File
@@ -505,12 +505,10 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt)
io.MouseDown[0] = evt.LeftIsDown(); io.MouseDown[0] = evt.LeftIsDown();
io.MouseDown[1] = evt.RightIsDown(); io.MouseDown[1] = evt.RightIsDown();
io.MouseDown[2] = evt.MiddleIsDown(); io.MouseDown[2] = evt.MiddleIsDown();
// Only a vertical wheel event with a direction is a wheel step. A macOS trackpad reports sideways motion float wheel_delta = static_cast<float>(evt.GetWheelDelta());
// as separate horizontal events, and its wheel events often carry a rotation of 0: wx truncates the if (wheel_delta != 0.0f) {
// sub-pixel deltas to whole ones and passes on events with no delta at all. Read as vertical steps,
// either kind scrolls against the gesture, and scrolling up bounces back and forth.
if (evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL && evt.GetWheelRotation() != 0)
io.MouseWheel = evt.GetWheelRotation() > 0 ? 1.f : -1.f; io.MouseWheel = evt.GetWheelRotation() > 0 ? 1.f : -1.f;
}
unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0); unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0);
m_mouse_buttons = buttons; m_mouse_buttons = buttons;
+10 -72
View File
@@ -1,7 +1,6 @@
#include "TextureLibrary.hpp" #include "TextureLibrary.hpp"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <cstddef> #include <cstddef>
#include <boost/filesystem/directory.hpp> #include <boost/filesystem/directory.hpp>
@@ -22,7 +21,6 @@
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
#include <vector> #include <vector>
#include <wx/app.h>
#include <wx/gdicmn.h> #include <wx/gdicmn.h>
#include <utility> #include <utility>
#include <wx/image.h> #include <wx/image.h>
@@ -38,9 +36,6 @@
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
namespace Slic3r::GUI { namespace Slic3r::GUI {
namespace { namespace {
@@ -55,7 +50,7 @@ bool is_image_file(const boost::filesystem::path &path)
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp"; return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
} }
void scan_dir(const boost::filesystem::path &dir, TextureSource source, std::vector<TextureLibraryEntry> &out) void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out)
{ {
boost::system::error_code ec; boost::system::error_code ec;
if (!boost::filesystem::is_directory(dir, ec)) if (!boost::filesystem::is_directory(dir, ec))
@@ -65,43 +60,12 @@ void scan_dir(const boost::filesystem::path &dir, TextureSource source, std::vec
for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) { for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) {
if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path())) if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path()))
continue; continue;
out.push_back({ it->path().stem().string(), it->path().string(), source }); out.push_back({ it->path().stem().string(), it->path().string(), is_user });
} }
std::sort(out.begin() + first, out.end(), std::sort(out.begin() + first, out.end(),
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; }); [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
} }
// Appends every loaded pack's textures, ordered by the pack's display name and then by file name. Each
// entry carries the contributing pack's display name, which the picker uses to group a pack's textures
// into one section; a pack name is unique among loaded packs of this capability type, so the name both
// groups the section and keeps its entries contiguous. A pack whose directory fails validation is
// skipped, so one bad pack never hides the others.
void scan_plugin_textures(std::vector<TextureLibraryEntry> &out)
{
std::vector<TextureLibraryEntry> found;
for (const ResourceTextureEntry &pack : PluginResourceTextures::instance().entries()) {
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files(pack.texture_dir, pack.plugin_root, error);
if (!error.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Texture pack '" << pack.display_name << "' was skipped: " << error;
continue;
}
for (const std::string &file : files) {
const boost::filesystem::path path(file);
found.push_back({ path.stem().string(), file, TextureSource::Plugin, pack.display_name });
}
}
std::stable_sort(found.begin(), found.end(), [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) {
if (a.pack_name != b.pack_name)
return a.pack_name < b.pack_name;
return a.name < b.name;
});
for (TextureLibraryEntry &entry : found)
out.push_back(std::move(entry));
}
// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A // True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A
// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of // photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of
// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer // the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer
@@ -244,11 +208,6 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
std::vector<TextureLibraryEntry> g_library; std::vector<TextureLibraryEntry> g_library;
bool g_library_scanned = false; bool g_library_scanned = false;
// The generation g_library was last built from, so texture_library() can rescan after a pack change.
uint64_t g_library_scan_generation = 0;
// Bumped by plugin callbacks, read by the GUI frame. Atomic so a read never races a write.
std::atomic<uint64_t> g_library_generation{ 0 };
// Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture - // Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture -
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new // shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
@@ -296,8 +255,6 @@ const size_t RECENT_MAX = 200;
std::vector<std::string> recent_texture_names() std::vector<std::string> recent_texture_names()
{ {
std::vector<std::string> names; std::vector<std::string> names;
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to read
return names;
if (wxGetApp().app_config == nullptr) if (wxGetApp().app_config == nullptr)
return names; return names;
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY); const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
@@ -312,8 +269,6 @@ std::vector<std::string> recent_texture_names()
void store_recent_texture_names(std::vector<std::string> names) void store_recent_texture_names(std::vector<std::string> names)
{ {
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to write
return;
if (wxGetApp().app_config == nullptr) if (wxGetApp().app_config == nullptr)
return; return;
if (names.size() > RECENT_MAX) if (names.size() > RECENT_MAX)
@@ -324,9 +279,8 @@ void store_recent_texture_names(std::vector<std::string> names)
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value); wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
} }
// Reorders the user's entries by recent use. Stable, so textures never used keep the name order // Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep
// scan_dir() gave them, after every used one. Only the user's own range is touched: shipped and pack // the name order scan_dir() gave them, after every used one.
// entries must keep the order their scans produced.
void sort_user_textures(std::vector<TextureLibraryEntry> &library) void sort_user_textures(std::vector<TextureLibraryEntry> &library)
{ {
const std::vector<std::string> names = recent_texture_names(); const std::vector<std::string> names = recent_texture_names();
@@ -337,11 +291,8 @@ void sort_user_textures(std::vector<TextureLibraryEntry> &library)
const auto it = rank.find(boost::filesystem::path(e.path).filename().string()); const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
return it == rank.end() ? std::numeric_limits<int>::max() : it->second; return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
}; };
const auto first_user = std::find_if(library.begin(), library.end(), const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; });
[](const TextureLibraryEntry &e) { return e.source == TextureSource::User; }); std::stable_sort(first_user, library.end(),
const auto user_end = std::find_if(first_user, library.end(),
[](const TextureLibraryEntry &e) { return e.source != TextureSource::User; });
std::stable_sort(first_user, user_end,
[&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); }); [&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); });
} }
@@ -360,16 +311,6 @@ bool in_user_texture_dir(const std::string &path)
} // namespace } // namespace
uint64_t texture_library_generation()
{
return g_library_generation.load();
}
void bump_texture_library_generation()
{
g_library_generation.fetch_add(1);
}
std::string user_texture_dir() std::string user_texture_dir()
{ {
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement"; const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
@@ -384,20 +325,17 @@ std::string user_texture_dir()
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan) const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
{ {
const uint64_t generation = texture_library_generation(); if (g_library_scanned && !force_rescan)
if (g_library_scanned && !force_rescan && generation == g_library_scan_generation)
return g_library; return g_library;
g_library.clear(); g_library.clear();
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", TextureSource::Shipped, g_library); scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library);
const std::string user_dir = user_texture_dir(); const std::string user_dir = user_texture_dir();
if (!user_dir.empty()) if (!user_dir.empty())
scan_dir(boost::filesystem::path(user_dir), TextureSource::User, g_library); scan_dir(boost::filesystem::path(user_dir), true, g_library);
sort_user_textures(g_library); sort_user_textures(g_library);
scan_plugin_textures(g_library);
g_library_scan_generation = generation; g_library_scanned = true;
g_library_scanned = true;
return g_library; return g_library;
} }
+11 -31
View File
@@ -1,7 +1,6 @@
#ifndef slic3r_TextureLibrary_hpp_ #ifndef slic3r_TextureLibrary_hpp_
#define slic3r_TextureLibrary_hpp_ #define slic3r_TextureLibrary_hpp_
#include <cstdint>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -11,45 +10,26 @@
namespace Slic3r::GUI { namespace Slic3r::GUI {
// Where a library texture came from. The picker lists each source under its own heading. // One selectable height-map texture in the texture-displacement gizmo's texture picker.
enum class TextureSource { Shipped, User, Plugin };
// One selectable height-map texture in the texture-displacement gizmo's texture picker. A texture that
// came from a plugin pack also carries the provenance the picker groups its sections by.
struct TextureLibraryEntry struct TextureLibraryEntry
{ {
std::string name; // display name (the file's stem, e.g. "Wood Grain") std::string name; // display name (the file's stem, e.g. "Wood Grain")
std::string path; // absolute path on disk std::string path; // absolute path on disk
TextureSource source; // which of the three sources it came from bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
std::string pack_name; // contributing pack's display name; empty for Shipped and User
}; };
// Every available height-map texture, in the order the picker lists them: the ones shipped in // Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
// resources/textures/displacement first, by name; then the user's own from <data_dir>/textures/displacement, // by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see
// most recently used first (see touch_user_texture()) and the rest by name; then the loaded texture // touch_user_texture()), then any never used, by name.
// packs, ordered by pack display name and then by file name.
// //
// The shipped and user textures live in separate directories deliberately: an app update replaces the // The two live in separate directories deliberately: an app update replaces the resources tree
// resources tree wholesale, so anything the user imported has to sit somewhere that update can never // wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
// overwrite or delete. Pack textures are read-only like the shipped ones. Every texture carries its // delete. `is_user` is what the picker uses to show them under separate headings.
// `source`; a pack texture also carries `pack_name`, and the picker groups pack textures into one
// collapsible section per pack name. A pack's name is unique among loaded packs of this capability type -
// the loader refuses a collision with "Capability collision for type ... and name ...", see
// PluginManager::check_registry_locked (PluginManager.cpp:211-225) - so the name on its own identifies a
// section and keeps that pack's entries contiguous.
// //
// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped // Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped
// into either folder by hand while the app was running. A rescan also happens on its own when the pack // into either folder by hand while the app was running.
// registry changes, which is what texture_library_generation() tracks.
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false); const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
// Monotonic counter, bumped whenever the set of loaded texture packs changes. The GUI reads it to
// decide whether texture_library() must rescan, and plugin callbacks bump it. Safe to read from any
// thread at any time.
uint64_t texture_library_generation();
void bump_texture_library_generation();
// Moves one of the user's own textures to the front of the recently-used order texture_library() lists // Moves one of the user's own textures to the front of the recently-used order texture_library() lists
// them in, and stores that order in the app config so it survives a restart. No-op for anything that is // them in, and stores that order in the app config so it survives a restart. No-op for anything that is
// not in user_texture_dir(). // not in user_texture_dir().
+18 -5
View File
@@ -297,9 +297,12 @@ void UVEditorCanvas::set_island_fill_colors(std::vector<ColorRGBA> colors)
Refresh(); Refresh();
} }
void UVEditorCanvas::set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored) void UVEditorCanvas::set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
float rotation_deg, bool tile_enabled, bool tile_mirrored)
{ {
m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f; m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f;
m_tiling_scale_v = (tiling_scale_v > 1e-6f) ? tiling_scale_v : m_tiling_scale;
m_mirror_sign = mirror_sign;
m_rotation_deg = rotation_deg; m_rotation_deg = rotation_deg;
m_tile_enabled = tile_enabled; m_tile_enabled = tile_enabled;
m_tile_mirrored = tile_mirrored; m_tile_mirrored = tile_mirrored;
@@ -661,11 +664,18 @@ Vec2f UVEditorCanvas::island_centroid(int island) const
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
{ {
// apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of // apply_uv_transform() maps unwrap -> uv as uv = S^-1 R(unwrap) + offset, with S the tile
// a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv). // extents, so the inverse of a *delta* (the translation drops out) is unwrap = R^-1(S delta_uv).
// The extents therefore multiply *before* the inverse rotation, which is the mirror image of the
// forward order; with a square tile the two are interchangeable, with a rectangular one they are
// not.
const float rad = m_rotation_deg * float(M_PI) / 180.f; const float rad = m_rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad), sn = std::sin(rad); const float cs = std::cos(rad), sn = std::sin(rad);
return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale; // The mirror is its own sign on the same axis, and a negation is its own inverse, so it comes
// back out here exactly as it went in.
const Vec2f scaled(delta_uv.x() * m_tiling_scale * m_mirror_sign.x(),
delta_uv.y() * m_tiling_scale_v * m_mirror_sign.y());
return Vec2f(scaled.x() * cs + scaled.y() * sn, -scaled.x() * sn + scaled.y() * cs);
} }
Vec2f UVEditorCanvas::snap_correction(int island) const Vec2f UVEditorCanvas::snap_correction(int island) const
@@ -2180,7 +2190,10 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s) void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
{ {
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap"); const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
const wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString(); // A square tile reads as one number; only a genuinely rectangular one is worth two.
const wxString tile = !s.has_layer ? wxString() :
std::abs(s.tile_mm_v - s.tile_mm) < 0.005f ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) :
wxString::Format(_L("%.1f x %.1f mm tile"), s.tile_mm, s.tile_mm_v);
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile; const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
m_layer_name->SetLabel(name); m_layer_name->SetLabel(name);
m_tile->SetLabel(tile); m_tile->SetLabel(tile);
+10 -4
View File
@@ -91,7 +91,10 @@ public:
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually // space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
// lives (see apply_uv_transform()). The tile settings come along because the background has to // lives (see apply_uv_transform()). The tile settings come along because the background has to
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked. // repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored); // `tiling_scale_v` is the tile's extent along v; pass <= 0 for a square tile, matching
// layer_tile_mm().
void set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
float rotation_deg, bool tile_enabled, bool tile_mirrored);
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown // Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the // beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
@@ -129,7 +132,8 @@ public:
{ {
bool has_layer = false; // an active layer mapped with Unwrap bool has_layer = false; // an active layer mapped with Unwrap
wxString layer_name; wxString layer_name;
float tile_mm = 0.f; float tile_mm = 0.f; // tile extent along u
float tile_mm_v = 0.f; // along v; equal to tile_mm for a square tile
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
int thumbnail_px = 0; int thumbnail_px = 0;
float seam_angle_deg = 40.f; float seam_angle_deg = 40.f;
@@ -292,8 +296,10 @@ private:
GLModel m_grid_glmodel; GLModel m_grid_glmodel;
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
float m_tiling_scale = 1.f; float m_tiling_scale = 1.f;
float m_rotation_deg = 0.f; float m_tiling_scale_v = 1.f; // already resolved: never the "unset" sentinel, see set_uv_transform()
Vec2f m_mirror_sign = Vec2f::Ones(); // +1 or -1 per axis, see layer_mirror_sign()
float m_rotation_deg = 0.f;
bool m_tile_enabled = true; bool m_tile_enabled = true;
bool m_tile_mirrored = false; bool m_tile_mirrored = false;
bool m_snap_enabled = false; bool m_snap_enabled = false;
+1 -9
View File
@@ -537,7 +537,7 @@ void WebViewPanel::cloud_signed_out(const wxString& signout_url)
} }
} }
void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString& page_url) void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
{ {
if (on) { if (on) {
// load_cloud_url records the origin from whatever it loads, including the plain cloud // load_cloud_url records the origin from whatever it loads, including the plain cloud
@@ -546,10 +546,6 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString&
create_cloud_browser(); create_cloud_browser();
m_cloud_seed_url = url; m_cloud_seed_url = url;
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url); schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
} else if (!page_url.empty()) {
// The pane already holds a session, so an explicit page loads straight in; the handoff is
// only needed to establish one, not to reach an ordinary page.
load_cloud_url(page_url);
} else if (!url.empty() && url != m_cloud_seed_url) { } else if (!url.empty() && url != m_cloud_seed_url) {
// Re-seed only when the Start URL actually changed — a login, an account switch or a // Re-seed only when the Start URL actually changed — a login, an account switch or a
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged // sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
@@ -560,10 +556,6 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString&
load_cloud_after_signout(url); load_cloud_after_signout(url);
} }
m_cloud_browser->Show(); m_cloud_browser->Show();
// The homepage shell owns the nav highlight and the cloud-mode layout class. A native open
// (not the page's own GotoCloud) must ask it to switch; the page's SetCloudMode no-ops if it
// already did. OnDocumentLoaded re-applies this when the homepage was still loading.
RunScript("SetCloudMode(true)");
} else if (m_cloud_browser) { } else if (m_cloud_browser) {
// Closing keeps the pane alive with its session, so re-opening does not have to sign in // Closing keeps the pane alive with its session, so re-opening does not have to sign in
// again (guide §10). Nothing is cleared here. // again (guide §10). Nothing is cleared here.
+1 -4
View File
@@ -118,10 +118,7 @@ public:
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the // Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide // cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
// whether it already has a session; empty falls back to the plain cloud page. // whether it already has a session; empty falls back to the plain cloud page.
// `page_url` is a full {web} page URL: an already-open pane is navigated straight there, void set_cloud_mode(bool on, const wxString& url = wxString());
// without re-running the handoff, which is what an explicit "open this page" wants. Empty
// keeps whatever page the pane holds.
void set_cloud_mode(bool on, const wxString& url = wxString(), const wxString& page_url = wxString());
// True while the OrcaCloud pane exists and is visible. // True while the OrcaCloud pane exists and is visible.
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); } bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
+33 -58
View File
@@ -38,8 +38,6 @@
#include <cstddef> #include <cstddef>
#include <wx/simplebook.h> #include <wx/simplebook.h>
#include <wx/dcgraph.h> #include <wx/dcgraph.h>
#include <wx/graphics.h>
#include <memory>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <wx/timer.h> #include <wx/timer.h>
@@ -1411,17 +1409,22 @@ void AMSLib::render_lite_lib(wxDC& dc)
dc.SetPen(wxPen(*wxTRANSPARENT_PEN)); dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
if (m_info.material_cols.size() > 1) { if (m_info.material_cols.size() > 1) {
wxRect color_rect(FromDIP(10), FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18)); int left = FromDIP(10);
int gwidth = std::round(libsize.x / (m_info.material_cols.size() - 1));
//gradient
if (m_info.ctype == 0) { if (m_info.ctype == 0) {
fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back()); for (int i = 0; i < m_info.material_cols.size() - 1; i++) {
auto rect = wxRect(left, FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18));
fill_gradient_rect_east(dc, rect, m_info.material_cols[i], m_info.material_cols[i + 1]);
left += gwidth;
}
} }
else { else {
int cols_size = m_info.material_cols.size(); int cols_size = m_info.material_cols.size();
for (int i = 0; i < cols_size; i++) { for (int i = 0; i < cols_size; i++) {
dc.SetBrush(wxBrush(m_info.material_cols[i])); dc.SetBrush(wxBrush(m_info.material_cols[i]));
int x = color_rect.x + color_rect.width * i / cols_size; float x = FromDIP(10) + ((float)libsize.x - FromDIP(18)) * i / cols_size;
int next_x = color_rect.x + color_rect.width * (i + 1) / cols_size; dc.DrawRoundedRectangle(x, FromDIP(10), ((float)libsize.x - FromDIP(17)) / cols_size, libsize.y - FromDIP(20), 0);
dc.DrawRectangle(x, color_rect.y, next_x - x, color_rect.height);
} }
dc.SetBrush(wxBrush(tmp_lib_colour)); dc.SetBrush(wxBrush(tmp_lib_colour));
} }
@@ -1513,27 +1516,7 @@ void AMSLib::render_generic_lib(wxDC &dc)
int top = height - curr_height; int top = height - curr_height;
if (m_ams_model == EXT_AMS){ if (m_ams_model == EXT_AMS){
wxRect color_rect(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1)); dc.DrawRoundedRectangle(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1), m_radius - 1);
if ((m_info.ctype == 0 || m_info.ctype == 1) && m_info.material_cols.size() > 1 && alpha != 0) {
if (m_info.ctype == 0) {
fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back());
}
else {
int cols_size = static_cast<int>(m_info.material_cols.size());
for (int i = 0; i < cols_size; i++) {
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
dc.SetBrush(wxBrush(m_info.material_cols[i]));
int x = color_rect.x + color_rect.width * i / cols_size;
int next_x = color_rect.x + color_rect.width * (i + 1) / cols_size;
dc.DrawRectangle(x, color_rect.y, next_x - x, color_rect.height);
}
}
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
dc.SetBrush(wxBrush(tmp_lib_colour));
}
else {
dc.DrawRoundedRectangle(color_rect.x, color_rect.y, color_rect.width, color_rect.height, m_radius - 1);
}
if (alpha == 0) { if (alpha == 0) {
dc.DrawBitmap(m_bitmap_transparent_def.bmp(), FromDIP(2), FromDIP(2)); dc.DrawBitmap(m_bitmap_transparent_def.bmp(), FromDIP(2), FromDIP(2));
} }
@@ -2861,38 +2844,30 @@ void AMSPreview::doRender(wxDC &dc)
//dc.DrawRoundedRectangle((size.x - rec_size.x) / 2, (size.y - rec_size.y) / 2, rec_size.x, rec_size.y, FromDIP(2)); //dc.DrawRoundedRectangle((size.x - rec_size.x) / 2, (size.y - rec_size.y) / 2, rec_size.x, rec_size.y, FromDIP(2));
if (iter.material_cols.size() > 1) if (iter.material_cols.size() > 1)
{ {
const wxSize swatch_size = m_ams_item_type == AMSModel::N3S_AMS ? AMS_ITEM_CUBE_SIZE : AMS_ITEM_CUBE_SIZE2; int fleft = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2;
const wxRect rect((size.x - swatch_size.x) / 2, (size.y - swatch_size.y) / 2, swatch_size.x, swatch_size.y);
const int radius = m_ams_item_type == AMSModel::N3S_AMS ? 0 : FromDIP(3); float total_width = AMS_ITEM_CUBE_SIZE.x;
std::unique_ptr<wxGraphicsContext> owned_gc; int gwidth = (total_width / (iter.material_cols.size()));
wxGraphicsContext* gc = dc.GetGraphicsContext(); if (iter.ctype == 0) {
if (!gc) { for (int i = 0; i < iter.material_cols.size() - 1; i++) {
owned_gc.reset(wxGraphicsContext::CreateFromUnknownDC(dc));
gc = owned_gc.get(); if ((fleft + gwidth) > (AMS_ITEM_CUBE_SIZE.x)) {
} gwidth = (fleft + AMS_ITEM_CUBE_SIZE.x) - fleft;
if (gc) {
wxGraphicsGradientStops stops(iter.material_cols.front(), iter.material_cols.back());
const size_t count = iter.material_cols.size();
if (iter.ctype == 0) {
for (size_t i = 1; i + 1 < count; ++i)
stops.Add(iter.material_cols[i], static_cast<float>(i) / (count - 1));
} else {
// Two stops at each boundary keep multicolour bands distinct.
for (size_t i = 1; i < count; ++i) {
const float position = static_cast<float>(i) / count;
stops.Add(iter.material_cols[i - 1], position);
stops.Add(iter.material_cols[i], position);
} }
auto rect = wxRect(fleft, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, gwidth, AMS_ITEM_CUBE_SIZE.y);
fill_gradient_rect_east(dc, rect, iter.material_cols[i], iter.material_cols[i + 1]);
fleft += gwidth;
}
}
else {
int cols_size = iter.material_cols.size();
for (int i = 0; i < cols_size; i++) {
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
dc.SetBrush(wxBrush(iter.material_cols[i]));
float x = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2 + total_width * i / cols_size;
dc.DrawRectangle(x, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, total_width / cols_size, AMS_ITEM_CUBE_SIZE.y);
} }
gc->PushState();
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(gc->CreateLinearGradientBrush(rect.x, rect.y, rect.x + rect.width, rect.y, stops));
gc->DrawRoundedRectangle(rect.x, rect.y, rect.width, rect.height, radius);
gc->PopState();
} else {
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(iter.material_colour);
dc.DrawRoundedRectangle(rect, radius);
} }
} }
else { else {
-27
View File
@@ -74,10 +74,6 @@ webkit_web_view_run_javascript_finish (WebKitWebView
GError **error); GError **error);
WEBKIT_API void WEBKIT_API void
webkit_javascript_result_unref (WebKitJavascriptResult *js_result); webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
WEBKIT_API guint
webkit_get_major_version (void);
WEBKIT_API guint
webkit_get_minor_version (void);
} }
#endif #endif
@@ -431,29 +427,6 @@ bool WebView::NeedsRecreateOnShow()
return false; return false;
#endif #endif
} }
bool WebView::NeedsCompositingWorkaround()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major < 2 || (major == 2 && minor < 42);
#else
return false;
#endif
}
bool WebView::SupportsForceSharedMemory()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major > 2 || (major == 2 && minor >= 44);
#else
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime() bool WebView::CheckWebViewRuntime()
{ {
-10
View File
@@ -34,16 +34,6 @@ public:
// ignoring every navigation. A panel that gets true here recreates its view on first Show(). // ignoring every navigation. A panel that gets true here recreates its view on first Show().
static bool NeedsRecreateOnShow(); static bool NeedsRecreateOnShow();
// True when the linked WebKitGTK predates 2.42, where accelerated compositing
// could fail silently on XWayland sessions, so it is forced off as a fallback.
// False on every other platform. Safe to call before GTK initialises.
static bool NeedsCompositingWorkaround();
// True when the linked WebKitGTK is new enough (>= 2.44) to honour
// WEBKIT_DMABUF_RENDERER_FORCE_SHM, the shared-memory transport switch.
// False on every other platform. Safe to call before GTK initialises.
static bool SupportsForceSharedMemory();
static void RecreateAll(); static void RecreateAll();
}; };
-1
View File
@@ -529,7 +529,6 @@ std::string PluginConfig::capability_type_label(PluginCapabilityType type)
case PluginCapabilityType::Visualization: return _u8L("Visualization"); case PluginCapabilityType::Visualization: return _u8L("Visualization");
case PluginCapabilityType::Script: return _u8L("Script"); case PluginCapabilityType::Script: return _u8L("Script");
case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline"); case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline");
case PluginCapabilityType::ResourceTexture: return _u8L("Resource Texture");
default: return _u8L("Unknown"); default: return _u8L("Unknown");
} }
} }
-73
View File
@@ -9,7 +9,6 @@
#include <boost/filesystem/directory.hpp> #include <boost/filesystem/directory.hpp>
#include <boost/filesystem/file_status.hpp> #include <boost/filesystem/file_status.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include "PluginAuditManager.hpp" #include "PluginAuditManager.hpp"
@@ -1256,76 +1255,4 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
return {}; return {};
} }
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
const std::string& plugin_root,
std::string& error)
{
namespace fs = boost::filesystem;
// Picker sanity bound, not a security control: the popup draws every entry in its grid.
constexpr size_t kMaxResourceTextureFiles = 64;
std::vector<std::string> files;
error.clear();
if (dir.empty()) {
error = _u8L("Resource texture directory is not set");
return files;
}
if (plugin_root.empty()) {
error = _u8L("Plugin folder could not be determined.");
return files;
}
boost::system::error_code ec;
const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec);
if (ec) {
error = _u8L("Resource texture directory could not be resolved");
return files;
}
if (!fs::is_directory(resolved_dir, ec) || ec) {
error = _u8L("Resource texture path is not a directory");
return files;
}
const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec);
if (ec) {
error = _u8L("Plugin folder could not be resolved");
return files;
}
if (!is_inside_allowed_root(resolved_dir, resolved_root)) {
error = _u8L("Resource texture directory is outside the plugin folder");
return files;
}
for (fs::directory_iterator it(resolved_dir, ec), end; it != end && !ec; it.increment(ec)) {
const fs::path& path = it->path();
if (!fs::is_regular_file(path, ec) || ec)
continue;
std::string ext = path.extension().string();
boost::algorithm::to_lower(ext);
if (ext != ".png" && ext != ".jpg" && ext != ".jpeg" && ext != ".bmp")
continue;
// Re-check per file: a symlink inside the pack must not resolve outside it.
if (!is_inside_allowed_root(path, resolved_root))
continue;
files.push_back(path.string());
if (files.size() >= kMaxResourceTextureFiles)
break;
}
if (ec) {
error = _u8L("Resource texture directory could not be read");
return {};
}
return files;
}
} // namespace Slic3r } // namespace Slic3r
-6
View File
@@ -176,12 +176,6 @@ bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginD
// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates. // Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error); boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
// Validated non-recursive .png/.jpg/.jpeg/.bmp files directly inside `dir`, every one contained in
// `plugin_root`. Empty with `error` set on failure.
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
const std::string& plugin_root,
std::string& error);
// Canonical writer: emits the .install_state.json schema for the given state. // Canonical writer: emits the .install_state.json schema for the given state.
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state); bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
// Builds a PluginInstallState from the descriptor and delegates to the canonical writer. // Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
-32
View File
@@ -9,7 +9,6 @@
#include "PluginFsUtils.hpp" #include "PluginFsUtils.hpp"
#include "PythonInterpreter.hpp" #include "PythonInterpreter.hpp"
#include "PythonPluginBridge.hpp" #include "PythonPluginBridge.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp" #include "slic3r/GUI/format.hpp"
@@ -388,37 +387,6 @@ bool load(const PluginDescriptor& descriptor,
instance->set_config_ui_available(false); instance->set_config_ui_available(false);
} }
// ResourceTexture capabilities resolve their texture directory here, under the
// GIL this block already holds, and cache the validated result. A resolution
// fault only costs this capability its textures, so it is caught locally like
// has_config_ui() above rather than failing the load.
if (auto* resource_texture = dynamic_cast<ResourceTexturePluginCapability*>(instance.get())) {
try {
const std::string texture_dir = resource_texture->get_texture_dir();
if (texture_dir.empty()) {
resource_texture->set_texture_dir({});
} else {
std::string texture_error;
collect_resource_texture_files(texture_dir, descriptor.plugin_root, texture_error);
if (texture_error.empty()) {
resource_texture->set_texture_dir(texture_dir);
} else {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': texture directory rejected (" << texture_error
<< "); the capability will provide no textures";
resource_texture->set_texture_dir({});
}
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': get_texture_dir() failed (" << ex.what()
<< "); the capability will provide no textures";
resource_texture->set_texture_dir({});
}
}
capabilities.push_back(std::move(instance)); capabilities.push_back(std::move(instance));
} }
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
-3
View File
@@ -25,7 +25,6 @@
#include "PyPluginTrampoline.hpp" #include "PyPluginTrampoline.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/pages/PagesPluginCapability.hpp" #include "pluginTypes/pages/PagesPluginCapability.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include <pybind11/pytypes.h> #include <pybind11/pytypes.h>
@@ -375,7 +374,6 @@ void bind_python_api(pybind11::module_& m)
.value("Visualization", PluginCapabilityType::Visualization) .value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script) .value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("ResourceTexture", PluginCapabilityType::ResourceTexture)
.value("Unknown", PluginCapabilityType::Unknown) .value("Unknown", PluginCapabilityType::Unknown)
.export_values(); .export_values();
@@ -529,7 +527,6 @@ void bind_python_api(pybind11::module_& m)
PagesPluginCapability::RegisterBindings(m); PagesPluginCapability::RegisterBindings(m);
ScriptPluginCapability::RegisterBindings(m); ScriptPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m); SlicingPipelinePluginCapability::RegisterBindings(m);
ResourceTexturePluginCapability::RegisterBindings(m);
PluginHost::RegisterBindings(m); PluginHost::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
+1 -5
View File
@@ -14,7 +14,7 @@
namespace Slic3r { namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, ResourceTexture, Unknown }; enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
struct PluginCapabilityId struct PluginCapabilityId
{ {
@@ -48,7 +48,6 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
case PluginCapabilityType::Visualization: return "visualization"; case PluginCapabilityType::Visualization: return "visualization";
case PluginCapabilityType::Script: return "script"; case PluginCapabilityType::Script: return "script";
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline"; case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
case PluginCapabilityType::ResourceTexture: return "resource-texture";
default: return "unknown"; default: return "unknown";
} }
} }
@@ -64,7 +63,6 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
case PluginCapabilityType::Visualization: return "Visualization"; case PluginCapabilityType::Visualization: return "Visualization";
case PluginCapabilityType::Script: return "Script"; case PluginCapabilityType::Script: return "Script";
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline"; case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
case PluginCapabilityType::ResourceTexture: return "Resource Texture";
default: return "Unknown"; default: return "Unknown";
} }
} }
@@ -94,8 +92,6 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
return PluginCapabilityType::Script; return PluginCapabilityType::Script;
if (lowered == "slicing-pipeline") if (lowered == "slicing-pipeline")
return PluginCapabilityType::SlicingPipeline; return PluginCapabilityType::SlicingPipeline;
if (lowered == "resource-texture")
return PluginCapabilityType::ResourceTexture;
return PluginCapabilityType::Unknown; return PluginCapabilityType::Unknown;
} }
@@ -1,115 +0,0 @@
#include "PluginResourceTextures.hpp"
#include "slic3r/plugin/PluginDescriptor.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
PluginResourceTextures &PluginResourceTextures::instance()
{
static PluginResourceTextures registry;
return registry;
}
std::optional<ResourceTextureEntry> PluginResourceTextures::build_entry(const PluginCapabilityId &id) const
{
// Query by id and re-check state; the manager copies the shared_ptr and releases its lock.
auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false);
if (!capability || !capability->is_enabled() || capability->type() != PluginCapabilityType::ResourceTexture)
return std::nullopt;
auto texture_capability = std::dynamic_pointer_cast<ResourceTexturePluginCapability>(capability);
if (!texture_capability)
return std::nullopt;
// An empty directory means resolution failed at load, so the pack contributes nothing.
const std::string &texture_dir = texture_capability->texture_dir();
if (texture_dir.empty())
return std::nullopt;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor_for_capability(capability->name(),
PluginCapabilityType::ResourceTexture,
descriptor))
return std::nullopt;
ResourceTextureEntry entry;
entry.id = capability->identity();
entry.plugin_key = entry.id.plugin_key;
entry.display_name = capability->name();
entry.texture_dir = texture_dir;
entry.plugin_root = descriptor.plugin_root;
return entry;
}
void PluginResourceTextures::on_plugin_register(const std::string &plugin_key)
{
for (const auto &capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::ResourceTexture)) {
if (capability)
on_cap_register(capability->identity());
}
}
void PluginResourceTextures::on_plugin_deregister(const std::string &plugin_key)
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_entries.begin(); it != m_entries.end();) {
if (it->first.plugin_key == plugin_key)
it = m_entries.erase(it);
else
++it;
}
}
void PluginResourceTextures::on_cap_register(const PluginCapabilityId &id)
{
std::optional<ResourceTextureEntry> entry = build_entry(id);
std::lock_guard<std::mutex> lock(m_mutex);
if (entry)
m_entries[entry->id] = std::move(*entry);
}
void PluginResourceTextures::on_cap_deregister(const PluginCapabilityId &id)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_entries.erase(id);
}
void PluginResourceTextures::initial_sweep()
{
std::map<PluginCapabilityId, ResourceTextureEntry> entries;
for (const auto &capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::ResourceTexture)) {
if (!capability)
continue;
std::optional<ResourceTextureEntry> entry = build_entry(capability->identity());
if (entry)
entries[entry->id] = std::move(*entry);
}
std::lock_guard<std::mutex> lock(m_mutex);
m_entries = std::move(entries);
}
std::vector<ResourceTextureEntry> PluginResourceTextures::entries() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<ResourceTextureEntry> result;
result.reserve(m_entries.size());
for (const auto &entry : m_entries)
result.push_back(entry.second);
return result;
}
} // namespace Slic3r
@@ -1,50 +0,0 @@
#pragma once
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r {
// One ResourceTexture capability contributing a texture directory to the texture picker.
struct ResourceTextureEntry
{
PluginCapabilityId id;
std::string plugin_key;
std::string display_name;
std::string texture_dir;
std::string plugin_root; // so a scan can re-check containment
};
// Tracks every loaded and enabled ResourceTexture capability by id. Holds strings only: a
// shared_ptr here would keep a Python object alive past Py_Finalize.
class PluginResourceTextures
{
public:
static PluginResourceTextures &instance();
PluginResourceTextures(const PluginResourceTextures &) = delete;
PluginResourceTextures &operator=(const PluginResourceTextures &) = delete;
void on_plugin_register(const std::string &plugin_key);
void on_plugin_deregister(const std::string &plugin_key);
void on_cap_register(const PluginCapabilityId &id);
void on_cap_deregister(const PluginCapabilityId &id);
void initial_sweep();
std::vector<ResourceTextureEntry> entries() const;
private:
PluginResourceTextures() = default;
std::optional<ResourceTextureEntry> build_entry(const PluginCapabilityId &id) const;
mutable std::mutex m_mutex;
std::map<PluginCapabilityId, ResourceTextureEntry> m_entries;
};
} // namespace Slic3r
@@ -1,32 +0,0 @@
#include "ResourceTexturePluginCapability.hpp"
#include "ResourceTexturePluginCapabilityTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <boost/log/trivial.hpp>
#include <memory>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
namespace py = pybind11;
namespace Slic3r {
void ResourceTexturePluginCapability::RegisterBindings(pybind11::module_& module)
{
BOOST_LOG_TRIVIAL(debug) << "Registering orca.resource_texture bindings";
auto resource_texture = module.def_submodule("resource_texture", "Plugin resource texture API");
py::class_<ResourceTexturePluginCapability, PluginCapabilityInterface,
PyResourceTexturePluginCapabilityTrampoline,
std::shared_ptr<ResourceTexturePluginCapability>>(resource_texture,
"ResourceTexturePluginCapabilityBase")
.def(py::init<>())
.def("get_type", &ResourceTexturePluginCapability::get_type)
.def("get_texture_dir", &ResourceTexturePluginCapability::get_texture_dir);
}
} // namespace Slic3r
@@ -1,28 +0,0 @@
#pragma once
#include "../../PythonPluginInterface.hpp"
#include "pybind11/pybind11.h"
#include <string>
#include <utility>
namespace Slic3r {
class ResourceTexturePluginCapability : public PluginCapabilityInterface
{
public:
static void RegisterBindings(pybind11::module_& module);
PluginCapabilityType get_type() const override { return PluginCapabilityType::ResourceTexture; }
// Python contract: the directory the host scans for textures. Resolved once at materialization
// under the GIL; never called from the GUI. Overridden in Python through the trampoline.
virtual std::string get_texture_dir() const = 0;
// C++-only read of the validated directory cached by the loader; empty if resolution failed.
const std::string& texture_dir() const { return m_texture_dir; }
void set_texture_dir(std::string dir) { m_texture_dir = std::move(dir); }
private:
std::string m_texture_dir;
};
} // namespace Slic3r
@@ -1,30 +0,0 @@
#pragma once
#include "ResourceTexturePluginCapability.hpp"
#include "../../PyPluginTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <string>
#include "slic3r/plugin/PythonPluginInterface.hpp"
namespace Slic3r {
class PyResourceTexturePluginCapabilityTrampoline :
public PyPluginCommonTrampoline<ResourceTexturePluginCapability>
{
public:
using PyPluginCommonTrampoline<ResourceTexturePluginCapability>::PyPluginCommonTrampoline;
std::string get_texture_dir() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {},
PYBIND11_OVERRIDE_PURE,
std::string,
ResourceTexturePluginCapability,
get_texture_dir);
}
};
} // namespace Slic3r
-2
View File
@@ -32,7 +32,6 @@ add_executable(${_TEST_NAME}_tests
test_printer_agent.cpp test_printer_agent.cpp
test_plugin_install.cpp test_plugin_install.cpp
test_plugin_lifecycle.cpp test_plugin_lifecycle.cpp
test_plugin_resource_texture.cpp
test_plugin_printer_agent.cpp test_plugin_printer_agent.cpp
test_printhost.cpp test_printhost.cpp
test_slicing_pipeline_bindings.cpp test_slicing_pipeline_bindings.cpp
@@ -45,7 +44,6 @@ add_executable(${_TEST_NAME}_tests
test_texture_color_palette.cpp test_texture_color_palette.cpp
test_texture_island_view.cpp test_texture_island_view.cpp
test_file_url.cpp test_file_url.cpp
test_imgui_wrapper.cpp
test_user_manager.cpp test_user_manager.cpp
../fff_print/test_helpers.cpp ../fff_print/test_helpers.cpp
) )
@@ -1,6 +1,5 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <slic3r/GUI/DeviceCore/DevManager.h> #include <slic3r/GUI/DeviceCore/DevManager.h>
#include <slic3r/GUI/DeviceManager.hpp> #include <slic3r/GUI/DeviceManager.hpp>
@@ -17,7 +16,6 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector>
using namespace Slic3r; using namespace Slic3r;
using json = nlohmann::json; using json = nlohmann::json;
@@ -51,21 +49,6 @@ public:
AgentInfo get_agent_info() override { return m_info; } AgentInfo get_agent_info() override { return m_info; }
int send_message(std::string dev_id, std::string json_str, int, int) override
{
published_device = std::move(dev_id);
published_message = json::parse(json_str);
return 0;
}
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{
return send_message(std::move(dev_id), std::move(json_str), qos, flag);
}
std::string published_device;
json published_message;
private: private:
AgentInfo m_info; AgentInfo m_info;
}; };
@@ -177,62 +160,3 @@ TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership
CHECK(manager.get_my_machine_list("integration-agent-a").empty()); CHECK(manager.get_my_machine_list("integration-agent-a").empty());
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1); CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
} }
TEST_CASE("AMS filament settings keep legacy single-color commands compatible", "[DeviceManager][AMSMaterialsSetting][integration]")
{
ScopedAppConfig app_config;
auto agent = std::make_shared<TestPrinterAgent>("integration-agent");
NetworkAgent network(nullptr, agent);
DeviceManager manager(&network, false, &app_config.config);
MachineObject object(&manager, &network, "Test printer", "integration-device", "192.0.2.10");
object.dev_connection_type = GENERATE("lan", "cloud");
REQUIRE(object.command_ams_filament_settings(1, 2, "filament-id", "setting-id", "FF000080", "PLA", 190, 220) == 0);
const json& payload = agent->published_message.at("print");
CHECK(agent->published_device == object.get_dev_id());
CHECK(payload.at("command") == "ams_filament_setting");
CHECK(payload.at("ams_id") == 1);
CHECK(payload.at("slot_id") == 2);
CHECK(payload.at("tray_color") == "FF000080");
CHECK_FALSE(payload.contains("cols"));
CHECK_FALSE(payload.contains("ctype"));
}
TEST_CASE("AMS filament settings preserve ordered gradient and multicolor RGBA components", "[DeviceManager][AMSMaterialsSetting][integration]")
{
ScopedAppConfig app_config;
auto agent = std::make_shared<TestPrinterAgent>("integration-agent");
NetworkAgent network(nullptr, agent);
DeviceManager manager(&network, false, &app_config.config);
MachineObject object(&manager, &network, "Test printer", "integration-device", "192.0.2.10");
object.dev_connection_type = GENERATE("lan", "cloud");
const int color_type = GENERATE(0, 1);
const std::vector<std::string> colors{"FF000080", "0000FF40", "00FF00FF"};
REQUIRE(object.command_ams_filament_settings(1, 2, "filament-id", "setting-id", colors.front(), "PLA", 190, 220,
colors, color_type) == 0);
const json& payload = agent->published_message.at("print");
CHECK(payload.at("tray_color") == colors.front());
CHECK(payload.at("cols").get<std::vector<std::string>>() == colors);
CHECK(payload.at("ctype") == color_type);
}
TEST_CASE("AMS filament settings default a supplied single-color list to type two", "[DeviceManager][AMSMaterialsSetting][integration]")
{
ScopedAppConfig app_config;
auto agent = std::make_shared<TestPrinterAgent>("integration-agent");
NetworkAgent network(nullptr, agent);
DeviceManager manager(&network, false, &app_config.config);
MachineObject object(&manager, &network, "Test printer", "integration-device", "192.0.2.10");
object.dev_connection_type = GENERATE("lan", "cloud");
const std::vector<std::string> colors{"FF000080"};
REQUIRE(object.command_ams_filament_settings(1, 2, "filament-id", "setting-id", colors.front(), "PLA", 190, 220,
colors) == 0);
const json& payload = agent->published_message.at("print");
CHECK(payload.at("cols").get<std::vector<std::string>>() == colors);
CHECK(payload.at("ctype") == 2);
}
-61
View File
@@ -1,61 +0,0 @@
// How ImGuiWrapper turns wx mouse events into ImGui input. ImGuiWrapper lives in libslic3r_gui; this is
// the suite that links it. Same Windows include prologue as test_texture_color_palette.cpp (wx pulls in
// <windows.h>; keep WIN32_LEAN_AND_MEAN / NOMINMAX ahead of it).
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <imgui/imgui.h>
#include <wx/event.h>
#include "slic3r/GUI/ImGuiWrapper.hpp"
using Catch::Matchers::WithinAbs;
using Slic3r::GUI::ImGuiWrapper;
namespace {
// ImGui's wheel after one wheel event, as wx on macOS builds it: a delta of 10 and the rotation in whole
// points of trackpad travel.
float wheel_after(int rotation, wxMouseWheelAxis axis)
{
ImGuiWrapper imgui;
imgui.set_display_size(800.f, 600.f); // update_mouse_data() ignores input until the display has a size
wxMouseEvent evt(wxEVT_MOUSEWHEEL);
evt.m_wheelRotation = rotation;
evt.m_wheelDelta = 10;
evt.m_wheelAxis = axis;
imgui.update_mouse_data(evt);
return ImGui::GetIO().MouseWheel;
}
} // namespace
TEST_CASE("a vertical wheel event steps ImGui's wheel in its own direction", "[ImGuiWrapper]")
{
const int rotation = GENERATE(1, 7, 120, -1, -7, -120);
CHECK_THAT(wheel_after(rotation, wxMOUSE_WHEEL_VERTICAL), WithinAbs(rotation > 0 ? 1.0 : -1.0, 0.0));
}
// Both kinds arrive from a macOS trackpad while scrolling; see ImGuiWrapper::update_mouse_data().
TEST_CASE("a wheel event with no rotation or on the horizontal axis does not step ImGui's wheel", "[ImGuiWrapper][Regression]")
{
const auto [rotation, axis] = GENERATE(table<int, wxMouseWheelAxis>({
{ 0, wxMOUSE_WHEEL_VERTICAL },
{ 5, wxMOUSE_WHEEL_HORIZONTAL },
{ -5, wxMOUSE_WHEEL_HORIZONTAL },
}));
CHECK_THAT(wheel_after(rotation, axis), WithinAbs(0.0, 0.0));
}
@@ -1,941 +0,0 @@
// ResourceTexture plugin type: directory validation, the host registry's lifecycle, the texture
// listing's plugin section, hostile get_texture_dir() overrides, decode parity and the .3mf round
// trip without the pack installed.
//
// The GUI wiring lives in GUI_App::init_plugin_gui_wiring; the test binary has no app, so the same
// registry callbacks are installed here. As in test_plugin_lifecycle.cpp, each Catch2 test case runs
// in its own process (catch_discover_tests), so the PluginManager and interpreter come up at most once
// per test.
//
// Same Windows include prologue as test_texture_color_palette.cpp: wx pulls in <windows.h>, so keep
// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers.
#include "test_utils.hpp"
#include <slic3r/plugin/PluginDescriptor.hpp>
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <boost/filesystem.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/system/error_code.hpp>
#include <catch2/catch_all.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <initializer_list>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <vector>
#include <wx/image.h>
#include "libslic3r/Config.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Semver.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Zipper.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include "slic3r/plugin/PythonPluginInterface.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include "plugin_test_utils.hpp"
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
// Brings the plugin system up, and tears it down explicitly at the end of the test, as
// test_plugin_lifecycle.cpp does. Declare this FIRST in a test so it is destroyed last.
struct ScopedPluginManager
{
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log.
ScopedDataDir python_data_dir{"texture-python"};
bool initialized = false;
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
~ScopedPluginManager()
{
PluginManager::instance().shutdown();
PythonInterpreter::instance().shutdown();
}
};
// The app installs these in GUI_App::init_plugin_gui_wiring. Without an app the callbacks run inline
// on the load worker, which is what lets the tests observe the registry right after the wait.
void install_resource_texture_wiring()
{
static bool installed = false;
if (installed)
return;
installed = true;
PluginManager& manager = PluginManager::instance();
manager.subscribe_on_load_callback([](const std::string& plugin_key) {
PluginResourceTextures::instance().on_plugin_register(plugin_key);
GUI::bump_texture_library_generation();
});
manager.subscribe_on_unload_callback([](const std::string& plugin_key) {
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
GUI::bump_texture_library_generation();
});
manager.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
PluginResourceTextures::instance().on_cap_register(capability);
GUI::bump_texture_library_generation();
});
manager.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
PluginResourceTextures::instance().on_cap_deregister(capability);
GUI::bump_texture_library_generation();
});
}
// A minimal resource-texture plugin. `display_name` is the PEP723 package name and, unless
// `capability_name` is given, the capability's get_name() too; pass `capability_name` to keep the
// package name and the capability name independent.
std::string make_resource_texture_source(const std::string& display_name,
const std::string& get_texture_dir_body,
const std::string& capability_name = "")
{
const std::string cap_name = capability_name.empty() ? display_name : capability_name;
return std::string{}
+ "# /// script\n"
+ "# requires-python = \">=3.12\"\n"
+ "# dependencies = []\n"
+ "#\n"
+ "# [tool.orcaslicer.plugin]\n"
+ "# name = \"" + display_name + "\"\n"
+ "# description = \"Resource texture pack fixture.\"\n"
+ "# author = \"tests\"\n"
+ "# version = \"1.0.0\"\n"
+ "# type = \"resource-texture\"\n"
+ "# ///\n"
+ "import os\n"
+ "import orca\n"
+ "\n\n"
+ "class PackTextures(orca.resource_texture.ResourceTexturePluginCapabilityBase):\n"
+ " def get_name(self):\n"
+ " return \"" + cap_name + "\"\n"
+ "\n"
+ " def get_texture_dir(self):\n"
+ " " + get_texture_dir_body + "\n"
+ "\n\n"
+ "@orca.plugin\n"
+ "class TexturePack(orca.base):\n"
+ " def register_capabilities(self):\n"
+ " orca.register_capability(PackTextures)\n";
}
std::string default_texture_dir_body()
{
return "return os.path.join(os.path.dirname(__file__), \"textures\")";
}
// Writes {data_dir}/orca_plugins/<stem>/<stem>.py and returns the plugin directory.
fs::path write_plugin(const ScopedDataDir& data_dir_guard, const std::string& stem, const std::string& source)
{
const fs::path plugin_dir = data_dir_guard.plugins_dir() / stem;
fs::create_directories(plugin_dir);
std::ofstream out((plugin_dir / (stem + ".py")).string(), std::ios::binary);
out << source;
out.close();
return plugin_dir;
}
bool load_and_wait(PluginManager& manager, const std::string& plugin_key, std::string& error)
{
manager.load_plugin(plugin_key, /*skip_deps=*/true);
return manager.wait_for_plugin_load(plugin_key, std::chrono::seconds(120), error);
}
// The Plugin-sourced slice of the texture listing, which is exactly what the picker's third heading
// draws. Pass force to call texture_library(true); without it the listing is reused unless a pack
// callback moved the generation.
std::vector<GUI::TextureLibraryEntry> plugin_textures(bool force)
{
std::vector<GUI::TextureLibraryEntry> found;
for (const GUI::TextureLibraryEntry& entry : GUI::texture_library(force))
if (entry.source == GUI::TextureSource::Plugin)
found.push_back(entry);
return found;
}
void write_text_file(const fs::path& path, const std::string& content = "x")
{
fs::create_directories(path.parent_path());
std::ofstream out(path.string(), std::ios::binary);
out << content;
}
std::vector<unsigned char> read_file_bytes(const fs::path& path)
{
std::ifstream ifs(path.string(), std::ios::binary);
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
void write_bytes(const fs::path& path, const std::vector<unsigned char>& bytes)
{
fs::create_directories(path.parent_path());
std::ofstream out(path.string(), std::ios::binary);
out.write(reinterpret_cast<const char*>(bytes.data()), std::streamsize(bytes.size()));
}
void write_gray_png(const fs::path& path, uint8_t value, size_t w = 4, size_t h = 4)
{
fs::create_directories(path.parent_path());
const std::vector<uint8_t> pixels(w * h, value);
REQUIRE(Slic3r::png::write_gray_to_file(path.string(), w, h, pixels));
}
// A 24-bit gray BMP (R == G == B), synthesised without an image toolkit. Rows are bottom-up and
// padded to four bytes; the image is uniform, so the orientation does not matter.
std::vector<unsigned char> make_gray_bmp(int width, int height, uint8_t value)
{
const int row_bytes = ((width * 3 + 3) / 4) * 4;
const uint32_t pixel_bytes = uint32_t(row_bytes) * uint32_t(height);
const uint32_t file_size = 14 + 40 + pixel_bytes;
std::vector<unsigned char> bmp(file_size, 0);
const auto put16 = [&bmp](size_t at, uint16_t v) {
bmp[at] = uint8_t(v);
bmp[at + 1] = uint8_t(v >> 8);
};
const auto put32 = [&bmp](size_t at, uint32_t v) {
bmp[at] = uint8_t(v);
bmp[at + 1] = uint8_t(v >> 8);
bmp[at + 2] = uint8_t(v >> 16);
bmp[at + 3] = uint8_t(v >> 24);
};
bmp[0] = 'B';
bmp[1] = 'M';
put32(2, file_size);
put32(10, 54); // pixel data offset
put32(14, 40); // BITMAPINFOHEADER size
put32(18, uint32_t(width));
put32(22, uint32_t(height));
put16(26, 1); // planes
put16(28, 24); // bits per pixel
put32(34, pixel_bytes);
for (int y = 0; y < height; ++y) {
unsigned char* row = bmp.data() + 54 + size_t(y) * size_t(row_bytes);
for (int x = 0; x < width; ++x) {
row[x * 3] = value;
row[x * 3 + 1] = value;
row[x * 3 + 2] = value;
}
}
return bmp;
}
// The app calls wxInitAllImageHandlers() during startup and, before that, wxEntryStart() runs the
// wxImageModule, which registers the standard handlers. wxInitAllImageHandlers() adds PNG, JPEG and
// the rest but NOT the BMP handler, so a test binary without a wxApp must add the standard set too or
// a .bmp texture cannot be loaded here at all.
void ensure_wx_image_handlers()
{
static bool initialized = false;
if (initialized)
return;
initialized = true;
wxImage::InitStandardHandlers();
wxInitAllImageHandlers();
}
// Stores `model` as a one-plate project, mirroring tests/libslic3r/test_3mf.cpp.
void store_project(Model& model, const std::string& path)
{
ScopedTemporaryDir backup_dir("orca_tex_pack_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));
}
bool load_project(const std::string& path, Model& model, const ScopedTemporaryDir& backup_dir)
{
model.set_backup_path(backup_dir.string());
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
PlateDataPtrs plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false;
bool is_orca_3mf = false;
Semver file_version;
const bool loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates, &project_presets,
&is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
release_PlateData_list(plates);
return loaded;
}
} // namespace
// ── Validation (collect_resource_texture_files, no Python) ──────────────────────────────────────
TEST_CASE("A texture file directly inside the plugin folder is listed", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-root");
write_text_file(root.path() / "textures" / "grain.png");
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files((root.path() / "textures").string(), root.string(), error);
CHECK(error.empty());
REQUIRE(files.size() == 1);
CHECK(fs::path(files.front()).filename() == "grain.png");
}
TEST_CASE("A directory reached with a parent-directory escape is refused", "[PluginResourceTexture]")
{
ScopedTemporaryDir parent("tex-escape");
const fs::path root = parent.path() / "pack";
const fs::path outside = parent.path() / "outside";
fs::create_directories(root);
write_text_file(outside / "leak.png");
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files((root / ".." / "outside").string(), root.string(), error);
CHECK(files.empty());
CHECK_FALSE(error.empty());
}
TEST_CASE("An absolute directory outside the plugin folder is refused", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-abs-root");
ScopedTemporaryDir elsewhere("tex-abs-elsewhere");
write_text_file(elsewhere.path() / "leak.png");
std::string error;
const std::vector<std::string> files = collect_resource_texture_files(elsewhere.string(), root.string(), error);
CHECK(files.empty());
CHECK_FALSE(error.empty());
}
TEST_CASE("A symlink pointing outside the plugin folder is skipped, not reported as an error", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-symlink-root");
ScopedTemporaryDir target("tex-symlink-target");
write_text_file(target.path() / "secret.png");
const fs::path link = root.path() / "textures" / "link.png";
fs::create_directories(link.parent_path());
boost::system::error_code ec;
fs::create_symlink(target.path() / "secret.png", link, ec);
if (ec)
SKIP("symlinks unavailable on this filesystem: " + ec.message());
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files(link.parent_path().string(), root.string(), error);
// The directory itself is legitimate; the escaping link is dropped, not an error.
CHECK(error.empty());
CHECK(files.empty());
}
TEST_CASE("Files in a subdirectory of the texture directory are not listed", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-recursive");
write_text_file(root.path() / "top.png");
write_text_file(root.path() / "nested" / "deep.png");
std::string error;
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
CHECK(error.empty());
REQUIRE(files.size() == 1);
CHECK(fs::path(files.front()).filename() == "top.png");
}
TEST_CASE("An entry that is not a regular file is skipped", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-nonregular");
write_text_file(root.path() / "real.png");
fs::create_directories(root.path() / "fake.png"); // a directory named like an image
std::string error;
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
CHECK(error.empty());
REQUIRE(files.size() == 1);
CHECK(fs::path(files.front()).filename() == "real.png");
}
TEST_CASE("Image extensions are matched case-insensitively and other extensions are ignored",
"[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-extensions");
for (const char* name : { "a.png", "b.PNG", "c.jpg", "d.JPG", "e.jpeg", "f.JPEG", "g.bmp", "h.BMP" })
write_text_file(root.path() / name);
for (const char* name : { "i.txt", "j.gif", "k.pngx", "l.pn", "m.png.bak" })
write_text_file(root.path() / name);
std::string error;
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
CHECK(error.empty());
CHECK(files.size() == 8);
std::vector<std::string> names;
for (const std::string& file : files)
names.push_back(fs::path(file).filename().string());
for (const char* accepted : { "a.png", "b.PNG", "c.jpg", "d.JPG", "e.jpeg", "f.JPEG", "g.bmp", "h.BMP" })
CHECK(std::find(names.begin(), names.end(), accepted) != names.end());
for (const char* rejected : { "i.txt", "j.gif", "k.pngx", "l.pn", "m.png.bak" })
CHECK(std::find(names.begin(), names.end(), rejected) == names.end());
}
TEST_CASE("The listing stops at 64 image files", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-cap");
for (int i = 0; i < 70; ++i)
write_text_file(root.path() / ("file" + std::to_string(i) + ".png"));
std::string error;
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
CHECK(error.empty());
CHECK(files.size() == 64);
}
TEST_CASE("A missing texture directory yields no files and an error", "[PluginResourceTexture]")
{
ScopedTemporaryDir root("tex-missing");
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files((root.path() / "textures").string(), root.string(), error);
CHECK(files.empty());
CHECK_FALSE(error.empty());
}
// ── Lifecycle through the real listing ──────────────────────────────────────────────────────────
TEST_CASE("A resource-texture pack's images appear in the listing and clear on disable and unload",
"[PluginResourceTexture][PluginLifecycle][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-listing");
const std::string plugin_key = "Texture_Pack";
const fs::path plugin_dir =
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Texture Pack", default_texture_dir_body()));
const fs::path textures = plugin_dir / "textures";
write_gray_png(textures / "alpha.png", 0x20);
write_gray_png(textures / "beta.png", 0x80);
write_text_file(textures / "gamma.jpg"); // the listing only checks the extension; decode is covered below
write_text_file(textures / "notes.txt");
// A user-owned texture, so the listing has a non-pack entry whose provenance must be empty.
write_gray_png(fs::path(GUI::user_texture_dir()) / "user_made.png", 0x40);
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, plugin_key, error));
INFO("load error: " << error);
CHECK(error.empty());
CHECK(PluginResourceTextures::instance().entries().size() == 1);
const uint64_t loaded_generation = GUI::texture_library_generation();
const auto loaded = plugin_textures(true);
REQUIRE(loaded.size() == 3);
std::vector<std::string> names;
for (const GUI::TextureLibraryEntry& entry : loaded)
names.push_back(entry.name);
CHECK(std::find(names.begin(), names.end(), "alpha") != names.end());
CHECK(std::find(names.begin(), names.end(), "beta") != names.end());
CHECK(std::find(names.begin(), names.end(), "gamma") != names.end());
CHECK(std::find(names.begin(), names.end(), "notes") == names.end());
// Every pack entry names its contributing pack; shipped and user entries carry no pack.
for (const GUI::TextureLibraryEntry& entry : GUI::texture_library(true)) {
if (entry.source == GUI::TextureSource::Plugin) {
CHECK_FALSE(entry.pack_name.empty());
CHECK(entry.pack_name == "Texture Pack");
} else {
CHECK(entry.pack_name.empty());
}
}
// A no-argument call reuses the cached listing until a pack callback moves the generation.
CHECK(plugin_textures(false).size() == 3);
manager.set_capability_enabled({ PluginCapabilityType::ResourceTexture, "Texture Pack", plugin_key }, false);
CHECK(GUI::texture_library_generation() > loaded_generation);
CHECK(PluginResourceTextures::instance().entries().empty());
CHECK(plugin_textures(false).empty());
manager.set_capability_enabled({ PluginCapabilityType::ResourceTexture, "Texture Pack", plugin_key }, true);
CHECK(plugin_textures(false).size() == 3);
REQUIRE(manager.unload_plugin(plugin_key));
CHECK(PluginResourceTextures::instance().entries().empty());
CHECK(plugin_textures(false).empty());
}
TEST_CASE("Two distinctly named packs each list as one contiguous run",
"[PluginResourceTexture][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-contiguous");
// Loaded out of name order, so it is the listing's pack-name sort that orders the runs.
const std::string key_zeta = "Pack_Zeta";
const std::string key_alpha = "Pack_Alpha";
const fs::path dir_zeta = write_plugin(
data_dir_guard, key_zeta, make_resource_texture_source("Zeta Pack", default_texture_dir_body()));
const fs::path dir_alpha = write_plugin(
data_dir_guard, key_alpha, make_resource_texture_source("Alpha Pack", default_texture_dir_body()));
write_gray_png(dir_zeta / "textures" / "zeta_one.png", 0x20);
write_gray_png(dir_zeta / "textures" / "zeta_two.png", 0x40);
write_gray_png(dir_alpha / "textures" / "alpha_one.png", 0x60);
write_gray_png(dir_alpha / "textures" / "alpha_two.png", 0x80);
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, key_zeta, error));
REQUIRE(load_and_wait(manager, key_alpha, error));
std::vector<GUI::TextureLibraryEntry> pack_entries;
for (const GUI::TextureLibraryEntry& entry : GUI::texture_library(true))
if (entry.source == GUI::TextureSource::Plugin)
pack_entries.push_back(entry);
REQUIRE(pack_entries.size() == 4);
const auto count_pack = [&pack_entries](const std::string& name) {
return std::count_if(pack_entries.begin(), pack_entries.end(),
[&name](const GUI::TextureLibraryEntry& e) { return e.pack_name == name; });
};
CHECK(count_pack("Zeta Pack") == 2);
CHECK(count_pack("Alpha Pack") == 2);
// Collapsing consecutive entries by pack name must leave one run per pack: a name never reappears
// after another name intervenes, which is what lets the picker draw one section per pack.
std::vector<std::string> runs;
for (const GUI::TextureLibraryEntry& entry : pack_entries)
if (runs.empty() || runs.back() != entry.pack_name)
runs.push_back(entry.pack_name);
std::vector<std::string> sorted_runs = runs;
std::sort(sorted_runs.begin(), sorted_runs.end());
CHECK(std::unique(sorted_runs.begin(), sorted_runs.end()) == sorted_runs.end());
CHECK(runs == std::vector<std::string>({ "Alpha Pack", "Zeta Pack" }));
manager.unload_plugin(key_zeta);
manager.unload_plugin(key_alpha);
}
TEST_CASE("A second pack declaring a loaded capability name is refused",
"[PluginResourceTexture][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-capability-clash");
// Distinct package names, one capability name: the loader allows only the first.
const std::string key_first = "Clash_First";
const std::string key_second = "Clash_Second";
const std::string capability = "Shared Textures";
const fs::path dir_first = write_plugin(
data_dir_guard, key_first,
make_resource_texture_source("Clash Package One", default_texture_dir_body(), capability));
write_plugin(data_dir_guard, key_second,
make_resource_texture_source("Clash Package Two", default_texture_dir_body(), capability));
write_gray_png(dir_first / "textures" / "one.png", 0x20);
write_gray_png(dir_first / "textures" / "two.png", 0x40);
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, key_first, error));
REQUIRE(manager.is_plugin_loaded(key_first));
const std::vector<GUI::TextureLibraryEntry> before = plugin_textures(true);
REQUIRE(before.size() == 2);
for (const GUI::TextureLibraryEntry& entry : before)
CHECK(entry.pack_name == capability);
// The second package claims the capability name the first already owns, so the load is refused.
std::string second_error;
CHECK_FALSE(load_and_wait(manager, key_second, second_error));
CHECK_FALSE(manager.is_plugin_loaded(key_second));
// The first pack's textures are untouched by the refused load.
const std::vector<GUI::TextureLibraryEntry> after = plugin_textures(true);
REQUIRE(after.size() == before.size());
for (size_t i = 0; i < before.size(); ++i) {
CHECK(after[i].name == before[i].name);
CHECK(after[i].path == before[i].path);
CHECK(after[i].pack_name == before[i].pack_name);
}
manager.unload_plugin(key_first);
}
TEST_CASE("A resource-texture pack with no images contributes no textures", "[PluginResourceTexture][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-empty");
const std::string plugin_key = "Empty_Pack";
const fs::path plugin_dir =
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Empty Pack", default_texture_dir_body()));
fs::create_directories(plugin_dir / "textures"); // valid, but empty
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, plugin_key, error));
CHECK(manager.is_plugin_loaded(plugin_key));
CHECK(plugin_textures(true).empty());
manager.unload_plugin(plugin_key);
}
// ── Hostile get_texture_dir() overrides ─────────────────────────────────────────────────────────
TEST_CASE("A texture directory outside the plugin root contributes no textures", "[PluginResourceTexture][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-hostile");
ScopedResourcesDir resources_dir_guard("texture-hostile-res");
write_text_file(fs::path(resources_dir_guard.dir) / "sentinel.png"); // would list if it were scanned
const std::string data_pack = "Data_Dir_Pack";
write_plugin(data_dir_guard, data_pack,
make_resource_texture_source("Data Dir Pack", "return r\"" + data_dir_guard.dir.string() + "\""));
const std::string resources_pack = "Resources_Dir_Pack";
write_plugin(data_dir_guard, resources_pack,
make_resource_texture_source("Resources Dir Pack", "return r\"" + resources_dir_guard.dir.string() + "\""));
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, data_pack, error));
REQUIRE(load_and_wait(manager, resources_pack, error));
// The packs load; their out-of-root directories are simply refused.
CHECK(manager.is_plugin_loaded(data_pack));
CHECK(manager.is_plugin_loaded(resources_pack));
CHECK(PluginResourceTextures::instance().entries().empty());
CHECK(plugin_textures(true).empty());
manager.unload_plugin(data_pack);
manager.unload_plugin(resources_pack);
}
TEST_CASE("A raising get_texture_dir leaves the pack loaded with no textures", "[PluginResourceTexture][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-raising");
const std::string plugin_key = "Raising_Pack";
write_plugin(data_dir_guard, plugin_key,
make_resource_texture_source("Raising Pack", "raise RuntimeError(\"no textures today\")"));
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, plugin_key, error));
CHECK(error.empty());
CHECK(manager.is_plugin_loaded(plugin_key));
CHECK(PluginResourceTextures::instance().entries().empty());
CHECK(plugin_textures(true).empty());
manager.unload_plugin(plugin_key);
}
// ── Decoding parity ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("Pack images decode through the same path as hand-picked files", "[PluginResourceTexture]")
{
ensure_wx_image_handlers();
ScopedTemporaryDir pack_root("tex-pack");
ScopedTemporaryDir hand_root("tex-hand");
const fs::path pack_dir = pack_root.path() / "textures";
fs::create_directories(pack_dir);
// PNG: grayscale, so load_texture_image_data passes it through verbatim.
const fs::path pack_png = pack_dir / "grain.png";
write_gray_png(pack_png, 0x40);
const std::vector<unsigned char> png_bytes = read_file_bytes(pack_png);
REQUIRE_FALSE(png_bytes.empty());
write_bytes(hand_root.path() / "grain.png", png_bytes);
// BMP: not a PNG, so it goes through wxImage and is re-encoded; the same file must give the same
// bytes wherever it sits.
const std::vector<unsigned char> bmp_bytes = make_gray_bmp(4, 4, 0x40);
const fs::path pack_bmp = pack_dir / "bump.bmp";
write_bytes(pack_bmp, bmp_bytes);
const fs::path hand_bmp = hand_root.path() / "bump.bmp";
write_bytes(hand_bmp, bmp_bytes);
// The collector lists both pack files, so these are the paths a pack would serve.
std::string collect_error;
const std::vector<std::string> pack_files =
collect_resource_texture_files(pack_dir.string(), pack_root.string(), collect_error);
REQUIRE(collect_error.empty());
CHECK(pack_files.size() == 2);
std::string pack_error;
std::string hand_error;
const auto pack_png_decoded = GUI::load_texture_image_data(pack_png.string(), pack_error);
const auto hand_png_decoded = GUI::load_texture_image_data((hand_root.path() / "grain.png").string(), hand_error);
REQUIRE(pack_png_decoded != nullptr);
REQUIRE(hand_png_decoded != nullptr);
CHECK(*pack_png_decoded == png_bytes); // supported PNG is passed through unchanged
CHECK(*pack_png_decoded == *hand_png_decoded);
const auto pack_bmp_decoded = GUI::load_texture_image_data(pack_bmp.string(), pack_error);
const auto hand_bmp_decoded = GUI::load_texture_image_data(hand_bmp.string(), hand_error);
REQUIRE(pack_bmp_decoded != nullptr);
REQUIRE(hand_bmp_decoded != nullptr);
REQUIRE_FALSE(pack_bmp_decoded->empty());
CHECK(*pack_bmp_decoded == *hand_bmp_decoded);
}
TEST_CASE("Reinstalling a pack with different bytes at the same path changes the decoded content",
"[PluginResourceTexture][Python]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-reinstall");
const std::string plugin_key = "Reinstall_Pack";
const fs::path plugin_dir =
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Reinstall Pack", default_texture_dir_body()));
const fs::path texture = plugin_dir / "textures" / "grain.png";
write_gray_png(texture, 0x10);
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, plugin_key, error));
auto loaded = plugin_textures(true);
REQUIRE(loaded.size() == 1);
const std::string path = loaded.front().path;
std::string decode_error;
const auto first = GUI::load_texture_image_data(path, decode_error);
REQUIRE(first != nullptr);
REQUIRE(manager.unload_plugin(plugin_key));
// The reinstalled pack writes different bytes to the same file path.
write_gray_png(texture, 0xE0);
REQUIRE(load_and_wait(manager, plugin_key, error));
loaded = plugin_textures(true);
REQUIRE(loaded.size() == 1);
CHECK(loaded.front().path == path);
const auto second = GUI::load_texture_image_data(path, decode_error);
REQUIRE(second != nullptr);
CHECK(*first != *second);
}
// ── Wheel metadata ──────────────────────────────────────────────────────────────────────────────
TEST_CASE("A minimal resource-texture wheel is accepted", "[PluginResourceTexture][Wheel]")
{
ScopedTemporaryDir dir("texture-wheel");
const fs::path wheel = dir.path() / "texture_pack-1.0-py3-none-any.whl";
const std::string dist_info = "texture_pack-1.0.dist-info";
const std::string metadata =
"Metadata-Version: 2.1\n"
"Name: texture_pack\n"
"Version: 1.0\n"
"Summary: Resource texture pack fixture\n"
"Author: tests\n";
const std::string wheel_metadata =
"Wheel-Version: 1.0\n"
"Generator: manual\n"
"Root-Is-Purelib: true\n"
"Tag: py3-none-any\n";
const std::string record = dist_info + "/METADATA,,\n" + dist_info + "/WHEEL,,\n" + dist_info + "/RECORD,,\n";
const std::string top_level = "texture_pack\n";
{
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
zipper.add_entry(dist_info + "/METADATA", metadata.data(), metadata.size());
zipper.add_entry(dist_info + "/WHEEL", wheel_metadata.data(), wheel_metadata.size());
zipper.add_entry(dist_info + "/RECORD", record.data(), record.size());
// read_wheel_plugin_metadata leaves `error` set when an optional top_level.txt is missing.
zipper.add_entry(dist_info + "/top_level.txt", top_level.data(), top_level.size());
zipper.finalize();
}
PluginDescriptor descriptor;
std::string error;
REQUIRE(read_wheel_plugin_metadata(wheel, descriptor, error));
INFO("wheel metadata error: " << error);
CHECK(error.empty());
CHECK(descriptor.name == "texture_pack");
CHECK(descriptor.version == "1.0");
CHECK(descriptor.entry_package == "texture_pack");
}
// ── .3mf round trip ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("A project keeps a pack texture after the pack is uninstalled",
"[PluginResourceTexture][PluginLifecycle][Python][3mf]")
{
ScopedPluginManager plugin_system;
if (!plugin_system.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
ScopedDataDir data_dir_guard("texture-3mf");
const std::string plugin_key = "Texture_Pack_3mf";
const fs::path plugin_dir =
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Texture Pack", default_texture_dir_body()));
write_gray_png(plugin_dir / "textures" / "grain.png", 0x60);
PluginManager& manager = PluginManager::instance();
install_resource_texture_wiring();
manager.discover_plugins(/*async=*/false, /*clear=*/true);
std::string error;
REQUIRE(load_and_wait(manager, plugin_key, error));
const auto loaded = plugin_textures(true);
REQUIRE(loaded.size() == 1);
std::string decode_error;
const auto pack_bytes = GUI::load_texture_image_data(loaded.front().path, decode_error);
REQUIRE(pack_bytes != nullptr);
REQUIRE_FALSE(pack_bytes->empty());
// Build a project the way the gizmo would: the layer carries the pack texture's bytes.
Model model;
ModelObject* object = model.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 0;
layer.name = loaded.front().name;
layer.path = loaded.front().path;
layer.image_data = pack_bytes;
volume->texture_displacement_layers.push_back(layer);
ScopedTemporaryFile first(".3mf");
store_project(model, first.string());
// Uninstall the pack; the project still has to open and re-save with the same image bytes.
REQUIRE(manager.unload_plugin(plugin_key));
CHECK(plugin_textures(true).empty());
ScopedTemporaryDir backup_dir("orca_tex_pack_dst");
Model reloaded;
REQUIRE(load_project(first.string(), reloaded, backup_dir));
REQUIRE(reloaded.objects.size() == 1);
REQUIRE(reloaded.objects.front()->volumes.size() == 1);
const std::vector<TextureDisplacementLayer>& reloaded_layers =
reloaded.objects.front()->volumes.front()->texture_displacement_layers;
REQUIRE(reloaded_layers.size() == 1);
REQUIRE(reloaded_layers.front().image_data);
CHECK(*reloaded_layers.front().image_data == *pack_bytes);
ScopedTemporaryFile second(".3mf");
store_project(reloaded, second.string());
ScopedTemporaryDir backup_dir2("orca_tex_pack_dst2");
Model reloaded_again;
REQUIRE(load_project(second.string(), reloaded_again, backup_dir2));
REQUIRE(reloaded_again.objects.size() == 1);
REQUIRE(reloaded_again.objects.front()->volumes.size() == 1);
const std::vector<TextureDisplacementLayer>& again_layers =
reloaded_again.objects.front()->volumes.front()->texture_displacement_layers;
REQUIRE(again_layers.size() == 1);
REQUIRE(again_layers.front().image_data);
CHECK(*again_layers.front().image_data == *pack_bytes);
}