mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 20:01:31 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbaed8a512 | ||
|
|
597b2abac4 | ||
|
|
ba4de8c1fc | ||
|
|
7bce53a773 | ||
|
|
e65113af6d | ||
|
|
91ab04e823 |
@@ -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
@@ -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
|
||||||
|
|||||||
Vendored
+17
-2
@@ -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}
|
||||||
)
|
)
|
||||||
|
|||||||
+2267
-3997
File diff suppressed because it is too large
Load Diff
+3068
-5238
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -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()">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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()) {
|
||||||
|
|||||||
@@ -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 ¢er_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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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().
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(); }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
-30
@@ -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
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user