mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 20:31:14 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
638dd9f100 | ||
|
|
82aee1abdd | ||
|
|
cefdd9b609 | ||
|
|
caade80624 | ||
|
|
cbd8ce98d5 | ||
|
|
99714afc4c | ||
|
|
a72aa236c4 | ||
|
|
8baeba1341 | ||
|
|
e0a6000d50 | ||
|
|
3d07663ab2 | ||
|
|
29b6bbb8c9 | ||
|
|
8b6c074c69 | ||
|
|
109d09e0b8 | ||
|
|
b00237807d | ||
|
|
4149401c53 | ||
|
|
f6cf18fb96 | ||
|
|
4ee1b9c6e9 | ||
|
|
bc8d0a7858 | ||
|
|
e0651ec31d | ||
|
|
0caefcf4aa | ||
|
|
86905a8a20 | ||
|
|
d4fabbf7c8 | ||
|
|
51efb24752 | ||
|
|
cf9ec18df5 | ||
|
|
99de46b9d0 | ||
|
|
258f927a0f | ||
|
|
b8d40ed61f | ||
|
|
54e6df0235 | ||
|
|
79ed246d26 | ||
|
|
92b5301bf6 | ||
|
|
20c157b0f4 | ||
|
|
4cf16febf6 | ||
|
|
70e52a105f | ||
|
|
b525cb79e1 | ||
|
|
86ab598899 |
@@ -569,12 +569,18 @@ on the **first** call. So:
|
||||
#endif
|
||||
```
|
||||
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
|
||||
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
|
||||
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
|
||||
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
|
||||
paths, 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. 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
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
|
||||
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
|
||||
|
||||
**wx's own detectors**, for reference:
|
||||
|
||||
@@ -61,7 +61,8 @@ 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
|
||||
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)
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces
|
||||
`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)
|
||||
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
|
||||
@@ -567,33 +568,69 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
|
||||
## WebKitGTK on Linux sessions
|
||||
|
||||
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
|
||||
branches on `GDK_BACKEND`:
|
||||
sets the NVIDIA DMABUF workaround and then 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
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
|
||||
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
|
||||
`DISPLAY` is set.
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
|
||||
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
|
||||
default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not
|
||||
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
|
||||
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
|
||||
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).
|
||||
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
|
||||
|
||||
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
|
||||
(see `references/platforms.md`), not these environment variables.
|
||||
|
||||
**Pitfall.**
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
|
||||
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
|
||||
initialises; never unconditionally. Keep JS-side fallbacks so a page never
|
||||
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
|
||||
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
|
||||
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.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
|
||||
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
|
||||
acceleration; without the JS timeout the user is stuck when the signal is lost.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
|
||||
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
|
||||
so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
|
||||
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
|
||||
```cpp
|
||||
// Wrong: unconditional, kills HW acceleration everywhere
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
||||
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
|
||||
if (display && *display && wayland && *wayland)
|
||||
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
|
||||
// and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the
|
||||
// case that runs through XWayland
|
||||
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
|
||||
```
|
||||
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
||||
|
||||
@@ -288,6 +288,19 @@ if (APPLE)
|
||||
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
|
||||
|
||||
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
|
||||
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
|
||||
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
|
||||
# both are set. The setting takes a single program path, not a CMake launcher list.
|
||||
# Xcode versions without it ignore it and build uncached.
|
||||
if (CMAKE_GENERATOR STREQUAL "Xcode")
|
||||
if (CMAKE_CXX_COMPILER_LAUNCHER)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
|
||||
elseif (CMAKE_C_COMPILER_LAUNCHER)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
|
||||
+15
-1
@@ -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 " -h: prints this help text"
|
||||
echo " -i: build the Orca Slicer AppImage (optional)"
|
||||
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
|
||||
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
|
||||
echo " -r: skip RAM and disk checks (low RAM compiling)"
|
||||
echo " -s: build the Orca Slicer (optional)"
|
||||
echo " -t: build tests (optional), requires -s flag"
|
||||
@@ -549,6 +549,20 @@ fi
|
||||
if [ -n "${CMAKE_CCACHE}" ] ; then
|
||||
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
||||
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
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
fi
|
||||
|
||||
@@ -111,6 +111,54 @@ else
|
||||
export CMAKE_POLICY_COMPAT=""
|
||||
fi
|
||||
|
||||
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
|
||||
# launcher already set in the environment (the CI build sets ccache there), else sccache or
|
||||
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
|
||||
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
|
||||
CMAKE_CCACHE_ARGS=()
|
||||
CCACHE_VAR=""
|
||||
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
|
||||
if [ -n "${!var:-}" ]; then
|
||||
CCACHE_VAR=$var
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$CCACHE_VAR" ]; then
|
||||
CCACHE_PROGRAM=${!CCACHE_VAR}
|
||||
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
|
||||
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
|
||||
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
|
||||
CMAKE_CCACHE=""
|
||||
}
|
||||
elif command -v sccache >/dev/null 2>&1 ; then
|
||||
CMAKE_CCACHE=$(command -v sccache)
|
||||
elif command -v ccache >/dev/null 2>&1 ; then
|
||||
CMAKE_CCACHE=$(command -v ccache)
|
||||
fi
|
||||
if [ -n "${CMAKE_CCACHE}" ] ; then
|
||||
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
||||
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
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
|
||||
# than removed: CMake fills a missing launcher entry from the environment.
|
||||
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
|
||||
fi
|
||||
|
||||
echo "Build params:"
|
||||
echo " - ARCH: $ARCH"
|
||||
echo " - BUILD_CONFIG: $BUILD_CONFIG"
|
||||
@@ -162,6 +210,7 @@ function build_deps() {
|
||||
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
@@ -264,6 +313,7 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
|
||||
Vendored
+2
-17
@@ -1,22 +1,7 @@
|
||||
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
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
DEPENDS ${ZLIB_PKG}
|
||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
CMAKE_ARGS
|
||||
-DENABLE_SHARED=OFF
|
||||
-DENABLE_STATIC=ON
|
||||
${jpeg_flag}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# ChromaStack - High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
ChromaStack prints a colour picture on a single face of a model: a few filaments are printed one on
|
||||
top of another, the printer swaps filament at fixed layer boundaries, and the *height* of every
|
||||
pixel decides how many of those layers reach it. Filament is translucent, so a pixel that stops
|
||||
part-way through a run shows a blend of that run's filament and whatever is under it, which is
|
||||
where the colours come from - far more of them than there are filaments loaded.
|
||||
|
||||
This is that model inside OrcaSlicer, as a mode of a texture displacement layer. The picture and
|
||||
the filament stack are the input; the output is a grayscale height map and a set of filament swaps.
|
||||
Nothing about it is a new pipeline: the height map is handed to the layer the user painted, so the
|
||||
existing projection, preview and bake turn it into real geometry, and the swaps are handed to the
|
||||
object's height ranges, which is the slicer's own way of printing a Z band in another filament.
|
||||
|
||||
It does not import other filament-painting tools' project files, and it does not choose the filaments or
|
||||
their order - that is the user's artistic decision, and the panel exists to make its result
|
||||
visible before the print.
|
||||
|
||||
## The colour model
|
||||
|
||||
`libslic3r/ChromaStack.hpp` holds the whole of it, free of any GUI dependency, which is what lets the
|
||||
solve run against plain image bytes and be unit tested.
|
||||
|
||||
A **stack** (`ChromaStack`) is a list of **bands** (`ChromaBand`), bottom-up: a filament and
|
||||
how many layers it gets. Alongside them it carries the print's layer height, which the colours
|
||||
depend on directly, and the **base**: how many layers of solid backing the picture sits on.
|
||||
|
||||
The base is not a band. It is a measurement of the model - everything below the painted face, often
|
||||
a hundred layers or more on a tall part - while a band is a handful of layers the user chooses. It
|
||||
prints in the bottom band's filament, because that is what is loaded when the print starts, so it
|
||||
extends that band's height range down to the first layer instead of being a swap of its own. Only
|
||||
the bands are quantized into the height map, which is why the 255-layer limit
|
||||
(`CHROMA_MAX_RELIEF_LAYERS`) bounds the picture and not the stack.
|
||||
|
||||
Each filament carries its **transmission distance** (TD), the usual measure for how deep
|
||||
light gets into it: about 1 for an opaque filament, 4 to 7 for a usefully translucent one, 8 and
|
||||
above for a nearly clear one. TD lives in the filament preset, as
|
||||
`filament_transmission_distance` - it is a property of the material, so it belongs with the
|
||||
material and ships with a profile rather than being retyped per project. It has no effect on
|
||||
slicing.
|
||||
|
||||
`chroma_palette()` turns a stack into the colour it shows at every possible column height, which
|
||||
is all the solver ever needs. The model is Beer-Lambert absorption, layer by layer: a layer of
|
||||
thickness `h` in a filament of transmission distance `TD` passes
|
||||
|
||||
T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
|
||||
|
||||
of the light from under it and shows its own colour with the rest, so walking up from an opaque
|
||||
base,
|
||||
|
||||
C_k = (1 - T_k) * c_k + T_k * C_(k-1), C_0 = c_1
|
||||
|
||||
The constants say what TD means in physical terms: a filament stops showing what is behind it at
|
||||
about a tenth of its TD number (`CHROMA_TD_OPAQUE_FRACTION`), and three e-folding lengths of
|
||||
absorption pass 5% of the light, which is where the eye stops seeing through. Those are calibrated
|
||||
against how the community's TD step prints behave, not derived from first principles - TD is an
|
||||
empirical scale.
|
||||
|
||||
How fast that happens is the single most useful fact about a stack, and `chroma_layers_to_opaque()`
|
||||
is it: a filament hides what is under it at about a tenth of its TD, so how many *layers* that takes
|
||||
depends entirely on how thick they are. TD 4 covers in two layers at 0.2 mm and in five at 0.08 mm.
|
||||
Below about two layers a band has no intermediate colours at all - the picture can only use the
|
||||
filament or not use it - which is why an otherwise sound stack prints as flat steps at a coarse
|
||||
layer height. The panel reports the number under the palette ramp, and warns when it falls below
|
||||
two, because nothing else makes it visible: the filaments look fine, the bands look long enough, and
|
||||
the result looks like the colour model is wrong when it is the layer height.
|
||||
|
||||
How many of a picture's layers actually earn their place is a second question, and
|
||||
`chroma_distinct_levels()` answers it: a band eventually hides what is under it, and another layer of
|
||||
it then shows the colour the one below already showed. It is measured against the palette rather than
|
||||
against TD, because how long a band keeps paying depends on what is under it as much as on the
|
||||
filament - a light band over a dark one is finished in four or five layers, while a dark one over a
|
||||
light one goes on moving the colour perceptibly well past the depth at which it is nominally opaque,
|
||||
since the last few percent of light coming through still shifts a near-black colour. `Trim` shortens
|
||||
each band to its last layer that still changes anything.
|
||||
|
||||
Beer-Lambert is also the only model that stays self-consistent applied per layer: transmittance
|
||||
multiplies, so a run of n layers of one filament passes `exp(-n h / d)` whether it is composited in
|
||||
one step or n, and the palette therefore does not depend on how the stack happens to be cut into
|
||||
bands. A unit test asserts exactly that. Compositing is done on linear light, because the quantity
|
||||
being attenuated is light rather than an sRGB code.
|
||||
|
||||
The base is assumed opaque. That is what makes a column's colour depend only on the layers above it,
|
||||
and it is why the panel warns when the base is thin.
|
||||
|
||||
## The solve
|
||||
|
||||
`chroma_solve()` maps each pixel of the picture to the column height whose palette colour is
|
||||
closest to it, perceptually: both sides go to CIELAB and the match is a CIE76 distance. A 1-D
|
||||
palette of a few dozen entries makes the extra accuracy of CIEDE2000 hard to see and the cost of it
|
||||
easy to feel - several hundred million comparisons for a large picture - so the cheaper metric is
|
||||
the deliberate choice here, unlike the texture colour quantizer, which matches against a
|
||||
two-dimensional palette of filament mixes and uses a precomputed CIEDE2000 cube.
|
||||
|
||||
Optional Floyd-Steinberg error diffusion, serpentine, trades the bands a photograph shows across a
|
||||
sky or a cheek for fine stipple. Off by default, since stipple is the artifact rather than the cure
|
||||
for flat-coloured artwork.
|
||||
|
||||
Three things come back: the per-pixel layer count, the same thing as an 8-bit grayscale height map
|
||||
(0 at the base, 255 at the top of the stack), and the palette colours chosen, as an RGB image. The
|
||||
quantization belongs to the solve rather than to the displacement because the relief's steps have
|
||||
to land on layer boundaries, and a grey level is the only channel the displacement reads.
|
||||
|
||||
The solve also reports the mean and worst colour difference it had to accept. That is the only
|
||||
honest answer to "will this stack reproduce my picture": under about 5 is a good match, over about
|
||||
15 means the filaments cannot reach the picture's colours and no number of layers will help.
|
||||
|
||||
## How it attaches to a texture displacement layer
|
||||
|
||||
A ChromaStack layer is an ordinary `TextureDisplacementLayer` whose height map is computed rather than
|
||||
imported:
|
||||
|
||||
- `chroma_source` keeps the picture the user chose, so the solve can be redone whenever the stack,
|
||||
the layer height or the picture changes;
|
||||
- `chroma` is the stack;
|
||||
- `image_data` - the same field every other layer uses - holds whatever the last solve produced.
|
||||
|
||||
So everything downstream needs no changes at all and knows nothing about colour stacks. It also
|
||||
means the solve writes `depth_mm` (the relief is exactly as tall as the layers above the base) and
|
||||
pins `invert`, `midlevel` and `smoothing`: blurring a solved height map moves its steps off the
|
||||
layer boundaries they were solved for.
|
||||
|
||||
Colour by filament swap and the layer's own `color_enabled` (one filament per triangle, via
|
||||
`mmu_segmentation_facets`) are mutually exclusive, and the panel enforces it. They are two
|
||||
different ways of colouring the same surface, and painting the relief per triangle would hide the
|
||||
very stack the heights were solved against.
|
||||
|
||||
## The filament swaps
|
||||
|
||||
`chroma_ranges()` turns the bands into Z bands in the object's own coordinates, merging any two
|
||||
adjacent bands that share a filament, since nothing is swapped between them. The panel writes them
|
||||
onto the object as `layer_config_ranges`, each range carrying `extruder` - which
|
||||
`apply_to_print_region_config()` spreads over every feature filament the range does not otherwise
|
||||
override, i.e. the whole band prints in that filament - and `layer_height`, both because the solve
|
||||
is only valid at the height it was run at and because `layer_height_profile_from_ranges()` reads
|
||||
that option without checking it is there.
|
||||
|
||||
Height ranges rather than colour-change custom G-code: they are object-relative, so they survive
|
||||
the object being moved, they are saved in the project file, and they appear in the object list where
|
||||
the user can see and edit them. The cost is that an object can only have one set of them, so
|
||||
applying a stack replaces whatever ranges the object had, with a confirmation when it had any.
|
||||
|
||||
`ObjectList::rebuild_layer_ranges()` mirrors them in the object tree without touching the
|
||||
selection - changing the selection while a gizmo is open tears the gizmo down under the user.
|
||||
|
||||
## Geometry the model assumes
|
||||
|
||||
A filament swap happens at one Z across the whole plate, and the relief grows along the painted
|
||||
surface's normal. The two agree only where that surface is horizontal, so:
|
||||
|
||||
- the painted patch's height above the bottom of the object is what the base layer count has to
|
||||
match, and the panel measures it from the patch and offers the number rather than leaving the user
|
||||
to work it out;
|
||||
- a patch that is not flat in Z gets a warning, because its colours will only be right where it is
|
||||
level. The relief itself is still computed, since a gently domed plaque is a legitimate thing to
|
||||
print and only the user can judge how much error is acceptable.
|
||||
|
||||
The model itself is the user's: the stack needs a slab of the base thickness with a face to paint,
|
||||
and nothing here creates or thickens geometry.
|
||||
|
||||
## Seeing the colours
|
||||
|
||||
The colours are blends of translucent filaments. No single filament makes them, so they cannot be
|
||||
expressed as the per-triangle filament assignment the 3D scene paints with, and there is nothing for
|
||||
the ordinary view to draw. They exist in two places: the sliced preview, which shows the filament
|
||||
bands as they print, and an overlay the gizmo draws over whatever surface is on screen.
|
||||
|
||||
That overlay is a shader, `resources/shaders/*/chromastack.*`, and it reads the colour from each
|
||||
fragment's own **print Z** rather than from the height map or from anything in the mesh. A filament
|
||||
swap happens at one Z across the whole plate, so the layer a point sits in is what decides its
|
||||
colour whatever the picture says. Two things follow. A band edge lands exactly on the layer boundary
|
||||
however coarse the triangles are - a per-triangle colouring puts it wherever the mesh happens to
|
||||
have an edge. And it keeps working after a bake, when the relief is real geometry, the paint has
|
||||
been cleared and the height map is attached to nothing on screen; the overlay is built from the live
|
||||
preview's displaced mesh before a bake and from the volume's own mesh after one.
|
||||
|
||||
The stack's colours reach the shader as a texture of one texel per printed layer, because a base can
|
||||
be several hundred layers and no uniform array holds that. The mesh carries nothing about the stack,
|
||||
so editing the stack costs a palette upload rather than a mesh rebuild.
|
||||
|
||||
On top of the colour the shader tries to look like printed plastic, because a flat shaded colour
|
||||
reads as a rendering and the whole point of the overlay is judging a print:
|
||||
|
||||
- the terminator is **wrapped** and the grazing angles **glow**, both scaled by the transmission
|
||||
distance of the filament on top - filament translucent enough to be worth stacking scatters light
|
||||
rather than reflecting it, and an opaque stack correctly gets neither;
|
||||
- the walls carry **layer lines**, a groove at every layer boundary that also modulates the
|
||||
specular, so a wall glints in stripes as a printed one does. Terraces get none: a terrace is one
|
||||
layer's top surface and has no lines to show.
|
||||
|
||||
Both effects are driven by the fragment's height, so they are free of any texture or UV, and both
|
||||
fade out through `fwidth` once a pixel covers more than about half a layer - lines finer than a
|
||||
pixel are noise, and a colour read from one layer out of the dozen under a pixel flickers as the
|
||||
camera moves. The palette lookup slides off the texel centres over the same range so the linear
|
||||
filter averages the bands a pixel covers instead of picking one of them.
|
||||
|
||||
The overlay is drawn pulled forward and without depth writes, the same pass the painted-colour
|
||||
overlay uses, since it is the same surface as whatever is underneath it.
|
||||
|
||||
## What the panel shows
|
||||
|
||||
The kind of a layer is the layer's own, chosen when it is added and fixed from then on: **Add layer**
|
||||
offers *Relief layer* or *ChromaStack picture*, and the card carries its kind as a chip. There is no
|
||||
panel-wide mode, so one object can hold a picture on its face and a knurl on its rim, and a card
|
||||
never shows a control that cannot do anything on it.
|
||||
|
||||
What the two cards do not share:
|
||||
|
||||
| Control | Relief card | ChromaStack card |
|
||||
| --- | --- | --- |
|
||||
| Depth | slider | the solve writes it; the card prints the result |
|
||||
| Tile size | repeats the pattern | **Picture size**, the same number named for what it does |
|
||||
| Midlevel, Smoothing, Invert, Edge fade | present | absent: each would move the steps off the layer boundaries |
|
||||
| Colors, Mix filaments, Denoise | one filament per triangle | absent: swaps are how this card colours |
|
||||
| Tile, tile method | present | absent: one copy, never tiled |
|
||||
| Blend | four modes | absent: it is the top layer and replaces what is under it |
|
||||
| Picture, prediction, stack, palette, match, Dither, Solve, Apply swaps | absent | present |
|
||||
|
||||
A ChromaStack card has no grip and cannot be reordered: its colours come from swapping filament at a
|
||||
height, so anything printed above it would bury them. An object may hold only one, refused when the
|
||||
layer is added rather than when the swaps are applied, because the swaps are the object's own height
|
||||
ranges and there is one set of those.
|
||||
|
||||
The card itself, top to bottom: what it is in one line, the picture and the predicted print side by
|
||||
side at the same size, the match figures, the base with the measurement from the painted face offered
|
||||
beside it, a row per band - swatch, filament, layer count, TD - the colours the stack reaches as a
|
||||
ramp, the totals, dither with the layer height it is solved against, and the two actions. Base and
|
||||
band counts are typed fields rather than sliders: a base is measured in hundreds of layers and a band
|
||||
in single digits, and dragging past the end of a slider's range is how a stack silently loses its
|
||||
backing.
|
||||
|
||||
The ramp is computed from the stack alone, before any solve. It is what answers "can these filaments
|
||||
print this picture" without spending a solve to find out they cannot: a short or flat ramp means the
|
||||
filaments are too alike, or too opaque, to give the picture anything to work with.
|
||||
|
||||
A per-slot flag records that the stack has been edited since the last solve, because a stale height
|
||||
map looks exactly like a good one.
|
||||
+3997
-2267
File diff suppressed because it is too large
Load Diff
+5238
-3068
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before 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="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>
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 415 B |
@@ -0,0 +1,92 @@
|
||||
#version 110
|
||||
|
||||
// What a ChromaStack print looks like, shaded per fragment.
|
||||
//
|
||||
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
|
||||
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
|
||||
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
|
||||
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
|
||||
// displaced mesh) and after one (on the volume's own).
|
||||
//
|
||||
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
|
||||
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
|
||||
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
|
||||
// the first thing the eye uses to read a print as a print.
|
||||
|
||||
#define INTENSITY_CORRECTION 0.6
|
||||
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
||||
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SHININESS 20.0
|
||||
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
|
||||
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
|
||||
#define INTENSITY_AMBIENT 0.3
|
||||
|
||||
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
|
||||
// array because a base can be several hundred layers and no uniform array survives that.
|
||||
uniform sampler2D palette;
|
||||
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
|
||||
uniform float base_z; // world Z of the bottom of the object
|
||||
uniform float layer_height; // mm
|
||||
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
|
||||
// It drives the wrap and the rim: an opaque stack should not glow.
|
||||
uniform float translucency;
|
||||
uniform float alpha;
|
||||
|
||||
varying vec3 clipping_planes_dots;
|
||||
varying vec3 eye_normal;
|
||||
varying vec3 eye_position;
|
||||
varying vec3 world_normal;
|
||||
varying float world_z;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
|
||||
discard;
|
||||
|
||||
float z_rel = world_z - base_z;
|
||||
float layers = max(z_rel / layer_height, 0.0);
|
||||
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
|
||||
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
|
||||
// flickers as the camera moves.
|
||||
float px = fwidth(layers);
|
||||
|
||||
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
|
||||
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
|
||||
// the bands it covers instead of picking one of them at random.
|
||||
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
|
||||
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
|
||||
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
|
||||
vec3 albedo = texture2D(palette, vec2(u, 0.5)).rgb;
|
||||
|
||||
vec3 N = normalize(eye_normal);
|
||||
vec3 V = normalize(-eye_position);
|
||||
|
||||
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
|
||||
// groove runs round the part at every layer boundary, so it is a function of height alone.
|
||||
float wall = 1.0 - abs(world_normal.z);
|
||||
float phase = fract(layers);
|
||||
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
|
||||
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
|
||||
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
|
||||
albedo *= lines;
|
||||
|
||||
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
|
||||
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
|
||||
float wrap = 0.25 + 0.45 * translucency;
|
||||
float ndotl = dot(N, LIGHT_TOP_DIR);
|
||||
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
|
||||
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
|
||||
|
||||
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
|
||||
// rather than evenly.
|
||||
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
|
||||
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
|
||||
|
||||
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
|
||||
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
|
||||
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
|
||||
|
||||
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#version 110
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
uniform mat4 projection_matrix;
|
||||
uniform mat3 view_normal_matrix;
|
||||
uniform mat4 volume_world_matrix;
|
||||
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
|
||||
// which is what decides whether layer lines show at all.
|
||||
uniform mat3 world_normal_matrix;
|
||||
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
|
||||
uniform vec2 z_range;
|
||||
// Clipping plane, general orientation (the section view).
|
||||
uniform vec4 clipping_plane;
|
||||
|
||||
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
|
||||
// nothing per vertex that the mesh does not already carry, because the colour comes from the
|
||||
// fragment's own height rather than from anything baked into the vertices.
|
||||
attribute vec3 v_position;
|
||||
attribute vec3 v_normal;
|
||||
|
||||
varying vec3 clipping_planes_dots;
|
||||
varying vec3 eye_normal;
|
||||
varying vec3 eye_position;
|
||||
varying vec3 world_normal;
|
||||
varying float world_z;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 model_pos = vec4(v_position, 1.0);
|
||||
vec4 world_pos = volume_world_matrix * model_pos;
|
||||
world_z = world_pos.z;
|
||||
world_normal = normalize(world_normal_matrix * v_normal);
|
||||
eye_normal = normalize(view_normal_matrix * v_normal);
|
||||
|
||||
vec4 eye_pos = view_model_matrix * model_pos;
|
||||
eye_position = eye_pos.xyz;
|
||||
gl_Position = projection_matrix * eye_pos;
|
||||
|
||||
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
|
||||
}
|
||||
@@ -36,11 +36,6 @@ 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 sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
|
||||
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 mat4 view_model_matrix;
|
||||
@@ -50,14 +45,6 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel;
|
||||
uniform float depth_mm;
|
||||
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
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -67,12 +54,6 @@ uniform bool invert;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
// 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;
|
||||
// 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.
|
||||
@@ -181,29 +162,13 @@ 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 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
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.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -272,12 +237,9 @@ void main()
|
||||
|
||||
// World millimetres throughout, like the bake - see the 140 variant.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
if (volume_mirrored)
|
||||
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
|
||||
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
|
||||
@@ -308,14 +270,14 @@ void main()
|
||||
} else if (weight > 0.0) {
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, tex_normal);
|
||||
vec2 uv = project_uv(tex_pos, tex_normal);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -327,18 +289,18 @@ void main()
|
||||
// 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
|
||||
// case of looking straight down at a surface.
|
||||
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 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
||||
float sweep = length(view_dir - triangle_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) {
|
||||
// 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
|
||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||
float s = h_hi / v_dot_n;
|
||||
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||
s -= ds;
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||
if (gap <= 0.0) {
|
||||
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||
@@ -360,23 +322,18 @@ void main()
|
||||
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 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// 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);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// ...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;
|
||||
|
||||
// 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);
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -396,9 +353,7 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
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)
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
|
||||
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -16,16 +16,7 @@ uniform bool volume_mirrored;
|
||||
uniform int mode;
|
||||
uniform float checker_freq;
|
||||
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
|
||||
// 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 vec2 uv_offset;
|
||||
uniform bool use_vertex_uv;
|
||||
@@ -88,30 +79,13 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
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 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
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
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -131,7 +105,7 @@ void main()
|
||||
|
||||
// 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 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor;
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -139,7 +113,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 c = floor(uv * checker_freq);
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#version 140
|
||||
|
||||
// What a ChromaStack print looks like, shaded per fragment.
|
||||
//
|
||||
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
|
||||
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
|
||||
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
|
||||
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
|
||||
// displaced mesh) and after one (on the volume's own).
|
||||
//
|
||||
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
|
||||
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
|
||||
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
|
||||
// the first thing the eye uses to read a print as a print.
|
||||
|
||||
#define INTENSITY_CORRECTION 0.6
|
||||
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
||||
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SHININESS 20.0
|
||||
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
|
||||
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
|
||||
#define INTENSITY_AMBIENT 0.3
|
||||
|
||||
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
|
||||
// array because a base can be several hundred layers and no uniform array survives that.
|
||||
uniform sampler2D palette;
|
||||
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
|
||||
uniform float base_z; // world Z of the bottom of the object
|
||||
uniform float layer_height; // mm
|
||||
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
|
||||
// It drives the wrap and the rim: an opaque stack should not glow.
|
||||
uniform float translucency;
|
||||
uniform float alpha;
|
||||
|
||||
in vec3 clipping_planes_dots;
|
||||
in vec3 eye_normal;
|
||||
in vec3 eye_position;
|
||||
in vec3 world_normal;
|
||||
in float world_z;
|
||||
|
||||
out vec4 out_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
|
||||
discard;
|
||||
|
||||
float z_rel = world_z - base_z;
|
||||
float layers = max(z_rel / layer_height, 0.0);
|
||||
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
|
||||
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
|
||||
// flickers as the camera moves.
|
||||
float px = fwidth(layers);
|
||||
|
||||
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
|
||||
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
|
||||
// the bands it covers instead of picking one of them at random.
|
||||
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
|
||||
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
|
||||
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
|
||||
vec3 albedo = texture(palette, vec2(u, 0.5)).rgb;
|
||||
|
||||
vec3 N = normalize(eye_normal);
|
||||
vec3 V = normalize(-eye_position);
|
||||
|
||||
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
|
||||
// groove runs round the part at every layer boundary, so it is a function of height alone.
|
||||
float wall = 1.0 - abs(world_normal.z);
|
||||
float phase = fract(layers);
|
||||
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
|
||||
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
|
||||
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
|
||||
albedo *= lines;
|
||||
|
||||
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
|
||||
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
|
||||
float wrap = 0.25 + 0.45 * translucency;
|
||||
float ndotl = dot(N, LIGHT_TOP_DIR);
|
||||
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
|
||||
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
|
||||
|
||||
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
|
||||
// rather than evenly.
|
||||
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
|
||||
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
|
||||
|
||||
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
|
||||
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
|
||||
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
|
||||
|
||||
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
|
||||
out_color = vec4(color, alpha);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#version 140
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
uniform mat4 projection_matrix;
|
||||
uniform mat3 view_normal_matrix;
|
||||
uniform mat4 volume_world_matrix;
|
||||
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
|
||||
// which is what decides whether layer lines show at all.
|
||||
uniform mat3 world_normal_matrix;
|
||||
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
|
||||
uniform vec2 z_range;
|
||||
// Clipping plane, general orientation (the section view).
|
||||
uniform vec4 clipping_plane;
|
||||
|
||||
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
|
||||
// nothing per vertex that the mesh does not already carry, because the colour comes from the
|
||||
// fragment's own height rather than from anything baked into the vertices.
|
||||
in vec3 v_position;
|
||||
in vec3 v_normal;
|
||||
|
||||
out vec3 clipping_planes_dots;
|
||||
out vec3 eye_normal;
|
||||
out vec3 eye_position;
|
||||
out vec3 world_normal;
|
||||
out float world_z;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 model_pos = vec4(v_position, 1.0);
|
||||
vec4 world_pos = volume_world_matrix * model_pos;
|
||||
world_z = world_pos.z;
|
||||
world_normal = normalize(world_normal_matrix * v_normal);
|
||||
eye_normal = normalize(view_normal_matrix * v_normal);
|
||||
|
||||
vec4 eye_pos = view_model_matrix * model_pos;
|
||||
eye_position = eye_pos.xyz;
|
||||
gl_Position = projection_matrix * eye_pos;
|
||||
|
||||
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
|
||||
}
|
||||
@@ -95,11 +95,6 @@ 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 sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
|
||||
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 mat4 view_model_matrix;
|
||||
@@ -109,14 +104,6 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
||||
uniform float depth_mm;
|
||||
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
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -126,12 +113,6 @@ uniform bool invert;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
// 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)
|
||||
// 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
|
||||
@@ -247,29 +228,13 @@ 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 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
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.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -340,12 +305,9 @@ void main()
|
||||
// printed part whatever the instance's scale or rotation, so the preview has to project from the
|
||||
// world position and perturb the world normal.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
if (volume_mirrored)
|
||||
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
|
||||
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
|
||||
@@ -389,14 +351,14 @@ void main()
|
||||
// closed-form uv here, unlike the LSCM case.
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, tex_normal);
|
||||
vec2 uv = project_uv(tex_pos, tex_normal);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -408,18 +370,18 @@ void main()
|
||||
// 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
|
||||
// case of looking straight down at a surface.
|
||||
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 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
||||
float sweep = length(view_dir - triangle_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) {
|
||||
// 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
|
||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||
float s = h_hi / v_dot_n;
|
||||
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||
s -= ds;
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||
if (gap <= 0.0) {
|
||||
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||
@@ -443,28 +405,23 @@ void main()
|
||||
// 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));
|
||||
|
||||
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
|
||||
// turns the uv-space gradient into a real surface slope.
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
|
||||
// gradient into a real surface slope.
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
|
||||
// gradient back into the axes' frame.
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// 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);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// ...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,
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -484,9 +441,7 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
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)
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
|
||||
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -25,16 +25,7 @@ uniform bool volume_mirrored;
|
||||
uniform int mode; // 0 checker, 1 distortion
|
||||
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
|
||||
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
|
||||
// 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 vec2 uv_offset;
|
||||
uniform bool use_vertex_uv;
|
||||
@@ -99,30 +90,13 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
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 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
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
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -143,7 +117,7 @@ void main()
|
||||
|
||||
// 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 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor;
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -151,7 +125,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 c = floor(uv * checker_freq);
|
||||
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.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<script src="../js/plugin-config-ui.js"></script>
|
||||
<script src="./index.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>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
|
||||
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
|
||||
if( bOn )
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
}
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
@@ -336,10 +336,10 @@ modules:
|
||||
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
|
||||
dest: external-packages/PNG
|
||||
|
||||
# libjpeg-turbo 3.0.1
|
||||
# libjpeg-turbo 3.2.0
|
||||
- type: file
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
||||
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||
dest: external-packages/JPEG
|
||||
|
||||
# Freetype 2.12.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
|
||||
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
|
||||
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
|
||||
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
||||
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
||||
|
||||
+21
-15
@@ -163,6 +163,7 @@ using json = nlohmann::json;
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
@@ -1156,8 +1157,10 @@ int CLI::run(int argc, char **argv)
|
||||
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
||||
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
||||
// unset preserves WebKit hardware acceleration on Device / Setup
|
||||
// Wizard / login / store. The default path still applies it on
|
||||
// XWayland sessions as a conservative fallback for older WebKit.
|
||||
// Wizard / login / store. The default path applies it only where it
|
||||
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really
|
||||
// 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");
|
||||
@@ -1165,6 +1168,14 @@ int CLI::run(int argc, char **argv)
|
||||
// honours the first backend in the comma-separated list).
|
||||
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) {
|
||||
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
||||
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
||||
@@ -1204,23 +1215,18 @@ int CLI::run(int argc, char **argv)
|
||||
if (wayland_env && *wayland_env) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||
::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
|
||||
|
||||
// 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
|
||||
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
||||
// (no Xlib involved), so the call is skipped.
|
||||
|
||||
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
|
||||
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
|
||||
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||
export GALLIUM_DRIVER=zink
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
|
||||
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -327,6 +327,8 @@ set(lisbslic3r_sources
|
||||
Geometry/VoronoiUtils.cpp
|
||||
Geometry/VoronoiUtils.hpp
|
||||
Geometry/VoronoiVisualUtils.hpp
|
||||
ChromaStack.cpp
|
||||
ChromaStack.hpp
|
||||
IMEXArrange.cpp
|
||||
IMEXArrange.hpp
|
||||
IMEXHelpers.cpp
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
#include "ChromaStack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <cstdlib>
|
||||
#include <numeric>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include <miniz.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// How many e-folding lengths of absorption count as "the colour beneath no longer shows". Three
|
||||
// passes 5% of the light, which is about where the eye stops seeing what is under a layer.
|
||||
static constexpr float OPAQUE_EFOLDS = 3.f;
|
||||
|
||||
// TD is a filament property, so a zero or negative value in a preset is a data error rather than a
|
||||
// request for an infinitely opaque filament. Clamp instead: below this the filament is opaque at a
|
||||
// single layer anyway.
|
||||
static constexpr float TD_MIN_MM = 0.05f;
|
||||
static constexpr float TD_MAX_MM = 100.f;
|
||||
|
||||
static float srgb_to_linear(float c)
|
||||
{
|
||||
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
|
||||
static float linear_to_srgb(float c)
|
||||
{
|
||||
return c <= 0.0031308f ? c * 12.92f : 1.055f * std::pow(c, 1.f / 2.4f) - 0.055f;
|
||||
}
|
||||
|
||||
// CIELAB under D65, from non-linear sRGB in [0, 1]. Only used for nearest-colour search, so the
|
||||
// absolute white point matters far less than both sides using the same one.
|
||||
struct Lab
|
||||
{
|
||||
float l = 0.f, a = 0.f, b = 0.f;
|
||||
};
|
||||
|
||||
static Lab srgb_to_lab(float r, float g, float b)
|
||||
{
|
||||
const float rl = srgb_to_linear(r), gl = srgb_to_linear(g), bl = srgb_to_linear(b);
|
||||
const float x = (0.4124f * rl + 0.3576f * gl + 0.1805f * bl) / 0.95047f;
|
||||
const float y = (0.2126f * rl + 0.7152f * gl + 0.0722f * bl);
|
||||
const float z = (0.0193f * rl + 0.1192f * gl + 0.9505f * bl) / 1.08883f;
|
||||
auto f = [](float t) {
|
||||
return t > 0.008856f ? std::cbrt(t) : (7.787f * t + 16.f / 116.f);
|
||||
};
|
||||
const float fx = f(x), fy = f(y), fz = f(z);
|
||||
return Lab{ 116.f * fy - 16.f, 500.f * (fx - fy), 200.f * (fy - fz) };
|
||||
}
|
||||
|
||||
static float delta_e(const Lab &p, const Lab &q)
|
||||
{
|
||||
const float dl = p.l - q.l, da = p.a - q.a, db = p.b - q.b;
|
||||
return std::sqrt(dl * dl + da * da + db * db);
|
||||
}
|
||||
|
||||
int ChromaStack::relief_layers() const
|
||||
{
|
||||
int n = 0;
|
||||
for (const ChromaBand &band : bands)
|
||||
n += std::max(0, band.layers);
|
||||
return n;
|
||||
}
|
||||
|
||||
int ChromaStack::total_layers() const { return std::max(0, base_layers) + relief_layers(); }
|
||||
|
||||
float ChromaStack::depth_mm() const { return float(relief_layers()) * layer_height_mm; }
|
||||
|
||||
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack)
|
||||
{
|
||||
const int total = stack.total_layers();
|
||||
// No bands means no picture and no filament to print the base in, whatever base_layers says.
|
||||
if (total <= 0 || stack.bands.empty() || stack.layer_height_mm <= 0.f)
|
||||
return {};
|
||||
|
||||
// The filament each printed layer is in, bottom-up: the base first, in the bottom band's filament,
|
||||
// then the bands themselves.
|
||||
std::vector<const ChromaFilament *> per_layer;
|
||||
per_layer.reserve(size_t(total));
|
||||
for (int i = 0; i < std::max(0, stack.base_layers); ++i)
|
||||
per_layer.push_back(&stack.bands.front().filament);
|
||||
for (const ChromaBand &band : stack.bands)
|
||||
for (int i = 0; i < band.layers; ++i)
|
||||
per_layer.push_back(&band.filament);
|
||||
|
||||
std::vector<ColorRGB> palette(size_t(total) + 1);
|
||||
|
||||
// Linear light, walking up from an opaque base: C_0 is the bottom layer's own colour, so a
|
||||
// column one layer tall comes out exactly that colour however translucent the filament is.
|
||||
std::array<float, 3> acc{ srgb_to_linear(per_layer.front()->color.r()), srgb_to_linear(per_layer.front()->color.g()),
|
||||
srgb_to_linear(per_layer.front()->color.b()) };
|
||||
auto store = [&palette](size_t k, const std::array<float, 3> &lin) {
|
||||
palette[k] = ColorRGB(std::clamp(linear_to_srgb(lin[0]), 0.f, 1.f), std::clamp(linear_to_srgb(lin[1]), 0.f, 1.f),
|
||||
std::clamp(linear_to_srgb(lin[2]), 0.f, 1.f));
|
||||
};
|
||||
store(0, acc);
|
||||
|
||||
for (int k = 1; k <= total; ++k) {
|
||||
const ChromaFilament &f = *per_layer[size_t(k - 1)];
|
||||
const float td = std::clamp(f.td_mm, TD_MIN_MM, TD_MAX_MM);
|
||||
// The depth at which this filament hides what is behind it, turned into the e-folding length
|
||||
// of its absorption; see CHROMA_TD_OPAQUE_FRACTION.
|
||||
const float efold = CHROMA_TD_OPAQUE_FRACTION * td / OPAQUE_EFOLDS;
|
||||
const float t = std::exp(-stack.layer_height_mm / efold);
|
||||
const std::array<float, 3> own{ srgb_to_linear(f.color.r()), srgb_to_linear(f.color.g()),
|
||||
srgb_to_linear(f.color.b()) };
|
||||
for (int c = 0; c < 3; ++c)
|
||||
acc[size_t(c)] = (1.f - t) * own[size_t(c)] + t * acc[size_t(c)];
|
||||
store(size_t(k), acc);
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack)
|
||||
{
|
||||
ChromaSolve solve;
|
||||
const int total = stack.total_layers();
|
||||
const int base = std::max(0, stack.base_layers);
|
||||
const int relief = stack.relief_layers();
|
||||
if (rgb == nullptr || width <= 0 || height <= 0 || relief <= 0 || relief > CHROMA_MAX_RELIEF_LAYERS)
|
||||
return solve;
|
||||
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
if (int(palette.size()) != total + 1)
|
||||
return solve;
|
||||
|
||||
// Only the heights the solver may choose: base (nothing of the relief printed) up to total.
|
||||
const int choices = relief + 1;
|
||||
const size_t choice_count = size_t(choices);
|
||||
std::vector<Lab> choice_lab(choice_count);
|
||||
std::vector<std::array<uint8_t, 3>> choice_rgb(choice_count);
|
||||
for (int i = 0; i < choices; ++i) {
|
||||
const ColorRGB &c = palette[size_t(base + i)];
|
||||
choice_lab[size_t(i)] = srgb_to_lab(c.r(), c.g(), c.b());
|
||||
choice_rgb[size_t(i)] = { uint8_t(std::lround(c.r() * 255.f)), uint8_t(std::lround(c.g() * 255.f)),
|
||||
uint8_t(std::lround(c.b() * 255.f)) };
|
||||
}
|
||||
|
||||
const size_t count = size_t(width) * size_t(height);
|
||||
solve.width = width;
|
||||
solve.height = height;
|
||||
solve.levels.resize(count);
|
||||
solve.grey.resize(count);
|
||||
solve.preview.resize(count * 3);
|
||||
|
||||
std::vector<float> errors(count, 0.f);
|
||||
|
||||
auto pick = [&](float r, float g, float b, float &out_error) {
|
||||
const Lab target = srgb_to_lab(std::clamp(r, 0.f, 1.f), std::clamp(g, 0.f, 1.f), std::clamp(b, 0.f, 1.f));
|
||||
int best = 0;
|
||||
float best_d = std::numeric_limits<float>::max();
|
||||
for (int i = 0; i < choices; ++i) {
|
||||
const float d = delta_e(target, choice_lab[size_t(i)]);
|
||||
if (d < best_d) {
|
||||
best_d = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
out_error = best_d;
|
||||
return best;
|
||||
};
|
||||
|
||||
// The grey level a chosen height is written as. Rounded to the nearest of 256 levels, which for
|
||||
// any practical layer budget (a few dozen) maps every height to its own level exactly - the
|
||||
// relief's steps stay on layer boundaries, which is the whole point of quantizing here.
|
||||
auto grey_of = [relief](int choice) {
|
||||
return uint8_t(std::lround(255.0 * double(choice) / double(relief)));
|
||||
};
|
||||
|
||||
auto write = [&](size_t px, int choice, float err) {
|
||||
solve.levels[px] = uint16_t(base + choice);
|
||||
solve.grey[px] = grey_of(choice);
|
||||
solve.preview[px * 3] = choice_rgb[size_t(choice)][0];
|
||||
solve.preview[px * 3 + 1] = choice_rgb[size_t(choice)][1];
|
||||
solve.preview[px * 3 + 2] = choice_rgb[size_t(choice)][2];
|
||||
errors[px] = err;
|
||||
};
|
||||
|
||||
if (stack.dither) {
|
||||
// Floyd-Steinberg, serpentine so the error does not drift consistently to one side. The
|
||||
// diffusion buffer holds the two rows the kernel reaches, in sRGB - which is where the
|
||||
// artifact being traded away lives, and what every reference implementation diffuses in.
|
||||
const size_t row_len = size_t(width);
|
||||
std::vector<std::array<float, 3>> row(row_len), next(row_len);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uint8_t *src = rgb + size_t(x) * 3;
|
||||
row[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
|
||||
}
|
||||
for (int y = 0; y < height; ++y) {
|
||||
if (y + 1 < height)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uint8_t *src = rgb + (size_t(y + 1) * size_t(width) + size_t(x)) * 3;
|
||||
next[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
|
||||
}
|
||||
const bool ltr = (y % 2) == 0;
|
||||
for (int i = 0; i < width; ++i) {
|
||||
const int x = ltr ? i : width - 1 - i;
|
||||
const size_t px = size_t(y) * size_t(width) + size_t(x);
|
||||
float err = 0.f;
|
||||
const int choice = pick(row[size_t(x)][0], row[size_t(x)][1], row[size_t(x)][2], err);
|
||||
write(px, choice, err);
|
||||
|
||||
std::array<float, 3> residual{};
|
||||
for (int c = 0; c < 3; ++c)
|
||||
residual[size_t(c)] = row[size_t(x)][size_t(c)] - choice_rgb[size_t(choice)][size_t(c)] / 255.f;
|
||||
|
||||
const int ahead = ltr ? x + 1 : x - 1;
|
||||
const int back = ltr ? x - 1 : x + 1;
|
||||
auto spread = [&](std::vector<std::array<float, 3>> &dst, int at, float w) {
|
||||
if (at < 0 || at >= width)
|
||||
return;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
dst[size_t(at)][size_t(c)] += residual[size_t(c)] * w;
|
||||
};
|
||||
spread(row, ahead, 7.f / 16.f);
|
||||
if (y + 1 < height) {
|
||||
spread(next, back, 3.f / 16.f);
|
||||
spread(next, x, 5.f / 16.f);
|
||||
spread(next, ahead, 1.f / 16.f);
|
||||
}
|
||||
}
|
||||
row.swap(next);
|
||||
}
|
||||
} else {
|
||||
tbb::parallel_for(tbb::blocked_range<int>(0, height), [&](const tbb::blocked_range<int> &range) {
|
||||
for (int y = range.begin(); y < range.end(); ++y)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const size_t px = size_t(y) * size_t(width) + size_t(x);
|
||||
const uint8_t *src = rgb + px * 3;
|
||||
float err = 0.f;
|
||||
write(px, pick(src[0] / 255.f, src[1] / 255.f, src[2] / 255.f, err), err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
solve.mean_delta_e = count == 0 ? 0. : std::accumulate(errors.begin(), errors.end(), 0.) / double(count);
|
||||
solve.max_delta_e = errors.empty() ? 0. : double(*std::max_element(errors.begin(), errors.end()));
|
||||
return solve;
|
||||
}
|
||||
|
||||
float chroma_layers_to_opaque(float td_mm, float layer_height_mm)
|
||||
{
|
||||
if (layer_height_mm <= 0.f)
|
||||
return 0.f;
|
||||
return CHROMA_TD_OPAQUE_FRACTION * std::clamp(td_mm, TD_MIN_MM, TD_MAX_MM) / layer_height_mm;
|
||||
}
|
||||
|
||||
// Below about one dE two colours are the same colour to the eye. Half of that is the threshold for
|
||||
// "this layer changed something", so a step has to be comfortably invisible before a layer is called
|
||||
// wasted.
|
||||
static constexpr float VISIBLE_DE = 0.5f;
|
||||
|
||||
static float step_delta_e(const ColorRGB &a, const ColorRGB &b)
|
||||
{
|
||||
return delta_e(srgb_to_lab(a.r(), a.g(), a.b()), srgb_to_lab(b.r(), b.g(), b.b()));
|
||||
}
|
||||
|
||||
int chroma_distinct_levels(const ChromaStack &stack)
|
||||
{
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
const int base = std::max(0, stack.base_layers);
|
||||
if (int(palette.size()) <= base + 1)
|
||||
return 0;
|
||||
int n = 0;
|
||||
for (size_t i = size_t(base) + 1; i < palette.size(); ++i)
|
||||
if (step_delta_e(palette[i - 1], palette[i]) > VISIBLE_DE)
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
void chroma_trim_bands(ChromaStack &stack)
|
||||
{
|
||||
// Walked over the stack's own palette rather than over the filaments' TD, because how fast a band
|
||||
// runs out of anything to say depends on what is under it as much as on the filament. A light band
|
||||
// over a dark one is finished in four or five layers; a dark one over a light one goes on paying
|
||||
// for many more, long past the depth at which it is nominally opaque, because the last few percent
|
||||
// of light coming through still moves a near-black colour perceptibly.
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
if (palette.empty())
|
||||
return;
|
||||
int layer = std::max(0, stack.base_layers); // palette index of the layer under the band being looked at
|
||||
for (ChromaBand &band : stack.bands) {
|
||||
int keep = 1;
|
||||
for (int i = 1; i <= band.layers; ++i) {
|
||||
const size_t cur = size_t(layer) + size_t(i);
|
||||
if (cur >= palette.size())
|
||||
break;
|
||||
if (step_delta_e(palette[cur - 1], palette[cur]) > VISIBLE_DE)
|
||||
keep = i; // the last layer of this band that still changes the colour
|
||||
}
|
||||
layer += band.layers;
|
||||
band.layers = std::clamp(keep, 1, band.layers);
|
||||
}
|
||||
}
|
||||
|
||||
int chroma_level_at(const ChromaStack &stack, double z_mm)
|
||||
{
|
||||
const int total = stack.total_layers();
|
||||
if (total <= 0 || stack.layer_height_mm <= 0.f)
|
||||
return 0;
|
||||
// Layer k occupies ((k-1)h, k h], so a point inside it rounds up. The epsilon keeps a surface
|
||||
// sitting exactly on a layer boundary in the layer that printed it rather than in the one above,
|
||||
// which is the difference between a terrace showing its own colour and showing the next band's.
|
||||
const int k = int(std::ceil(z_mm / double(stack.layer_height_mm) - 1e-6));
|
||||
return std::clamp(k, 1, total);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve)
|
||||
{
|
||||
if (solve.empty())
|
||||
return {};
|
||||
size_t size = 0;
|
||||
void *png = tdefl_write_image_to_png_file_in_memory_ex(solve.grey.data(), solve.width, solve.height, 1, &size,
|
||||
MZ_DEFAULT_LEVEL, MZ_FALSE);
|
||||
if (png == nullptr || size == 0)
|
||||
return {};
|
||||
const unsigned char *bytes = static_cast<const unsigned char *>(png);
|
||||
std::vector<unsigned char> out(bytes, bytes + size);
|
||||
mz_free(png);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom)
|
||||
{
|
||||
std::vector<ChromaRange> out;
|
||||
if (stack.layer_height_mm <= 0.f)
|
||||
return out;
|
||||
// The layer height is stored as a float, like every other dimension of a texture displacement
|
||||
// layer, so multiplying it up leaves noise in the last few digits - 5 layers of 0.1 mm come out as
|
||||
// 0.5000000074. The slicer derives its own layer boundaries from the print config in double
|
||||
// precision and lands on 0.5 exactly, and a boundary that misses it by any amount is a boundary a
|
||||
// layer can fall on the wrong side of. Rounding to the nanometre removes the noise without
|
||||
// disturbing any layer height a profile can express.
|
||||
const auto snap = [](double z) { return std::round(z * 1e6) / 1e6; };
|
||||
|
||||
// The base is printed in the bottom band's filament - it is what is loaded when the print starts -
|
||||
// so it extends that band's range down to the first layer instead of being a swap of its own.
|
||||
int layer = std::max(0, stack.base_layers);
|
||||
bool first = true;
|
||||
for (const ChromaBand &band : stack.bands) {
|
||||
if (band.layers <= 0)
|
||||
continue;
|
||||
const double z_min = snap(z_bottom + (first ? 0. : double(layer) * double(stack.layer_height_mm)));
|
||||
first = false;
|
||||
layer += band.layers;
|
||||
const double z_max = snap(z_bottom + double(layer) * double(stack.layer_height_mm));
|
||||
if (!out.empty() && out.back().filament == band.filament.filament)
|
||||
out.back().z_max = z_max; // no swap happens here, so it is one range
|
||||
else
|
||||
out.push_back(ChromaRange{ z_min, z_max, band.filament.filament });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
|
||||
int base_layers, int relief_layers)
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = layer_height_mm > 0.f ? layer_height_mm : 0.08f;
|
||||
stack.base_layers = std::max(0, base_layers);
|
||||
if (filaments.empty() || relief_layers <= 0)
|
||||
return stack;
|
||||
|
||||
std::vector<ChromaFilament> ordered = filaments;
|
||||
std::stable_sort(ordered.begin(), ordered.end(), [](const ChromaFilament &a, const ChromaFilament &b) {
|
||||
// Relative luminance of the linear colour: the perceptual order, so a saturated yellow
|
||||
// counts as light and a saturated blue as dark, which a plain r+g+b sum gets wrong.
|
||||
auto lum = [](const ColorRGB &c) {
|
||||
return 0.2126f * srgb_to_linear(c.r()) + 0.7152f * srgb_to_linear(c.g()) + 0.0722f * srgb_to_linear(c.b());
|
||||
};
|
||||
return lum(a.color) > lum(b.color);
|
||||
});
|
||||
|
||||
const int count = int(ordered.size());
|
||||
int left = relief_layers;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
// Spread the remainder over the first bands rather than piling it onto the last one.
|
||||
const int share = left / (count - i) + ((left % (count - i)) > 0 ? 1 : 0);
|
||||
ChromaBand band;
|
||||
band.filament = ordered[size_t(i)];
|
||||
band.layers = std::max(1, share);
|
||||
left -= share;
|
||||
stack.bands.push_back(band);
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,286 @@
|
||||
#ifndef slic3r_ChromaStack_hpp_
|
||||
#define slic3r_ChromaStack_hpp_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <cereal/cereal.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
#include "Color.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ChromaStack-style colour printing: a picture is reproduced on a single face by stacking thin layers
|
||||
// of a few filaments, swapping filament at fixed layer boundaries, and varying the *height* of each
|
||||
// pixel's column. Where the column is low, the layers above it are missing and the filament below
|
||||
// shows; where it is high, the topmost filament covers everything beneath it. Because filament is
|
||||
// translucent, a column that stops part-way through a run shows a blend of that run's filament and
|
||||
// what is under it, which is what gives far more colours than there are filaments loaded.
|
||||
//
|
||||
// So there are only two things to compute, and they are both here:
|
||||
// - the *palette*: the colour the stack shows at every possible column height (chroma_palette());
|
||||
// - the *height map*: per pixel, the column height whose palette colour is closest to the picture
|
||||
// (chroma_solve()).
|
||||
//
|
||||
// Everything else is machinery that already exists. The height map is handed to a texture
|
||||
// displacement layer as its height texture, so the existing projection/preview/bake pipeline turns
|
||||
// it into real geometry (see TextureDisplacement.hpp), and the filament swaps are handed to the
|
||||
// object's height ranges (ModelObject::layer_config_ranges), which is the slicer's own way of
|
||||
// saying "print this Z band with that filament".
|
||||
|
||||
// The depth of filament that blocks essentially all the light behind it, as a fraction of the
|
||||
// filament's transmission distance (TD).
|
||||
//
|
||||
// TD's scale is empirical, not physical: its values run from about 1
|
||||
// for an opaque filament to 10 and above for a nearly clear one, and the community's calibration
|
||||
// prints (the "seashell" step test) read a TD off the step where the colour beneath stops showing.
|
||||
// That step is about a tenth of the TD number, which is the only thing this constant encodes - a
|
||||
// filament of TD 2 stops hiding what is under it at roughly 0.2 mm, i.e. after two or three layers,
|
||||
// while one of TD 8 needs most of a millimetre. The model below turns that into a per-layer
|
||||
// transmittance; see chroma_palette().
|
||||
static constexpr float CHROMA_TD_OPAQUE_FRACTION = 0.1f;
|
||||
|
||||
// What a fresh stack gets above the base. Twenty-four layers is about two millimetres at a fine layer
|
||||
// height: enough steps for a continuous tonal ramp, and still a print that finishes in an evening.
|
||||
static constexpr int CHROMA_DEFAULT_RELIEF_LAYERS = 24;
|
||||
|
||||
// The most picture layers the solve can express. A column's height above the base is stored as one
|
||||
// byte per pixel in the 8-bit grayscale height map the displacement reads, so beyond this two
|
||||
// different heights would have to share a grey level and the relief's steps would stop landing on
|
||||
// layer boundaries - which is the one thing the quantization exists to guarantee.
|
||||
//
|
||||
// It bounds the bands, not the stack: the base is whatever the model already is under the painted
|
||||
// face, often hundreds of layers on a tall part, and nothing about it is quantized into the height
|
||||
// map. As a limit on the picture it is not one in practice - 255 layers is 20 mm of relief at
|
||||
// 0.08 mm, far past where a picture gains anything.
|
||||
static constexpr int CHROMA_MAX_RELIEF_LAYERS = 255;
|
||||
|
||||
// The most filament changes one stack may hold. Every band is a swap the printer has to make, so this is
|
||||
// a limit on patience rather than on the model; the palette itself is bounded by the layer count.
|
||||
static constexpr size_t CHROMA_MAX_BANDS = 16;
|
||||
|
||||
// One filament as the colour model sees it: what colour it is, and how far light gets into it.
|
||||
struct ChromaFilament
|
||||
{
|
||||
// 0-based index into the project's filaments, which is what a height range's `extruder` option
|
||||
// (1-based) is written from. -1 for a filament that is not loaded, which the GUI refuses to
|
||||
// solve with rather than silently printing in the wrong colour.
|
||||
int filament = -1;
|
||||
ColorRGB color;
|
||||
// The filament preset's own name, so the panel can say which filament this is. Six similar greys
|
||||
// are six identical swatches and six identical numbers otherwise. Stored rather than looked up on
|
||||
// demand because a stack outlives the project's filament list - a preset can be swapped out from
|
||||
// under a saved stack, and then the name is the only record of what it was solved against.
|
||||
std::string name;
|
||||
// Transmission distance, mm, as the filament preset reports it
|
||||
// (`filament_transmission_distance`). Clamped to a sane range when the palette is built: zero
|
||||
// would make a layer infinitely opaque and a negative value is meaningless.
|
||||
float td_mm = 4.f;
|
||||
|
||||
template<class Archive> void serialize(Archive &ar)
|
||||
{
|
||||
float r = color.r(), g = color.g(), b = color.b();
|
||||
ar(filament, r, g, b, td_mm, name);
|
||||
color = ColorRGB(r, g, b);
|
||||
}
|
||||
};
|
||||
|
||||
// A run of consecutive layers printed in one filament. The stack is a list of these, bottom-up, so
|
||||
// a band boundary is exactly where the print pauses and the filament is changed.
|
||||
struct ChromaBand
|
||||
{
|
||||
ChromaFilament filament;
|
||||
int layers = 1;
|
||||
|
||||
template<class Archive> void serialize(Archive &ar) { ar(filament, layers); }
|
||||
};
|
||||
|
||||
// The whole recipe: which filaments, in which order, how thick a layer is, and how much of the
|
||||
// bottom is solid backing rather than picture.
|
||||
struct ChromaStack
|
||||
{
|
||||
std::vector<ChromaBand> bands; // bottom-up; bands.front() is the one the base is printed in
|
||||
|
||||
// The print's layer height, mm. Taken from the print profile rather than chosen here - a stack
|
||||
// solved against 0.08 mm layers prints as the wrong picture at 0.2 mm, so the GUI re-solves when
|
||||
// the profile's layer height changes.
|
||||
float layer_height_mm = 0.08f;
|
||||
|
||||
// Layers of solid backing under the picture: everything the model already is below the painted
|
||||
// face. Not part of any band - the bands are the picture - but printed in the bottom band's
|
||||
// filament, since that is what is loaded when the print starts.
|
||||
//
|
||||
// Keeping it out of the bands is what lets it be what it really is: a measurement of the model,
|
||||
// often a hundred layers or more, while a band is a handful. Folding the two together put a
|
||||
// three-digit number on a slider meant for single digits and made "how thick is the backing" and
|
||||
// "how many layers does the first filament get" the same control, which they are not.
|
||||
//
|
||||
// The colour model treats the base as opaque. That is what makes a column's colour depend only on
|
||||
// the layers above it, and why the panel warns about a base thin enough to let the plate show
|
||||
// through.
|
||||
int base_layers = 4;
|
||||
|
||||
// Whether `base_layers` follows the model or was typed in. It follows by default, and it should:
|
||||
// the painted face is wherever the geometry puts it, and a base that disagrees with it shifts
|
||||
// every colour in the picture - the stack ends up anchored at the bottom of the part instead of
|
||||
// under the face, so the picture's whole range is squeezed into the first few millimetres and
|
||||
// everything above it clamps to the top band.
|
||||
//
|
||||
// It cannot be measured once and left: a layer is created, and its picture chosen, before there
|
||||
// is any paint to measure, and the face moves again whenever the part is scaled or the layer
|
||||
// height changes. So the panel re-measures it every frame while this is set.
|
||||
bool base_auto = true;
|
||||
|
||||
// Spread the quantization error into neighbouring pixels (Floyd-Steinberg, serpentine) instead
|
||||
// of rounding each pixel to its nearest palette colour on its own. A palette of twenty or thirty
|
||||
// colours has to cover a photograph's whole range, so plain rounding leaves visible bands across
|
||||
// a sky or a cheek; diffusing the error trades them for fine stipple, which at a tenth of a
|
||||
// millimetre per pixel reads as the intermediate colour. Off for flat-coloured artwork, where
|
||||
// stipple is the artifact rather than the cure.
|
||||
bool dither = false;
|
||||
|
||||
// Total printed layers, base included: what the tallest column is.
|
||||
int total_layers() const;
|
||||
// Layers the picture is built from, i.e. the bands' own total above the base.
|
||||
int relief_layers() const;
|
||||
// The relief's height in mm, which is the `depth_mm` of the texture displacement layer the
|
||||
// height map is handed to.
|
||||
float depth_mm() const;
|
||||
bool empty() const { return bands.empty(); }
|
||||
|
||||
template<class Archive> void serialize(Archive &ar) { ar(bands, layer_height_mm, base_layers, dither, base_auto); }
|
||||
};
|
||||
|
||||
// The colour the stack shows at every column height: entry k is a column of exactly k printed
|
||||
// layers, so the vector has total_layers() + 1 entries.
|
||||
//
|
||||
// The model is Beer-Lambert absorption, layer by layer. A layer of thickness h in a filament of
|
||||
// transmission distance TD passes
|
||||
//
|
||||
// T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
|
||||
//
|
||||
// of the light coming from under it and shows its own colour with the rest, so
|
||||
//
|
||||
// C_k = (1 - T_k) * c_k + T_k * C_(k-1)
|
||||
//
|
||||
// walking up from an opaque base, C_0 = c_1. The divisor is what makes "opaque at
|
||||
// CHROMA_TD_OPAQUE_FRACTION * TD" true: three e-folding lengths pass 5% of the light, which is
|
||||
// where the colour beneath stops being visible to the eye.
|
||||
//
|
||||
// Beer-Lambert is not just the physically honest choice here, it is the only one that stays
|
||||
// consistent when applied per layer: transmittance multiplies, so a run of n layers of one filament
|
||||
// passes exp(-n h / d) whether it is composited in one step or n, and the palette does not depend on
|
||||
// how the stack happens to be cut into bands.
|
||||
//
|
||||
// Compositing is done on linear light and the entries are converted back to sRGB, because the
|
||||
// quantity being attenuated is light, not an sRGB code.
|
||||
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack);
|
||||
|
||||
// What chroma_solve() produced.
|
||||
struct ChromaSolve
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
// Per pixel, row-major and top-to-bottom like DecodedHeightTexture: the column's printed layer
|
||||
// count, between base_layers and total_layers(). Sixteen bits because it counts from the build
|
||||
// plate, and the base alone can be several hundred layers on a tall part.
|
||||
std::vector<uint16_t> levels;
|
||||
// The same thing as the 8-bit grayscale height map the displacement samples: 0 at base_layers,
|
||||
// 255 at total_layers(). This is what gets encoded as a PNG into the layer's `image_data`, which
|
||||
// is why the solve has to be the thing that owns the quantization - the relief's steps have to
|
||||
// land on layer boundaries, and a grey level is the only channel the displacement reads.
|
||||
std::vector<uint8_t> grey;
|
||||
// Per pixel RGB: the palette colour the solve chose, i.e. what the print is predicted to look
|
||||
// like. Shown in the panel next to the source picture, since the 3D view can only show the
|
||||
// relief - the colours do not exist until the print swaps filament.
|
||||
std::vector<uint8_t> preview;
|
||||
|
||||
// How far the prediction landed from the picture, as a mean and worst-case CIE76 dE over all
|
||||
// pixels. The panel reports it because it is the only honest answer to "will this stack
|
||||
// reproduce my image": under about 5 is a good match, over about 15 means the filaments cannot
|
||||
// reach the picture's colours and no amount of layers will fix it.
|
||||
double mean_delta_e = 0.;
|
||||
double max_delta_e = 0.;
|
||||
|
||||
bool empty() const { return width <= 0 || height <= 0 || levels.empty(); }
|
||||
};
|
||||
|
||||
// Solves `rgb` (width * height * 3 bytes, row-major, top-to-bottom) against the stack's palette.
|
||||
// An empty stack, a zero-sized image or a stack with no relief layers gives an empty result.
|
||||
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack);
|
||||
|
||||
// How many layers of this filament it takes to hide what is under it, at this layer height. The
|
||||
// single most useful number about a stack and the one nothing else makes visible: it is what decides
|
||||
// whether a band is a gradient or a step.
|
||||
//
|
||||
// A filament hides what is behind it at about CHROMA_TD_OPAQUE_FRACTION of its TD (see
|
||||
// chroma_palette()), so this is that depth in layers. Below two the band has no intermediate colours
|
||||
// at all - the picture can only use the filament or not use it - which is what a coarse layer height
|
||||
// does to an otherwise good stack: TD 4 at 0.2 mm covers in two layers, the same filament at 0.08 mm
|
||||
// in five.
|
||||
float chroma_layers_to_opaque(float td_mm, float layer_height_mm);
|
||||
|
||||
// How many of the picture's layers actually change the colour.
|
||||
//
|
||||
// A band eventually stops saying anything: once it hides what is under it, another layer of it shows
|
||||
// the colour the one below already showed. Those layers still cost print time and still take up
|
||||
// relief height, and nothing about the stack makes them visible - the band looks like ten layers of
|
||||
// gradient and is, say, five of gradient and five of flat.
|
||||
//
|
||||
// Measured against the palette rather than against TD, because how long a band keeps paying depends
|
||||
// on what is under it as much as on the filament: a light band over a dark one is finished in four or
|
||||
// five layers, while a dark one over a light one goes on moving the colour perceptibly well past the
|
||||
// depth at which it is nominally opaque.
|
||||
int chroma_distinct_levels(const ChromaStack &stack);
|
||||
|
||||
// Shortens every band to its last layer that still changes the colour. Bands already shorter than
|
||||
// that are left alone: they are still inside their fade, where every layer counts.
|
||||
void chroma_trim_bands(ChromaStack &stack);
|
||||
|
||||
// The printed layer a point `z_mm` above the bottom of the object falls in: 1-based, clamped to the
|
||||
// stack, 0 for a stack that prints nothing. Pair it with chroma_palette() to get the colour shown
|
||||
// there.
|
||||
//
|
||||
// This, rather than the height map, is what decides the colour a surface shows. A filament swap
|
||||
// happens at one print Z across the whole plate, so every surface crossing that height changes colour
|
||||
// whatever the picture says - the terraces of the relief, the walls between them, and the sides of the
|
||||
// model alike. Colouring a preview this way is therefore not an approximation of the print; it is what
|
||||
// the print does, and it keeps working after the relief has been baked into the mesh and the height
|
||||
// map is no longer attached to anything on screen.
|
||||
int chroma_level_at(const ChromaStack &stack, double z_mm);
|
||||
|
||||
// The solve's height map as an 8-bit grayscale PNG, ready to be stored in a
|
||||
// TextureDisplacementLayer's `image_data`.
|
||||
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve);
|
||||
|
||||
// One filament swap as an object height range: the Z band, in the object's own coordinates (0 at its
|
||||
// bottom), that prints in `filament` (0-based).
|
||||
struct ChromaRange
|
||||
{
|
||||
double z_min = 0.;
|
||||
double z_max = 0.;
|
||||
int filament = -1;
|
||||
};
|
||||
|
||||
// The stack's bands as height ranges, in object coordinates, starting at `z_bottom` (which is 0 for
|
||||
// a stack whose base starts at the object's first layer). Bands printed in the same filament as the
|
||||
// band below are merged, so a stack that repeats a filament does not produce a pointless swap.
|
||||
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom = 0.);
|
||||
|
||||
// A starting point for a stack: the given filaments, lightest at the bottom, each with an equal
|
||||
// share of `relief_layers` on top of `base_layers`.
|
||||
//
|
||||
// Light-to-dark is the convention filament-painting guides teach, and it is not arbitrary: a dark
|
||||
// filament over a light one reads as a midtone at one or two layers and as the dark colour at four,
|
||||
// so putting the dark filaments last is what buys the long tonal ramp. The reverse order gives a few
|
||||
// washed-out steps and then nothing.
|
||||
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
|
||||
int base_layers, int relief_layers);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_ChromaStack_hpp_
|
||||
+27
-18
@@ -432,27 +432,36 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
std::string OozePrevention::pre_toolchange(GCode& gcodegen)
|
||||
{
|
||||
std::string gcode;
|
||||
|
||||
unsigned int extruder_id = gcodegen.writer().filament()->id();
|
||||
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
|
||||
if (filament_idle_temp.get_at(extruder_id) == 0) {
|
||||
// There is no idle temperature defined in filament settings.
|
||||
// Use the delta value from print config.
|
||||
if (gcodegen.config().standby_temperature_delta.value != 0) {
|
||||
// we assume that heating is always slower than cooling, so no need to block
|
||||
gcode += gcodegen.writer().set_temperature
|
||||
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
}
|
||||
} else {
|
||||
// Use the value from filament settings. That one is absolute, not delta.
|
||||
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
|
||||
// Check if this tool will ever be used again in this print.
|
||||
bool is_last_use = false;
|
||||
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
|
||||
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
|
||||
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
|
||||
const auto& cfg = gcodegen.config();
|
||||
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
|
||||
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
|
||||
for (unsigned int f : tool_ordering.all_extruders())
|
||||
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
|
||||
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
|
||||
}
|
||||
|
||||
int temp = 0;
|
||||
if (!is_last_use) {
|
||||
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
|
||||
if (filament_idle_temp.get_at(extruder_id) == 0) {
|
||||
if (gcodegen.config().standby_temperature_delta.value == 0)
|
||||
return std::string();
|
||||
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
|
||||
} else {
|
||||
temp = filament_idle_temp.get_at(extruder_id);
|
||||
}
|
||||
}
|
||||
|
||||
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
|
||||
gcode.pop_back();
|
||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
||||
return gcode;
|
||||
}
|
||||
|
||||
|
||||
@@ -1004,6 +1004,7 @@ protected:
|
||||
size_t num_objects,
|
||||
size_t num_islands);
|
||||
|
||||
friend class OozePrevention;
|
||||
friend class Wipe;
|
||||
friend class WipeTowerIntegration;
|
||||
friend class PressureEqualizer;
|
||||
|
||||
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
|
||||
sort_remove_duplicates(m_all_printing_extruders);
|
||||
}
|
||||
|
||||
m_last_layer_per_extruder.clear();
|
||||
for (const LayerTools& lt : m_layer_tools) {
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
m_last_layer_per_extruder[ext] = lt.print_z;
|
||||
}
|
||||
}
|
||||
|
||||
if (prime_multi_material && ! m_all_printing_extruders.empty()) {
|
||||
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
|
||||
// Then set m_first_printing_extruder to the 1st extruder primed.
|
||||
@@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
|
||||
{
|
||||
auto it = m_last_layer_per_extruder.find(extruder_id);
|
||||
if (it == m_last_layer_per_extruder.end())
|
||||
return true;
|
||||
return print_z >= it->second - EPSILON;
|
||||
}
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -370,6 +370,7 @@ public:
|
||||
std::vector<int> & initial_filaments);
|
||||
|
||||
bool has_non_support_filament(const PrintConfig &config);
|
||||
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
|
||||
|
||||
private:
|
||||
void initialize_layers(std::vector<coordf_t> &zs);
|
||||
@@ -413,6 +414,8 @@ private:
|
||||
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
|
||||
// no-ops; downstream emission falls through to the per-object branch).
|
||||
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
|
||||
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
|
||||
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
|
||||
const PrintObject* m_print_object_ptr = nullptr;
|
||||
Print* m_print;
|
||||
bool m_sorted = false;
|
||||
|
||||
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
|
||||
return decode_colored_png(stream, out_img);
|
||||
}
|
||||
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
struct ReadBufStream stream{pngbuf};
|
||||
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
|
||||
stream.read(sig.data(), PNG_SIG_BYTES);
|
||||
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
PNGDescr dsc;
|
||||
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!dsc.png)
|
||||
return false;
|
||||
dsc.info = png_create_info_struct(dsc.png);
|
||||
if (!dsc.info)
|
||||
return false;
|
||||
|
||||
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
|
||||
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
|
||||
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
|
||||
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
|
||||
bit_depth == 8 && (grey || colour);
|
||||
}
|
||||
|
||||
|
||||
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
|
||||
// Based on https://www.lemoda.net/c/write-png/
|
||||
|
||||
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
|
||||
|
||||
bool is_png(const ReadBuf &pngbuf);
|
||||
|
||||
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
|
||||
|
||||
struct ReadBufStream: public IStream {
|
||||
const ReadBuf &rbuf_ref;
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -1489,7 +1489,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
|
||||
"filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility",
|
||||
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
|
||||
"filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
|
||||
"filament_flow_ratio", "filament_density", "filament_transmission_distance", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
|
||||
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
|
||||
"filament_tower_interface_print_temp",
|
||||
"nozzle_temperature", "nozzle_temperature_initial_layer",
|
||||
|
||||
@@ -228,6 +228,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"filament_diameter",
|
||||
"volumetric_speed_coefficients",
|
||||
"filament_density",
|
||||
"filament_transmission_distance",
|
||||
"filament_cost",
|
||||
"filament_notes",
|
||||
"outer_wall_acceleration",
|
||||
|
||||
@@ -3440,6 +3440,20 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloats { 0. });
|
||||
|
||||
def = this->add("filament_transmission_distance", coFloats);
|
||||
def->label = L("Transmission distance");
|
||||
def->tooltip = L("How deep light gets into this filament, in millimetres, also called its "
|
||||
"transmission distance or TD. About 1 for an opaque filament, 4 to 7 for a usefully "
|
||||
"translucent one, 8 and above for a nearly clear one. Used only to predict the "
|
||||
"colours of a ChromaStack in the texture displacement tool; it has no effect on "
|
||||
"slicing. Measure it with a step test print, or take a published value for the "
|
||||
"filament.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloats { 4. });
|
||||
|
||||
def = this->add("filament_type", coStrings);
|
||||
def->label = L("Type");
|
||||
def->tooltip = L("Filament material type");
|
||||
|
||||
@@ -1709,6 +1709,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionStrings, volumetric_speed_coefficients))
|
||||
((ConfigOptionInts, filament_adhesiveness_category))
|
||||
((ConfigOptionFloats, filament_density))
|
||||
// Transmission distance: how deep light gets into this filament. Not used by
|
||||
// slicing - it is the colour model of a ChromaStack stack (see ChromaStack.hpp).
|
||||
((ConfigOptionFloats, filament_transmission_distance))
|
||||
((ConfigOptionStrings, filament_type))
|
||||
((ConfigOptionBools, filament_soluble))
|
||||
((ConfigOptionStrings, filament_ids))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "TextureDisplacement.hpp"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/SVD>
|
||||
#include <Eigen/Geometry>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -284,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
|
||||
return decode_height_texture(layer).has_color();
|
||||
}
|
||||
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
|
||||
{
|
||||
DecodedHeightTexture result;
|
||||
if (layer.empty())
|
||||
@@ -352,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
if (keep_decoded) {
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
}
|
||||
|
||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||
@@ -723,10 +724,7 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
||||
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
||||
if (tex.width <= 0)
|
||||
continue;
|
||||
// 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);
|
||||
const float texel = layer.tiling_scale / float(tex.width);
|
||||
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
||||
out.texel_mm = texel;
|
||||
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
||||
@@ -2168,7 +2166,7 @@ namespace {
|
||||
// of them was most of the per-sample arithmetic.
|
||||
struct UVTransform
|
||||
{
|
||||
float inv_u, inv_v, cs, sn, aspect;
|
||||
float scale, cs, sn, aspect;
|
||||
Vec2f offset;
|
||||
|
||||
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
|
||||
@@ -2176,18 +2174,7 @@ struct UVTransform
|
||||
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
cs = std::cos(rad);
|
||||
sn = std::sin(rad);
|
||||
@@ -2196,11 +2183,9 @@ struct UVTransform
|
||||
}
|
||||
Vec2f operator()(const Vec2f &planar) const
|
||||
{
|
||||
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
|
||||
// the rotation rather than preceding it.
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
rotated.x() *= inv_u;
|
||||
rotated.y() *= inv_v;
|
||||
const Vec2f scaled = planar * scale;
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
// See apply_uv_transform() for why the aspect correction follows the rotation.
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
return rotated + offset;
|
||||
@@ -2220,43 +2205,26 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
||||
|
||||
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 cs = std::cos(rad);
|
||||
const float sn = std::sin(rad);
|
||||
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();
|
||||
}
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
|
||||
// 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 one tile and every feature in it comes out half
|
||||
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
|
||||
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
|
||||
// changes nothing for the shipped library.
|
||||
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||
//
|
||||
// Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
|
||||
// already-rotated coordinate before turning it would be a shear.
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
@@ -3600,36 +3568,13 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
|
||||
(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)
|
||||
{
|
||||
// The model's own orientation, world scale, and the origin moved to where the volume's origin
|
||||
// sits. So the texture rides with the model however it is moved *and* however it is turned,
|
||||
// while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a
|
||||
// single untiled stamp starts on the model rather than at the plate's corner.
|
||||
//
|
||||
// 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;
|
||||
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
|
||||
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
|
||||
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
|
||||
// printed millimetres whatever the instance's scale.
|
||||
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
|
||||
@@ -5840,13 +5785,7 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
|
||||
|
||||
j["depth_mm"] = l.depth_mm;
|
||||
j["tiling_scale"] = l.tiling_scale;
|
||||
// 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["rotation_deg"] = l.rotation_deg;
|
||||
j["offset"] = vec2_to_json(l.offset);
|
||||
j["invert"] = l.invert;
|
||||
j["midlevel"] = l.midlevel;
|
||||
@@ -5954,12 +5893,6 @@ bool texture_displacement_layers_from_json(const std::string
|
||||
|
||||
read(j, "depth_mm", l.depth_mm);
|
||||
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);
|
||||
if (const auto o = j.find("offset"); o != j.end())
|
||||
l.offset = vec2_from_json(*o, l.offset);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "ChromaStack.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "TriangleSelector.hpp"
|
||||
|
||||
@@ -225,17 +226,7 @@ struct TextureDisplacementLayer
|
||||
std::shared_ptr<std::vector<unsigned char>> image_data;
|
||||
|
||||
float depth_mm = 0.4f; // maximum displacement along the surface normal, 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 tiling_scale = 12.5f; // size of one texture tile, in mm
|
||||
float rotation_deg = 0.f;
|
||||
Vec2f offset = Vec2f::Zero();
|
||||
bool invert = false;
|
||||
@@ -363,17 +354,33 @@ struct TextureDisplacementLayer
|
||||
// for the boundary to be drawn on.
|
||||
bool color_enabled = false;
|
||||
|
||||
// ChromaStack (see ChromaStack.hpp): when set, `image_data` is not a texture the user picked but the
|
||||
// height map chroma_solve() computed, so that the whole displacement pipeline - preview, UV
|
||||
// editor, bake - goes on reading one grayscale height map and needs to know nothing about colour
|
||||
// stacks. The picture it was solved from is kept here instead, because the solve has to be redone
|
||||
// whenever the stack, the print's layer height or the picture itself changes.
|
||||
//
|
||||
// A ChromaStack layer gets its colour from filament swaps at fixed Z, which is what `chroma`
|
||||
// records and what the object's height ranges carry out. That is a different mechanism from
|
||||
// `color_enabled` (per-triangle filament assignment), and the two are mutually exclusive: the
|
||||
// panel turns colour off on a ChromaStack layer, since painting the relief in one filament per
|
||||
// triangle would hide the very stack the heights were solved against.
|
||||
bool chroma_enabled = false;
|
||||
ChromaStack chroma;
|
||||
std::shared_ptr<std::vector<unsigned char>> chroma_source;
|
||||
|
||||
bool empty() const { return !image_data || image_data->empty(); }
|
||||
|
||||
template<class Archive> void save(Archive &ar) const
|
||||
{
|
||||
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
|
||||
std::string source = chroma_source ? std::string(chroma_source->begin(), chroma_source->end()) : std::string();
|
||||
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
|
||||
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
|
||||
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
|
||||
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, tiling_scale_v,
|
||||
mirror_u, mirror_v);
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, chroma_enabled, chroma,
|
||||
source);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -381,12 +388,14 @@ struct TextureDisplacementLayer
|
||||
int tile_method_int = 0;
|
||||
int projection_method_int = 0;
|
||||
int blend_mode_int = 0;
|
||||
std::string source;
|
||||
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
|
||||
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
|
||||
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
|
||||
view_project_matrix, color_enabled, chroma_enabled, chroma, source);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
chroma_source = source.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(source.begin(), source.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
blend_mode = static_cast<TextureBlendMode>(blend_mode_int);
|
||||
@@ -394,25 +403,6 @@ 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
|
||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||
struct TextureDisplacementOptions
|
||||
@@ -629,8 +619,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
|
||||
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
|
||||
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
|
||||
// as it did when the importer flattened it to grey on the way in. Returns an empty
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
|
||||
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
|
||||
|
||||
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
|
||||
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||
@@ -698,19 +689,14 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// 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).
|
||||
// `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: the tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
|
||||
// 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.
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
//
|
||||
// `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 tile extents (mm
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
|
||||
// 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, the mirroring and the offset are left.
|
||||
// the image's proportions) applies, and only the rotation and offset are left.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
|
||||
bool normalized_uv = false);
|
||||
|
||||
@@ -1082,10 +1068,6 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
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
|
||||
// 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);
|
||||
|
||||
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
|
||||
|
||||
@@ -842,6 +842,13 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
|
||||
|
||||
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
|
||||
|
||||
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
|
||||
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
|
||||
// is made absolute and lexically normalized first, because the prefix disables normalisation.
|
||||
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
|
||||
// or when an absolute path cannot be formed, the input is returned unchanged.
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#if WIN32
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#include "boost/nowide/convert.hpp"
|
||||
#endif
|
||||
|
||||
#include "I18N.hpp"
|
||||
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
||||
|
||||
FILE *f = nullptr;
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
f = boost::nowide::fopen(fname, mode);
|
||||
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
|
||||
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
|
||||
const std::wstring wide_path = boost::nowide::widen(fname);
|
||||
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
|
||||
f = boost::nowide::fopen(extended.c_str(), mode);
|
||||
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||
f = fopen64(fname, mode);
|
||||
#else
|
||||
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
// mz_file_write_func writing an extracted entry into an already-open FILE*.
|
||||
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
|
||||
{
|
||||
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
|
||||
@@ -116,6 +129,36 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
||||
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
||||
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
||||
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
|
||||
{
|
||||
if (!zip)
|
||||
return false;
|
||||
#ifdef WIN32
|
||||
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
|
||||
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
|
||||
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
|
||||
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
|
||||
if (!file) {
|
||||
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
if (!written || !closed) {
|
||||
if (written && !closed)
|
||||
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
|
||||
// Drop the partial entry rather than leave a truncated file behind.
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
|
||||
{
|
||||
if (stat.m_is_utf8)
|
||||
@@ -156,10 +199,13 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
}
|
||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||
try {
|
||||
// Use the extended-length form for every filesystem call so entries under a destination
|
||||
// past MAX_PATH are created even with LongPathsEnabled off.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
const boost::filesystem::path io_dest = extended_path(dest_path);
|
||||
if (stat.m_is_directory) {
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (!boost::filesystem::exists(dest_path))
|
||||
boost::filesystem::create_directories(dest_path);
|
||||
if (!boost::filesystem::exists(io_dest))
|
||||
boost::filesystem::create_directories(io_dest);
|
||||
continue;
|
||||
}
|
||||
if (stat.m_uncomp_size == 0) {
|
||||
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
continue;
|
||||
}
|
||||
// Replace a symlink at the destination rather than writing through it.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
||||
boost::filesystem::remove(dest_path);
|
||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
|
||||
boost::filesystem::remove(io_dest);
|
||||
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MINIZ_EXTENSION_HPP
|
||||
#define MINIZ_EXTENSION_HPP
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <string>
|
||||
#include <miniz.h>
|
||||
|
||||
@@ -13,6 +14,10 @@ bool close_zip_writer(mz_zip_archive *zip);
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
||||
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
|
||||
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
|
||||
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
|
||||
|
||||
class MZ_Archive {
|
||||
public:
|
||||
|
||||
@@ -2254,5 +2254,45 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
|
||||
return mod >= 0 && mod < count;
|
||||
}
|
||||
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path) {
|
||||
#ifdef WIN32
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Already extended, or an explicit device path (which is not subject to MAX_PATH and must not
|
||||
// be absolutized or normalized): leave it alone.
|
||||
{
|
||||
fs::path prefix_candidate = path;
|
||||
const std::wstring native = prefix_candidate.make_preferred().wstring();
|
||||
if (native.rfind(L"\\\\?\\", 0) == 0 || native.rfind(L"\\\\.\\", 0) == 0)
|
||||
return fs::path(native);
|
||||
}
|
||||
|
||||
fs::path absolute = path;
|
||||
if (!absolute.is_absolute()) {
|
||||
boost::system::error_code ec;
|
||||
absolute = fs::absolute(absolute, ec);
|
||||
if (ec)
|
||||
return path; // can't form a valid prefixed path from a relative one
|
||||
}
|
||||
|
||||
// A trailing separator is redundant to the Win32 APIs, but lexically_normal() would rewrite it
|
||||
// as a "." element, which the "\\?\" prefix (normalisation off) takes literally. Drop it first.
|
||||
while (absolute.has_relative_path() && absolute.filename() == ".")
|
||||
absolute = absolute.parent_path();
|
||||
|
||||
// The "\\?\" prefix turns off Win32 normalisation, so do it here first.
|
||||
absolute = absolute.lexically_normal();
|
||||
|
||||
// Extended paths only recognize backslash separators.
|
||||
const std::wstring native = absolute.make_preferred().wstring();
|
||||
if (native.empty())
|
||||
return path;
|
||||
if (native.rfind(L"\\\\", 0) == 0)
|
||||
return fs::path(L"\\\\?\\UNC\\" + native.substr(2)); // \\server\share\... -> \\?\UNC\server\share\...
|
||||
return fs::path(L"\\\\?\\" + native); // C:\dir\... -> \\?\C:\dir\...
|
||||
#else
|
||||
return path;
|
||||
#endif
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
||||
@@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Gizmos/GLGizmoUtils.hpp
|
||||
GUI/GLCanvas3D.cpp
|
||||
GUI/GLCanvas3D.hpp
|
||||
GUI/SequentialPrintClearance.hpp
|
||||
GUI/GLModel.cpp
|
||||
GUI/GLModel.hpp
|
||||
GUI/GLSelectionRectangle.cpp
|
||||
@@ -689,6 +690,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/host/PluginHostUi.hpp
|
||||
plugin/host/PluginPages.cpp
|
||||
plugin/host/PluginPages.hpp
|
||||
plugin/host/PluginResourceTextures.cpp
|
||||
plugin/host/PluginResourceTextures.hpp
|
||||
plugin/CloudPluginService.cpp
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
@@ -712,6 +715,9 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.hpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.cpp
|
||||
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.cpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
|
||||
|
||||
@@ -85,6 +85,96 @@ static std::string float_to_string_with_precision(float value, int precision = 3
|
||||
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)
|
||||
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
, m_color_picker_popup(ColorPickerPopup(this))
|
||||
@@ -740,8 +830,19 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
|
||||
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
|
||||
wxColour color = m_clr_picker->m_colour;
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
|
||||
std::string tray_color = colour_to_ams_string(color);
|
||||
std::vector<std::string> tray_colors;
|
||||
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()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
|
||||
@@ -760,13 +861,33 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
<< ", tray_info_idx (filament_id) = " << ams_filament_id
|
||||
<< ", setting_id = " << ams_setting_id
|
||||
<< ", tray_type = " << m_filament_type
|
||||
<< ", tray_color = " << col_buf
|
||||
<< ", tray_color = " << tray_color
|
||||
<< ", nozzle_temp_min = " << nozzle_temp_min_int
|
||||
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
|
||||
|
||||
// set filament
|
||||
if (m_is_third) {
|
||||
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);
|
||||
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,
|
||||
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
|
||||
@@ -923,8 +1044,15 @@ void AMSMaterialsSetting::set_ctype(int ctype)
|
||||
|
||||
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
|
||||
{
|
||||
unsigned int color_num = event.GetInt();
|
||||
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
|
||||
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
|
||||
if (colors.empty()) {
|
||||
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)
|
||||
@@ -939,14 +1067,12 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<wxColour> ams_colors;
|
||||
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
|
||||
|
||||
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
|
||||
m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id));
|
||||
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 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.set_ams_colours(ams_colors);
|
||||
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
|
||||
m_color_picker_popup.Popup();
|
||||
}
|
||||
|
||||
@@ -1735,7 +1861,46 @@ void ColorPicker::doRender(wxDC& dc)
|
||||
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
|
||||
if (m_selected) radius -= FromDIP(1);
|
||||
|
||||
if (alpha == 0) {
|
||||
if (m_show_full && m_cols.size() > 1) {
|
||||
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();
|
||||
int center_x = (size.x - bmp_size.x) / 2;
|
||||
int center_y = (size.y - bmp_size.y) / 2;
|
||||
@@ -1777,52 +1942,6 @@ void ColorPicker::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(wxColour("#6B6B6B")));
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
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) {
|
||||
@@ -1891,19 +2010,21 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
|
||||
//other
|
||||
wxFlexGridSizer* fg_sizer;
|
||||
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
|
||||
fg_sizer->SetFlexibleDirection(wxBOTH);
|
||||
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
|
||||
m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
|
||||
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
|
||||
|
||||
for (wxColour col : m_def_colors) {
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(col);
|
||||
cp->set_colors({col});
|
||||
cp->ctype = 2;
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
m_color_pickers.push_back(cp);
|
||||
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
m_default_color_pickers.push_back(cp);
|
||||
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour);
|
||||
|
||||
@@ -1970,7 +2091,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
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_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_other_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_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
|
||||
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
|
||||
@@ -1981,7 +2102,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_def_color_box->Fit();
|
||||
|
||||
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
|
||||
SetSizer(m_sizer_main);
|
||||
SetSizerAndFit(m_sizer_main);
|
||||
Layout();
|
||||
Fit();
|
||||
|
||||
@@ -2019,13 +2140,15 @@ void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
|
||||
{
|
||||
m_ams_fg_sizer->Clear(false);
|
||||
if (m_ams_color_pickers.size() > 0) {
|
||||
for (ColorPicker* col_pick:m_ams_color_pickers) {
|
||||
|
||||
std::vector<ColorPicker*>::iterator iter = 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);
|
||||
}
|
||||
@@ -2035,17 +2158,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
}
|
||||
|
||||
|
||||
m_ams_colors = ams;
|
||||
for (wxColour col : m_ams_colors) {
|
||||
m_ams_color_items = ams;
|
||||
for (const ColorItem& item : m_ams_color_items) {
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(col);
|
||||
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)));
|
||||
m_color_pickers.push_back(cp);
|
||||
m_ams_color_pickers.push_back(cp);
|
||||
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour);
|
||||
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);
|
||||
@@ -2058,9 +2186,68 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_def_colour(wxColour col)
|
||||
void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors)
|
||||
{
|
||||
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;
|
||||
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) {
|
||||
if (cp->m_selected) {
|
||||
@@ -2069,7 +2256,8 @@ void ColorPickerPopup::set_def_colour(wxColour col)
|
||||
}
|
||||
|
||||
for (ColorPicker* cp : m_color_pickers) {
|
||||
if (cp->m_colour == m_def_col) {
|
||||
if (!cp->IsShown()) continue;
|
||||
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
|
||||
cp->set_selected(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -82,14 +82,22 @@ public:
|
||||
class ColorPickerPopup : public PopupWindow
|
||||
{
|
||||
public:
|
||||
struct ColorItem
|
||||
{
|
||||
std::vector<wxColour> colors;
|
||||
int ctype = 2;
|
||||
wxString name;
|
||||
};
|
||||
|
||||
ScalableBitmap m_ts_bitmap_custom;
|
||||
wxStaticBitmap* m_ts_stbitmap_custom;
|
||||
StaticBox* m_custom_cp;
|
||||
StaticBox* m_def_color_box;
|
||||
wxFlexGridSizer* m_ams_fg_sizer;
|
||||
wxFlexGridSizer* m_other_fg_sizer;
|
||||
wxColour m_def_col;
|
||||
std::vector<wxColour> m_def_colors;
|
||||
std::vector<wxColour> m_ams_colors;
|
||||
std::vector<ColorItem> m_ams_color_items;
|
||||
std::vector<ColorPicker*> m_color_pickers;
|
||||
std::vector<ColorPicker*> m_ams_color_pickers;
|
||||
|
||||
@@ -97,13 +105,20 @@ public:
|
||||
ColorPickerPopup(wxWindow* parent);
|
||||
~ColorPickerPopup() {};
|
||||
void on_custom_clr_picker(wxMouseEvent& event);
|
||||
void set_ams_colours(std::vector<wxColour> ams);
|
||||
void set_def_colour(wxColour col);
|
||||
void set_ams_colours(const std::vector<ColorItem>& ams);
|
||||
void set_preset_colours(const std::vector<ColorItem>& preset_colors);
|
||||
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);
|
||||
virtual void OnDismiss() wxOVERRIDE;
|
||||
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
|
||||
|
||||
public:
|
||||
private:
|
||||
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,7 +747,9 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
|
||||
|
||||
// Orca: prevent resetting under gcode viewer mode
|
||||
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
|
||||
const auto plater = GUI::wxGetApp().mainframe->m_plater;
|
||||
// Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply()
|
||||
// (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()) {
|
||||
invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
|
||||
}
|
||||
|
||||
@@ -1798,7 +1798,8 @@ 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()));
|
||||
}
|
||||
|
||||
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 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, const std::vector<std::string>& tray_colors, int tray_ctype)
|
||||
{
|
||||
int tag_tray_id = 0;
|
||||
int tag_ams_id = ams_id;
|
||||
@@ -1827,6 +1828,10 @@ 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_max"] = nozzle_temp_max;
|
||||
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);
|
||||
}
|
||||
@@ -5478,6 +5483,7 @@ void MachineObject::parse_new_info(json print)
|
||||
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_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)) {
|
||||
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
|
||||
|
||||
@@ -674,6 +674,7 @@ public:
|
||||
bool is_support_remote_dry = false;
|
||||
bool is_support_check_track_switch_match_slice_printer{false};
|
||||
bool is_support_pa_mode{false};
|
||||
bool is_support_filament_manual_multi_color{false};
|
||||
std::optional<bool> m_firmware_support_print_tpu_left;
|
||||
|
||||
bool installed_upgrade_kit{false};
|
||||
@@ -804,7 +805,7 @@ public:
|
||||
int command_ams_switch_filament(bool switch_filament);
|
||||
int command_ams_air_print_detect(bool air_print_detect);
|
||||
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);
|
||||
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_select_tray(std::string tray_id);
|
||||
int command_ams_refresh_rfid(int ams_id, int slot_id);
|
||||
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>>();
|
||||
for (const auto& color_str : fila_color_strs) {
|
||||
if (color_str.size() > 3) /* Skip the value like "#0"*/{
|
||||
fila_color.m_colors.emplace(wxColour(color_str));
|
||||
fila_color.AddColor(wxColour(color_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,17 @@ struct FilamentColor
|
||||
|
||||
public:
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
#include <string_view>
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "SequentialPrintClearance.hpp"
|
||||
#ifdef SLIC3R_CAD
|
||||
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
|
||||
#endif
|
||||
@@ -6397,17 +6398,16 @@ void GLCanvas3D::update_sequential_clearance()
|
||||
//BBS: add the height logic
|
||||
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
||||
Polygons polygons;
|
||||
std::vector<std::pair<Polygon, float>> height_polygons;
|
||||
polygons.reserve(instances_count);
|
||||
height_polygons.reserve(instances_count);
|
||||
std::vector<struct height_info> convex_and_bounding_boxes;
|
||||
struct height_info
|
||||
{
|
||||
double instance_height;
|
||||
BoundingBox bounding_box;
|
||||
Polygon hull_polygon;
|
||||
};
|
||||
std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
|
||||
convex_and_bounding_boxes.reserve(instances_count);
|
||||
std::map<ObjectID, int> print_order;
|
||||
for (const PrintObject* print_object : fff_print()->objects())
|
||||
for (const PrintInstance& instance : print_object->instances())
|
||||
print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order);
|
||||
|
||||
for (size_t i = 0; i < instance_transforms.size(); ++i) {
|
||||
const size_t first_instance = convex_and_bounding_boxes.size();
|
||||
const auto& instances = instance_transforms[i];
|
||||
double rotation_z0 = instances.front()->get_rotation().z();
|
||||
int index = 0;
|
||||
@@ -6427,96 +6427,20 @@ void GLCanvas3D::update_sequential_clearance()
|
||||
Polygon convex_hull(std::move(inst_pts));
|
||||
BoundingBox bouding_box = convex_hull.bounding_box();
|
||||
BoundingBox plate_bb = plate->get_bounding_box_crd();
|
||||
const ObjectID instance_id = m_model->objects[i]->instances[index]->id();
|
||||
double instance_height = m_model->objects[i]->get_instance_max_z(index++);
|
||||
//skip the object for not current plate
|
||||
if (!plate_bb.overlap(bouding_box))
|
||||
continue;
|
||||
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull});
|
||||
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull, instance_id});
|
||||
polygons.emplace_back(std::move(convex_hull));
|
||||
}
|
||||
sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order);
|
||||
}
|
||||
|
||||
//sort the print instance
|
||||
std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(),
|
||||
[](auto &l, auto &r) {
|
||||
auto ly1 = l.bounding_box.min.y();
|
||||
auto ly2 = l.bounding_box.max.y();
|
||||
auto ry1 = r.bounding_box.min.y();
|
||||
auto ry2 = r.bounding_box.max.y();
|
||||
auto inter_min = std::max(ly1, ry1);
|
||||
auto inter_max = std::min(ly2, ry2);
|
||||
auto lx = l.bounding_box.min.x();
|
||||
auto rx = r.bounding_box.min.x();
|
||||
if (inter_max - inter_min > 0)
|
||||
return (lx < rx) || ((lx == rx)&&(ly1 < ry1));
|
||||
else
|
||||
return (ly1 < ry1);
|
||||
});
|
||||
|
||||
/*bool has_interlaced_objects = false;
|
||||
for (int k = 0; k < bounding_box_count; k++)
|
||||
{
|
||||
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
|
||||
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
|
||||
auto iy1 = bbox.min.y();
|
||||
auto iy2 = bbox.max.y();
|
||||
|
||||
for (int i = k+1; i < bounding_box_count; i++)
|
||||
{
|
||||
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
|
||||
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
|
||||
auto py1 = next_bbox.min.y();
|
||||
auto py2 = next_bbox.max.y();
|
||||
auto inter_min = std::max(iy1, py1); // min y of intersection
|
||||
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
|
||||
if (inter_max - inter_min > 0) {
|
||||
has_interlaced_objects = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (has_interlaced_objects)
|
||||
break;
|
||||
}*/
|
||||
|
||||
int bounding_box_count = convex_and_bounding_boxes.size();
|
||||
double printable_height = fff_print()->config().printable_height;
|
||||
double hc1 = fff_print()->config().extruder_clearance_height_to_lid;
|
||||
double hc2 = fff_print()->config().extruder_clearance_height_to_rod;
|
||||
for (int k = 0; k < bounding_box_count; k++)
|
||||
{
|
||||
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
|
||||
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
|
||||
auto iy1 = bbox.min.y();
|
||||
auto iy2 = bbox.max.y();
|
||||
double height = (k == (bounding_box_count - 1))?printable_height:hc1;
|
||||
|
||||
/*if (has_interlaced_objects) {
|
||||
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc2)) {
|
||||
height_polygons.emplace_back(std::make_pair(convex, hc2));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc1)) {
|
||||
height_polygons.emplace_back(std::make_pair(convex, hc1));
|
||||
}
|
||||
}*/
|
||||
|
||||
for (int i = k+1; i < bounding_box_count; i++)
|
||||
{
|
||||
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
|
||||
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
|
||||
auto py1 = next_bbox.min.y();
|
||||
auto py2 = next_bbox.max.y();
|
||||
auto inter_min = std::max(iy1, py1); // min y of intersection
|
||||
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
|
||||
if (inter_max - inter_min > 0) {
|
||||
height = hc2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (height < convex_and_bounding_boxes[k].instance_height)
|
||||
height_polygons.emplace_back(std::make_pair(convex, height));
|
||||
}
|
||||
const auto& config = fff_print()->config();
|
||||
const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height,
|
||||
config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod);
|
||||
|
||||
// sends instances 2d hulls to be rendered
|
||||
set_sequential_print_clearance_visible(true);
|
||||
|
||||
@@ -114,6 +114,8 @@ std::pair<bool, std::string> GLShadersManager::init()
|
||||
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
|
||||
// Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
|
||||
valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" });
|
||||
// ChromaStack: the printed colours on the model, read per fragment from its own height.
|
||||
valid &= append_shader("chromastack", { prefix + "chromastack.vs", prefix + "chromastack.fs" });
|
||||
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
|
||||
// painted patch, to sanity-check the unwrap.
|
||||
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
|
||||
|
||||
+108
-15
@@ -252,9 +252,11 @@
|
||||
#include "slic3r/GUI/ParamsDialog.hpp"
|
||||
#include "slic3r/GUI/ParamsPanel.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/TextureLibrary.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include "slic3r/plugin/host/PluginPages.hpp"
|
||||
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
|
||||
#include <wx/defs.h>
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#include <cwchar>
|
||||
@@ -333,6 +335,23 @@ static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent*
|
||||
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()
|
||||
{
|
||||
return;
|
||||
@@ -3063,6 +3082,37 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
refresh_plugins_dialog();
|
||||
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()
|
||||
@@ -5434,12 +5484,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
this->request_open_project({});
|
||||
}
|
||||
else if (command_str.compare("homepage_open_cloud") == 0) {
|
||||
// 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());
|
||||
});
|
||||
this->open_orca_cloud();
|
||||
}
|
||||
else if (command_str.compare("homepage_close_cloud") == 0) {
|
||||
CallAfter([]() {
|
||||
@@ -5588,6 +5633,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
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)
|
||||
{
|
||||
try {
|
||||
@@ -5865,7 +5926,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
|
||||
}*/
|
||||
}
|
||||
|
||||
wxString GUI_App::cloud_webview_handoff()
|
||||
wxString GUI_App::cloud_webview_handoff(const std::string& next)
|
||||
{
|
||||
// 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
|
||||
@@ -5873,7 +5934,7 @@ wxString GUI_App::cloud_webview_handoff()
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return {};
|
||||
return from_u8(orca->build_web_session_start_url());
|
||||
return from_u8(orca->build_web_session_start_url(next));
|
||||
}
|
||||
|
||||
void GUI_App::request_cloud_web_session(const std::string& state)
|
||||
@@ -6016,7 +6077,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
m_cloud_handoff_state.clear();
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
if (home->cloud_mode_active())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
|
||||
}
|
||||
|
||||
|
||||
@@ -6863,7 +6924,7 @@ void GUI_App::push_notification(const MachineObject* obj, wxString msg, wxStrin
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::reload_settings()
|
||||
bool GUI_App::reload_settings()
|
||||
{
|
||||
if (preset_bundle && m_agent) {
|
||||
// Load user's personal presets
|
||||
@@ -6871,7 +6932,7 @@ void GUI_App::reload_settings()
|
||||
int result = m_agent->get_user_presets(&user_presets);
|
||||
if (result != 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
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 {
|
||||
@@ -6946,13 +7007,30 @@ void GUI_App::reload_settings()
|
||||
refresh_synced_ui();
|
||||
else
|
||||
CallAfter(refresh_synced_ui);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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"))
|
||||
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();
|
||||
if (legacy_presets.empty()) {
|
||||
if (by_user)
|
||||
@@ -6997,6 +7075,9 @@ 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));
|
||||
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);
|
||||
mainframe->update_side_preset_ui();
|
||||
for (auto tab : tabs_list) {
|
||||
@@ -7763,6 +7844,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
|
||||
BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
|
||||
// BBS
|
||||
m_user_sync_token.reset(new int(0));
|
||||
m_user_preset_pull_pending = true;
|
||||
if (with_progress_dlg) {
|
||||
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores
|
||||
// repeat triggers while it is on screen (prevents stacking modal dialogs).
|
||||
@@ -7836,8 +7918,16 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
|
||||
|
||||
finishFn(ret == 0);
|
||||
|
||||
if (ret == 0 && m_agent && !t.expired())
|
||||
reload_settings();
|
||||
// Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded.
|
||||
if (ret == 0 && m_agent && !t.expired() && 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
|
||||
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
|
||||
@@ -7852,8 +7942,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
|
||||
// Sync once immediately, then every 60 seconds.
|
||||
while (!t.expired()) {
|
||||
++tick_tock;
|
||||
// Sync once immediately, then every 60s, or right away when a force-push asked for it.
|
||||
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
|
||||
// Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked
|
||||
// for it. The request is consumed on every tick, so one made before a scheduled pass is served by it
|
||||
// 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;
|
||||
if (m_agent) {
|
||||
if (!m_agent->is_user_login()) {
|
||||
|
||||
@@ -355,6 +355,9 @@ private:
|
||||
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_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};
|
||||
bool m_is_dark_mode{ false };
|
||||
bool m_adding_script_handler { false };
|
||||
@@ -365,6 +368,8 @@ private:
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
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 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
@@ -466,6 +471,9 @@ public:
|
||||
#if wxUSE_WEBVIEW_EDGE
|
||||
void init_webview_runtime();
|
||||
#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 bool dark_mode();
|
||||
const wxColour get_label_default_clr_system();
|
||||
@@ -572,7 +580,7 @@ public:
|
||||
// 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
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
wxString cloud_webview_handoff(const std::string& next = std::string());
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
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);
|
||||
@@ -589,7 +597,8 @@ public:
|
||||
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
|
||||
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
|
||||
// reminded; by_user also reports when there is nothing to migrate.
|
||||
// reminded; while the user presets sync with the cloud, it waits until they have been pulled. by_user also
|
||||
// reports when there is nothing to migrate.
|
||||
void check_legacy_user_filament_presets(bool by_user);
|
||||
bool process_network_msg(std::string dev_id, std::string msg);
|
||||
void request_new_version(int by_user);
|
||||
@@ -600,7 +609,8 @@ public:
|
||||
std::string format_IP(const std::string& ip);
|
||||
void show_dialog(wxString msg);
|
||||
void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL);
|
||||
void reload_settings();
|
||||
// Returns false when the cloud presets could not be fetched, so nothing was reloaded.
|
||||
bool reload_settings();
|
||||
void remove_user_presets();
|
||||
|
||||
bool maybe_migrate_user_presets_on_login();
|
||||
|
||||
@@ -2875,6 +2875,37 @@ void ObjectList::del_layer_from_object(const int obj_idx, const t_layer_height_r
|
||||
changed_object(obj_idx);
|
||||
}
|
||||
|
||||
void ObjectList::rebuild_layer_ranges(const int obj_idx)
|
||||
{
|
||||
if (obj_idx < 0 || obj_idx >= int(m_objects->size()))
|
||||
return;
|
||||
const wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx);
|
||||
if (!obj_item.IsOk())
|
||||
return;
|
||||
|
||||
const t_layer_config_ranges& ranges = object(obj_idx)->layer_config_ranges;
|
||||
// Adding and deleting tree items fires selection events on GTK; suppress them as every other
|
||||
// bulk edit of the tree here does.
|
||||
const bool prevent_events = m_prevent_list_events;
|
||||
m_prevent_list_events = true;
|
||||
wxDataViewItem root = m_objects_model->GetLayerRootItem(obj_item);
|
||||
if (root.IsOk()) {
|
||||
m_objects_model->DeleteChildren(root);
|
||||
if (ranges.empty())
|
||||
m_objects_model->Delete(root);
|
||||
else {
|
||||
for (const auto& range : ranges)
|
||||
add_layer_item(range.first, root);
|
||||
Expand(root);
|
||||
}
|
||||
} else {
|
||||
add_layer_root_item(obj_item);
|
||||
}
|
||||
m_prevent_list_events = prevent_events;
|
||||
|
||||
changed_object(obj_idx);
|
||||
}
|
||||
|
||||
void ObjectList::del_layers_from_object(const int obj_idx)
|
||||
{
|
||||
object(obj_idx)->layer_config_ranges.clear();
|
||||
|
||||
@@ -321,6 +321,11 @@ public:
|
||||
void del_settings_from_config(const wxDataViewItem& parent_item);
|
||||
void del_instances_from_object(const int obj_idx);
|
||||
void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range);
|
||||
// Mirrors an object's height ranges in the tree after they have been rewritten from outside the
|
||||
// list - the texture displacement gizmo writes a whole set at once for a ChromaStack stack's
|
||||
// filament swaps. Deliberately leaves the selection alone: changing it while a gizmo is open
|
||||
// tears the gizmo down under the user.
|
||||
void rebuild_layer_ranges(const int obj_idx);
|
||||
void del_layers_from_object(const int obj_idx);
|
||||
bool del_from_cut_object(bool is_connector, bool is_model_part = false, bool is_negative_volume = false);
|
||||
bool del_subobject_from_object(const int obj_idx, const int idx, const int type);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -264,7 +264,11 @@ private:
|
||||
ModelVolume* texture_volume();
|
||||
const ModelVolume* texture_volume() const;
|
||||
|
||||
void add_texture_layer();
|
||||
// `chroma` picks the kind: a relief layer, a height map pushed into the surface, or a ChromaStack
|
||||
// picture, a colour picture made of filament swaps. The kind is a property of the layer and is
|
||||
// fixed when it is created - there is no panel-wide mode, so a plaque can carry a picture on its
|
||||
// face and a knurl on its rim at the same time.
|
||||
void add_texture_layer(bool chroma = false);
|
||||
void remove_texture_layer(int slot);
|
||||
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
|
||||
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
|
||||
@@ -428,9 +432,116 @@ private:
|
||||
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
|
||||
int m_picker_slot = -1;
|
||||
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 import_custom_texture(TextureDisplacementLayer &layer);
|
||||
|
||||
// ---- ChromaStack (see libslic3r/ChromaStack.hpp) ----
|
||||
//
|
||||
// A ChromaStack layer is an ordinary texture displacement layer whose height map is computed rather
|
||||
// than imported: the picture the user picked is kept in `chroma_source`, the stack in
|
||||
// `chroma`, and `image_data` holds whatever the last solve produced. Everything downstream -
|
||||
// projection, preview, bake - therefore needs no changes at all, and this is the whole of the
|
||||
// feature's GUI side: pick a picture, edit the stack, solve, hand the filament swaps to the
|
||||
// object's height ranges.
|
||||
|
||||
// The project's loaded filaments as the colour model sees them: colour from the project, TD from
|
||||
// each filament's preset. Index is the filament index the swaps are written from.
|
||||
static std::vector<ChromaFilament> chroma_filaments();
|
||||
// The layer height the print profile is set to, mm; 0 when there is no print preset. The solve is
|
||||
// only valid for the layer height it was run at, so the panel compares it against the stack's.
|
||||
static float print_layer_height();
|
||||
|
||||
// Where the active layer's painted patch sits, in the object's own printed coordinates - which is
|
||||
// what the stack has to be lined up with, because a filament swap happens at a print Z and the
|
||||
// relief grows upward from this patch.
|
||||
struct ChromaPatch
|
||||
{
|
||||
bool valid = false;
|
||||
float top_mm = 0.f; // the patch's surface above the bottom of the object
|
||||
float spread_mm = 0.f; // how far from flat the patch is in Z; 0 is a horizontal face
|
||||
// The patch's extent in the plane the texture is projected from, in the bake frame - i.e. the
|
||||
// (x, y) the Z-facing plane of the tri-planar projection samples by. This is what the picture
|
||||
// has to be placed against: tiling_scale and offset are in that space, not in the patch's.
|
||||
Vec2f planar_min = Vec2f::Zero();
|
||||
Vec2f planar_max = Vec2f::Zero();
|
||||
};
|
||||
ChromaPatch chroma_patch(const TextureDisplacementLayer &layer) const;
|
||||
// The painted face's height in layers, which is what the stack's base has to be: -1 when nothing
|
||||
// is painted yet or there is no layer height to divide by.
|
||||
int chroma_measured_base(const TextureDisplacementLayer &layer) const;
|
||||
// Extracting the painted patch is far too expensive to repeat once a frame, which is how often the
|
||||
// panel wants the number. The paint's own timestamp and the transform the patch was measured in are
|
||||
// between them everything the answer depends on.
|
||||
mutable std::array<ChromaPatch, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_cache{};
|
||||
mutable std::array<uint64_t, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_stamp{};
|
||||
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_trafo{};
|
||||
// The patch is measured in two frames at once - the instance's, for the print height, and the
|
||||
// bake's, for the placement - so both are part of what the cached answer depends on.
|
||||
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_bake{};
|
||||
|
||||
// Picks a colour picture for the layer, turns ChromaStack on, and seeds a stack from the loaded
|
||||
// filaments and the patch if the layer has none yet. Solves straight away, so the relief appears
|
||||
// without a second click.
|
||||
void chroma_pick_picture(TextureDisplacementLayer &layer);
|
||||
// Re-runs the solve and replaces the layer's height map with its result. False (with the reason
|
||||
// shown to the user) when the layer has no picture, no stack or no relief layers.
|
||||
bool chroma_solve_layer(TextureDisplacementLayer &layer);
|
||||
// Writes the stack's filament swaps onto the object as height ranges, replacing whatever ranges it
|
||||
// had. Takes its own undo snapshot.
|
||||
void chroma_apply_swaps(const TextureDisplacementLayer &layer);
|
||||
// The stack's swaps as the object's height ranges already express them: true when applying would
|
||||
// change nothing, which is what the panel's hint keys off.
|
||||
bool chroma_swaps_applied(const TextureDisplacementLayer &layer) const;
|
||||
// The whole of a ChromaStack card between its header and More settings: the picture and the print
|
||||
// it predicts, the base, the filaments and the colours they reach, and the two actions.
|
||||
void render_chroma_card(TextureDisplacementLayer &layer, float card_pad, float label_w, float wrap_w);
|
||||
|
||||
// The surface drawn in the colours the stack will print, over whatever neutral surface is there:
|
||||
// the live preview's displaced mesh before a bake, the volume's own mesh after one. Every triangle
|
||||
// takes the palette colour of the printed layer its centroid falls in (chroma_level_at()), which
|
||||
// is what the eye will see at that height - so the relief's terraces read as the picture, the walls
|
||||
// between them show the bands they cut through, and looking straight down gives the blended colours
|
||||
// the whole feature exists to produce.
|
||||
//
|
||||
// Nothing else in the application can show this: the colours are blends of translucent filaments
|
||||
// that no single filament can make, so they cannot be expressed as the per-triangle filament
|
||||
// assignment the 3D scene paints with. They exist only here and in the sliced preview.
|
||||
void rebuild_chroma_colors(const TextureDisplacementLayer &layer);
|
||||
bool render_chroma_colors(const TextureDisplacementLayer &layer);
|
||||
GLModel m_chroma_color_model;
|
||||
// The surface the overlay was built from. The mesh alone: the colours are not in it, so a change
|
||||
// to the stack costs a palette upload rather than a mesh rebuild.
|
||||
size_t m_chroma_color_key = 0;
|
||||
// The stack's colours, one texel per printed layer, and what they were built from. A texture
|
||||
// because a uniform array cannot hold a base of several hundred layers.
|
||||
std::unique_ptr<GLTexture> m_chroma_palette_tex;
|
||||
size_t m_chroma_palette_key = 0;
|
||||
bool m_show_chroma_colors = true;
|
||||
|
||||
// What the last solve of each slot reported, for the panel. Not saved: it is a property of the
|
||||
// solve, and a project is reopened with whatever height map the solve produced.
|
||||
struct ChromaReport
|
||||
{
|
||||
bool valid = false;
|
||||
double mean = 0.;
|
||||
double max = 0.;
|
||||
};
|
||||
std::array<ChromaReport, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_report{};
|
||||
// The predicted print, per slot: the palette colours the solve chose, as a panel thumbnail. The
|
||||
// 3D view cannot show this - the colours do not exist until the print swaps filament - so it is
|
||||
// the only place the user sees what the stack will actually produce.
|
||||
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_preview;
|
||||
// The source picture's thumbnail, beside the prediction, keyed by the image it was built from.
|
||||
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_thumb;
|
||||
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_key{};
|
||||
// Per slot: the stack has been edited since the height map was solved, so the relief on screen is the
|
||||
// previous stack's. Nothing else can show this - a stale height map looks like a perfectly good one.
|
||||
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_stale{};
|
||||
|
||||
// Paint / Erase, from the panel. See get_left_button_state_type().
|
||||
bool m_erase_mode = false;
|
||||
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
||||
@@ -508,120 +619,25 @@ private:
|
||||
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
|
||||
void invalidate_adjust_textures();
|
||||
|
||||
// "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
|
||||
// on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
|
||||
// where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
|
||||
// tile extents - so each handle is one of the values it edits rather than a stand-in for it.
|
||||
// Anchored to the centroid/average-normal of the active layer's paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
|
||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
|
||||
// the centroid/average-normal of the active layer's current paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
|
||||
//
|
||||
// Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
|
||||
// it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
|
||||
// cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
|
||||
// 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();
|
||||
};
|
||||
|
||||
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
|
||||
// the texture should appear to move as the handle is dragged - it could not be visually
|
||||
// confirmed while writing it (no way to render/see pixels in this environment), so it may
|
||||
// need a one-line sign flip once actually tested.
|
||||
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);
|
||||
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
|
||||
// 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.
|
||||
void render_paint_cursor_hint();
|
||||
// Bake-frame tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
|
||||
// Mesh-local 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.
|
||||
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
||||
|
||||
@@ -1157,6 +1173,8 @@ private:
|
||||
|
||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||
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
|
||||
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
|
||||
@@ -1198,12 +1216,15 @@ private:
|
||||
// The config key behind m_undocked.
|
||||
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
|
||||
|
||||
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
||||
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
||||
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
|
||||
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
|
||||
// Smooth scrolling for a child window, called inside it: the panel body (everything between the header
|
||||
// and the pinned Bake footer) and the texture library's grid. ImGui jumps a fixed number of lines per
|
||||
// wheel notch, which on tall layer cards reads as a hard jolt rather than a scroll. The child sets
|
||||
// ImGuiWindowFlags_NoScrollWithMouse, and the wheel moves `target` instead; the real scroll is eased
|
||||
// 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_applied = -1.f; // what the easing wrote last frame; <0 until the first one
|
||||
float m_panel_scroll_applied = -1.f;
|
||||
// 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
|
||||
// as its content, capped so the footer still fits above the bottom of the canvas.
|
||||
@@ -1213,47 +1234,23 @@ private:
|
||||
// See the "Adjust Texture" block of private methods above.
|
||||
bool m_adjust_texture_mode = false;
|
||||
bool m_adjust_anchor_valid = false;
|
||||
// Mesh-local, as compute_layer_paint_anchor() produces them. Everything else about the gizmo
|
||||
// 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();
|
||||
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
|
||||
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
|
||||
|
||||
// What the cursor is over, and what is being dragged. `index` picks the arm or the corner
|
||||
// (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
|
||||
// Only ever one of each is set, so two handles can never both look live.
|
||||
// Which corner currently carries the rotate ring. Persists between frames, which is what gives
|
||||
// update_rotate_corner() something to apply its margin against.
|
||||
int m_adjust_rotate_corner = 0;
|
||||
AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
|
||||
int m_adjust_hover_index = -1;
|
||||
AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
|
||||
int m_adjust_drag_index = -1;
|
||||
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
|
||||
// the corresponding arrow, moves offset along only that one axis.
|
||||
enum class AdjustHandle { None, Pan, AxisU, AxisV };
|
||||
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
|
||||
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
|
||||
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
|
||||
// moment the current drag started; every subsequent frame's delta is measured against this,
|
||||
// rather than accumulated frame-to-frame, to avoid drift.
|
||||
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
|
||||
|
||||
// Everything the live drag is measured against, captured once on mouse-down. Every frame's
|
||||
// delta is taken from these rather than accumulated frame to frame, so a long drag cannot
|
||||
// drift away from the cursor.
|
||||
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;
|
||||
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
|
||||
// both the U and V axis arrows), transformed into place at render time.
|
||||
GLModel m_adjust_panel_glmodel;
|
||||
GLModel m_adjust_arrow_glmodel;
|
||||
|
||||
std::map<std::string, wxString> m_desc;
|
||||
|
||||
|
||||
@@ -83,11 +83,13 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
|
||||
if (url && *url && clicked)
|
||||
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(url && *url)
|
||||
ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url);
|
||||
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide") + "\n" + url, -1.f);
|
||||
else
|
||||
ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str());
|
||||
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide"), -1.f);
|
||||
}
|
||||
return clicked; // for dynamically generated links
|
||||
}
|
||||
@@ -101,9 +103,9 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
|
||||
if (ImGui::IsItemHovered()){
|
||||
if(url && *url)
|
||||
ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url);
|
||||
wxGetApp().imgui()->tooltip(_u8L("Video Guide") + "\n" + url, -1.f);
|
||||
else
|
||||
ImGui::SetTooltip("%s", _u8L("Video Guide").c_str());
|
||||
wxGetApp().imgui()->tooltip(_u8L("Video Guide"), -1.f);
|
||||
}
|
||||
return clicked; // for dynamically generated links
|
||||
}
|
||||
|
||||
@@ -857,11 +857,12 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
|
||||
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()) {
|
||||
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);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
// ORCA use TextColored to match axes color
|
||||
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
|
||||
@@ -964,11 +965,12 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
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();
|
||||
ImGui::PopID();
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
|
||||
if (ImGui::IsItemHovered()) {
|
||||
wxString tooltip_str = _L("Reference for alignment");
|
||||
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();
|
||||
int current_axis = -1;
|
||||
@@ -1377,11 +1379,12 @@ void GizmoObjectManipulation::do_render_scale_input_window(ImGuiWrapper* imgui_w
|
||||
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
|
||||
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()) {
|
||||
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);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
// ORCA use TextColored to match axes color
|
||||
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
|
||||
|
||||
@@ -505,10 +505,12 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt)
|
||||
io.MouseDown[0] = evt.LeftIsDown();
|
||||
io.MouseDown[1] = evt.RightIsDown();
|
||||
io.MouseDown[2] = evt.MiddleIsDown();
|
||||
float wheel_delta = static_cast<float>(evt.GetWheelDelta());
|
||||
if (wheel_delta != 0.0f) {
|
||||
// Only a vertical wheel event with a direction is a wheel step. A macOS trackpad reports sideways motion
|
||||
// as separate horizontal events, and its wheel events often carry a rotation of 0: wx truncates the
|
||||
// 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;
|
||||
}
|
||||
unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0);
|
||||
m_mouse_buttons = buttons;
|
||||
|
||||
|
||||
@@ -1739,6 +1739,9 @@ void MainFrame::fit_tab_labels()
|
||||
for (size_t i = count - 1; i >= 1; --i) {
|
||||
int right = ScreenToClient(m_slice_option_btn->ClientToScreen({})).x;
|
||||
int left = sizer->GetSize().GetWidth();
|
||||
// The overflow button sits between the tab buttons and the side tools, so it is not free space.
|
||||
if (const wxWindow* overflow = ctrl->GetOverflowButton())
|
||||
left += overflow->GetSize().GetWidth();
|
||||
if (right - left - FromDIP(15) > 0) return;
|
||||
ctrl->SetCompact(i, true);
|
||||
m_tabpanel->Refresh();
|
||||
|
||||
@@ -57,6 +57,7 @@ public:
|
||||
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
|
||||
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
|
||||
void SetOverflowButton(wxWindow* button);
|
||||
wxWindow* GetOverflowButton() const { return m_overflow_button; }
|
||||
|
||||
private:
|
||||
wxFlexGridSizer* m_buttons_sizer;
|
||||
|
||||
@@ -2225,8 +2225,10 @@ void PublishSettingsDialog::refresh_tab_indicators()
|
||||
if (section.variant_switch != nullptr) {
|
||||
const wxString& title = m_categories[section.categories[i]].title;
|
||||
const wxString text = on ? title + wxString(" ") + wxString(wxUniChar(0x2022)) : title;
|
||||
if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text)
|
||||
if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text) {
|
||||
section.variant_switch->SetOptionText(static_cast<unsigned int>(i), text);
|
||||
section.page->Layout();
|
||||
}
|
||||
}
|
||||
any = any || on;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
struct SequentialClearanceInstance
|
||||
{
|
||||
double instance_height;
|
||||
BoundingBox bounding_box;
|
||||
Polygon hull_polygon;
|
||||
ObjectID instance_id;
|
||||
};
|
||||
|
||||
// PrintApply may group rotated copies in a different order from ModelObject::instances.
|
||||
// Reorder one object's instances only when validation has assigned all of them an order.
|
||||
inline void sort_sequential_clearance_instances(
|
||||
std::vector<SequentialClearanceInstance>::iterator first, std::vector<SequentialClearanceInstance>::iterator last,
|
||||
const std::map<ObjectID, int>& print_order)
|
||||
{
|
||||
if (!std::all_of(first, last, [&print_order](const SequentialClearanceInstance& instance) {
|
||||
const auto order = print_order.find(instance.instance_id);
|
||||
return order != print_order.end() && order->second > 0;
|
||||
}))
|
||||
return;
|
||||
|
||||
std::stable_sort(first, last, [&print_order](const SequentialClearanceInstance& lhs, const SequentialClearanceInstance& rhs) {
|
||||
return print_order.at(lhs.instance_id) < print_order.at(rhs.instance_id);
|
||||
});
|
||||
}
|
||||
|
||||
// Input follows object-list order with validated print order within each object.
|
||||
inline std::vector<std::pair<Polygon, float>> sequential_clearance_height_polygons(
|
||||
const std::vector<SequentialClearanceInstance>& instances, double printable_height, double height_to_lid, double height_to_rod)
|
||||
{
|
||||
std::vector<std::pair<Polygon, float>> height_polygons;
|
||||
height_polygons.reserve(instances.size());
|
||||
for (size_t k = 0; k < instances.size(); ++k) {
|
||||
const auto& instance = instances[k];
|
||||
double height = k + 1 == instances.size() ? printable_height : height_to_lid;
|
||||
for (size_t i = k + 1; i < instances.size(); ++i) {
|
||||
const auto& next_bbox = instances[i].bounding_box;
|
||||
if (std::min(instance.bounding_box.max.y(), next_bbox.max.y()) >
|
||||
std::max(instance.bounding_box.min.y(), next_bbox.min.y())) {
|
||||
height = height_to_rod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (height < instance.instance_height)
|
||||
height_polygons.emplace_back(instance.hull_polygon, float(height));
|
||||
}
|
||||
return height_polygons;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -4538,6 +4538,7 @@ void TabFilament::build()
|
||||
optgroup->append_single_option_line("filament_adhesiveness_category", "material_basic_information#adhesiveness-category");
|
||||
|
||||
optgroup->append_single_option_line("filament_density", "material_basic_information#density");
|
||||
optgroup->append_single_option_line("filament_transmission_distance", "material_basic_information#transmission-distance");
|
||||
optgroup->append_single_option_line("filament_shrink", "material_basic_information#shrinkage-xy");
|
||||
optgroup->append_single_option_line("filament_shrinkage_compensation_z", "material_basic_information#shrinkage-z");
|
||||
optgroup->append_single_option_line("filament_cost", "material_basic_information#price");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TextureLibrary.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <cstddef>
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
@@ -21,6 +22,7 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <utility>
|
||||
#include <wx/image.h>
|
||||
@@ -36,6 +38,9 @@
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
#include "slic3r/plugin/PluginFsUtils.hpp"
|
||||
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
namespace {
|
||||
@@ -50,7 +55,7 @@ bool is_image_file(const boost::filesystem::path &path)
|
||||
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
|
||||
}
|
||||
|
||||
void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out)
|
||||
void scan_dir(const boost::filesystem::path &dir, TextureSource source, std::vector<TextureLibraryEntry> &out)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
if (!boost::filesystem::is_directory(dir, ec))
|
||||
@@ -60,12 +65,43 @@ void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<Text
|
||||
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()))
|
||||
continue;
|
||||
out.push_back({ it->path().stem().string(), it->path().string(), is_user });
|
||||
out.push_back({ it->path().stem().string(), it->path().string(), source });
|
||||
}
|
||||
std::sort(out.begin() + first, out.end(),
|
||||
[](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
|
||||
// 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
|
||||
@@ -187,27 +223,20 @@ std::vector<unsigned char> read_file_bytes(const std::string &path)
|
||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
|
||||
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is
|
||||
// the height and whose RGB is available to colour the model). Anything else is converted on load.
|
||||
// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is.
|
||||
// Anything else is converted on load.
|
||||
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
{
|
||||
if (bytes.empty())
|
||||
return false;
|
||||
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
|
||||
if (!png::is_png(rbuf))
|
||||
return false;
|
||||
png::ImageGreyscale gray;
|
||||
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
|
||||
return true;
|
||||
png::ImageColorscale color;
|
||||
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
|
||||
color.bytes_per_pixel >= 3 &&
|
||||
color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel);
|
||||
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
|
||||
}
|
||||
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
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 -
|
||||
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
|
||||
@@ -255,6 +284,8 @@ const size_t RECENT_MAX = 200;
|
||||
std::vector<std::string> recent_texture_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)
|
||||
return names;
|
||||
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
|
||||
@@ -269,6 +300,8 @@ std::vector<std::string> recent_texture_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)
|
||||
return;
|
||||
if (names.size() > RECENT_MAX)
|
||||
@@ -279,8 +312,9 @@ void store_recent_texture_names(std::vector<std::string> names)
|
||||
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
|
||||
}
|
||||
|
||||
// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep
|
||||
// the name order scan_dir() gave them, after every used one.
|
||||
// Reorders the user's entries by recent use. Stable, so textures never used keep the name order
|
||||
// scan_dir() gave them, after every used one. Only the user's own range is touched: shipped and pack
|
||||
// entries must keep the order their scans produced.
|
||||
void sort_user_textures(std::vector<TextureLibraryEntry> &library)
|
||||
{
|
||||
const std::vector<std::string> names = recent_texture_names();
|
||||
@@ -291,8 +325,11 @@ void sort_user_textures(std::vector<TextureLibraryEntry> &library)
|
||||
const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
|
||||
return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
|
||||
};
|
||||
const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; });
|
||||
std::stable_sort(first_user, library.end(),
|
||||
const auto first_user = std::find_if(library.begin(), library.end(),
|
||||
[](const TextureLibraryEntry &e) { return e.source == TextureSource::User; });
|
||||
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); });
|
||||
}
|
||||
|
||||
@@ -311,6 +348,16 @@ bool in_user_texture_dir(const std::string &path)
|
||||
|
||||
} // 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()
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
|
||||
@@ -325,17 +372,20 @@ std::string user_texture_dir()
|
||||
|
||||
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
|
||||
{
|
||||
if (g_library_scanned && !force_rescan)
|
||||
const uint64_t generation = texture_library_generation();
|
||||
if (g_library_scanned && !force_rescan && generation == g_library_scan_generation)
|
||||
return g_library;
|
||||
|
||||
g_library.clear();
|
||||
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library);
|
||||
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", TextureSource::Shipped, g_library);
|
||||
const std::string user_dir = user_texture_dir();
|
||||
if (!user_dir.empty())
|
||||
scan_dir(boost::filesystem::path(user_dir), true, g_library);
|
||||
scan_dir(boost::filesystem::path(user_dir), TextureSource::User, g_library);
|
||||
sort_user_textures(g_library);
|
||||
scan_plugin_textures(g_library);
|
||||
|
||||
g_library_scanned = true;
|
||||
g_library_scan_generation = generation;
|
||||
g_library_scanned = true;
|
||||
return g_library;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef slic3r_TextureLibrary_hpp_
|
||||
#define slic3r_TextureLibrary_hpp_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -10,26 +11,45 @@
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// One selectable height-map texture in the texture-displacement gizmo's texture picker.
|
||||
// Where a library texture came from. The picker lists each source under its own heading.
|
||||
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
|
||||
{
|
||||
std::string name; // display name (the file's stem, e.g. "Wood Grain")
|
||||
std::string path; // absolute path on disk
|
||||
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
|
||||
std::string name; // display name (the file's stem, e.g. "Wood Grain")
|
||||
std::string path; // absolute path on disk
|
||||
TextureSource source; // which of the three sources it came from
|
||||
|
||||
std::string pack_name; // contributing pack's display name; empty for Shipped and User
|
||||
};
|
||||
|
||||
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
|
||||
// by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see
|
||||
// touch_user_texture()), then any never used, by name.
|
||||
// Every available height-map texture, in the order the picker lists them: the ones shipped in
|
||||
// resources/textures/displacement first, by name; then the user's own from <data_dir>/textures/displacement,
|
||||
// most recently used first (see touch_user_texture()) and the rest by name; then the loaded texture
|
||||
// packs, ordered by pack display name and then by file name.
|
||||
//
|
||||
// The two live in separate directories deliberately: an app update replaces the resources tree
|
||||
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
|
||||
// delete. `is_user` is what the picker uses to show them under separate headings.
|
||||
// The shipped and user textures live in separate directories deliberately: an app update replaces the
|
||||
// resources tree wholesale, so anything the user imported has to sit somewhere that update can never
|
||||
// overwrite or delete. Pack textures are read-only like the shipped ones. Every texture carries its
|
||||
// `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
|
||||
// into either folder by hand while the app was running.
|
||||
// into either folder by hand while the app was running. A rescan also happens on its own when the pack
|
||||
// registry changes, which is what texture_library_generation() tracks.
|
||||
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
|
||||
// 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().
|
||||
|
||||
@@ -297,12 +297,9 @@ void UVEditorCanvas::set_island_fill_colors(std::vector<ColorRGBA> colors)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
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)
|
||||
void UVEditorCanvas::set_uv_transform(float tiling_scale, 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_v = (tiling_scale_v > 1e-6f) ? tiling_scale_v : m_tiling_scale;
|
||||
m_mirror_sign = mirror_sign;
|
||||
m_rotation_deg = rotation_deg;
|
||||
m_tile_enabled = tile_enabled;
|
||||
m_tile_mirrored = tile_mirrored;
|
||||
@@ -664,18 +661,11 @@ Vec2f UVEditorCanvas::island_centroid(int island) const
|
||||
|
||||
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
|
||||
{
|
||||
// apply_uv_transform() maps unwrap -> uv as uv = S^-1 R(unwrap) + offset, with S the tile
|
||||
// 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.
|
||||
// apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of
|
||||
// a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv).
|
||||
const float rad = m_rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad), sn = std::sin(rad);
|
||||
// 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);
|
||||
return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale;
|
||||
}
|
||||
|
||||
Vec2f UVEditorCanvas::snap_correction(int island) const
|
||||
@@ -2190,10 +2180,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
{
|
||||
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
|
||||
// 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 wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString();
|
||||
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
|
||||
m_layer_name->SetLabel(name);
|
||||
m_tile->SetLabel(tile);
|
||||
|
||||
@@ -91,10 +91,7 @@ public:
|
||||
// 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
|
||||
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
|
||||
// `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);
|
||||
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||
|
||||
// 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
|
||||
@@ -132,8 +129,7 @@ public:
|
||||
{
|
||||
bool has_layer = false; // an active layer mapped with Unwrap
|
||||
wxString layer_name;
|
||||
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
|
||||
float tile_mm = 0.f;
|
||||
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
|
||||
int thumbnail_px = 0;
|
||||
float seam_angle_deg = 40.f;
|
||||
@@ -296,10 +292,8 @@ private:
|
||||
GLModel m_grid_glmodel;
|
||||
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_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;
|
||||
float m_tiling_scale = 1.f;
|
||||
float m_rotation_deg = 0.f;
|
||||
bool m_tile_enabled = true;
|
||||
bool m_tile_mirrored = 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)
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString& page_url)
|
||||
{
|
||||
if (on) {
|
||||
// load_cloud_url records the origin from whatever it loads, including the plain cloud
|
||||
@@ -546,6 +546,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
create_cloud_browser();
|
||||
m_cloud_seed_url = 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) {
|
||||
// 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
|
||||
@@ -556,6 +560,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
load_cloud_after_signout(url);
|
||||
}
|
||||
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) {
|
||||
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
|
||||
// again (guide §10). Nothing is cleared here.
|
||||
|
||||
@@ -118,7 +118,10 @@ public:
|
||||
// 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
|
||||
// whether it already has a session; empty falls back to the plain cloud page.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString());
|
||||
// `page_url` is a full {web} page URL: an already-open pane is navigated straight there,
|
||||
// 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.
|
||||
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
#include <cstddef>
|
||||
#include <wx/simplebook.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/graphics.h>
|
||||
#include <memory>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/timer.h>
|
||||
@@ -1409,22 +1411,17 @@ void AMSLib::render_lite_lib(wxDC& dc)
|
||||
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
if (m_info.material_cols.size() > 1) {
|
||||
int left = FromDIP(10);
|
||||
int gwidth = std::round(libsize.x / (m_info.material_cols.size() - 1));
|
||||
//gradient
|
||||
wxRect color_rect(FromDIP(10), FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18));
|
||||
if (m_info.ctype == 0) {
|
||||
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;
|
||||
}
|
||||
fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back());
|
||||
}
|
||||
else {
|
||||
int cols_size = m_info.material_cols.size();
|
||||
for (int i = 0; i < cols_size; i++) {
|
||||
dc.SetBrush(wxBrush(m_info.material_cols[i]));
|
||||
float x = FromDIP(10) + ((float)libsize.x - FromDIP(18)) * i / cols_size;
|
||||
dc.DrawRoundedRectangle(x, FromDIP(10), ((float)libsize.x - FromDIP(17)) / cols_size, libsize.y - FromDIP(20), 0);
|
||||
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.SetBrush(wxBrush(tmp_lib_colour));
|
||||
}
|
||||
@@ -1516,7 +1513,27 @@ void AMSLib::render_generic_lib(wxDC &dc)
|
||||
int top = height - curr_height;
|
||||
|
||||
if (m_ams_model == EXT_AMS){
|
||||
dc.DrawRoundedRectangle(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1), m_radius - 1);
|
||||
wxRect color_rect(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(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) {
|
||||
dc.DrawBitmap(m_bitmap_transparent_def.bmp(), FromDIP(2), FromDIP(2));
|
||||
}
|
||||
@@ -2844,30 +2861,38 @@ 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));
|
||||
if (iter.material_cols.size() > 1)
|
||||
{
|
||||
int fleft = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2;
|
||||
|
||||
float total_width = AMS_ITEM_CUBE_SIZE.x;
|
||||
int gwidth = (total_width / (iter.material_cols.size()));
|
||||
if (iter.ctype == 0) {
|
||||
for (int i = 0; i < iter.material_cols.size() - 1; i++) {
|
||||
|
||||
if ((fleft + gwidth) > (AMS_ITEM_CUBE_SIZE.x)) {
|
||||
gwidth = (fleft + AMS_ITEM_CUBE_SIZE.x) - fleft;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const wxSize swatch_size = m_ams_item_type == AMSModel::N3S_AMS ? AMS_ITEM_CUBE_SIZE : AMS_ITEM_CUBE_SIZE2;
|
||||
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);
|
||||
std::unique_ptr<wxGraphicsContext> owned_gc;
|
||||
wxGraphicsContext* gc = dc.GetGraphicsContext();
|
||||
if (!gc) {
|
||||
owned_gc.reset(wxGraphicsContext::CreateFromUnknownDC(dc));
|
||||
gc = owned_gc.get();
|
||||
}
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -32,7 +32,7 @@ END_EVENT_TABLE()
|
||||
*/
|
||||
|
||||
#define TAB_BUTTON_SPACE 2
|
||||
#define TAB_BUTTON_PADDING_X 2
|
||||
#define TAB_BUTTON_PADDING_X 3
|
||||
#define TAB_BUTTON_PADDING_Y 2
|
||||
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
|
||||
|
||||
|
||||
@@ -74,6 +74,10 @@ webkit_web_view_run_javascript_finish (WebKitWebView
|
||||
GError **error);
|
||||
WEBKIT_API void
|
||||
webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
|
||||
WEBKIT_API guint
|
||||
webkit_get_major_version (void);
|
||||
WEBKIT_API guint
|
||||
webkit_get_minor_version (void);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -427,6 +431,29 @@ bool WebView::NeedsRecreateOnShow()
|
||||
return false;
|
||||
#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
|
||||
bool WebView::CheckWebViewRuntime()
|
||||
{
|
||||
|
||||
@@ -34,6 +34,16 @@ public:
|
||||
// ignoring every navigation. A panel that gets true here recreates its view on first Show().
|
||||
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();
|
||||
};
|
||||
|
||||
|
||||
@@ -529,6 +529,7 @@ std::string PluginConfig::capability_type_label(PluginCapabilityType type)
|
||||
case PluginCapabilityType::Visualization: return _u8L("Visualization");
|
||||
case PluginCapabilityType::Script: return _u8L("Script");
|
||||
case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline");
|
||||
case PluginCapabilityType::ResourceTexture: return _u8L("Resource Texture");
|
||||
default: return _u8L("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
#include <boost/filesystem/file_status.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
@@ -33,10 +34,6 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
const char* const INSTALL_STATE_FILE = ".install_state.json";
|
||||
@@ -664,7 +661,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(destination, ec);
|
||||
fs::create_directories(extended_path(destination), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin staging directory: " + ec.message();
|
||||
return false;
|
||||
@@ -708,7 +705,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
|
||||
const fs::path output_path = destination / relative_path;
|
||||
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
|
||||
fs::create_directories(output_path, ec);
|
||||
fs::create_directories(extended_path(output_path), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
|
||||
return false;
|
||||
@@ -716,26 +713,19 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_directories(output_path.parent_path(), ec);
|
||||
fs::create_directories(extended_path(output_path.parent_path()), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
|
||||
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
|
||||
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string encoded_output_path = encode_path(output_path.string().c_str());
|
||||
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
|
||||
#ifdef WIN32
|
||||
if (!extracted) {
|
||||
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
|
||||
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
|
||||
}
|
||||
#endif
|
||||
if (!extracted) {
|
||||
// Shared with the rest of the archive code: long-path capable and binary-safe.
|
||||
if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) {
|
||||
error = "Failed to extract plugin zip entry " + relative_key + ": " +
|
||||
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
@@ -1255,4 +1245,76 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
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
|
||||
|
||||
@@ -176,6 +176,12 @@ 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PythonPluginBridge.hpp"
|
||||
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
@@ -387,6 +388,37 @@ bool load(const PluginDescriptor& descriptor,
|
||||
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));
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
|
||||
#include "pluginTypes/pages/PagesPluginCapability.hpp"
|
||||
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
|
||||
#include "pluginTypes/script/ScriptPluginCapability.hpp"
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
#include <pybind11/pytypes.h>
|
||||
@@ -374,6 +375,7 @@ void bind_python_api(pybind11::module_& m)
|
||||
.value("Visualization", PluginCapabilityType::Visualization)
|
||||
.value("Script", PluginCapabilityType::Script)
|
||||
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
|
||||
.value("ResourceTexture", PluginCapabilityType::ResourceTexture)
|
||||
.value("Unknown", PluginCapabilityType::Unknown)
|
||||
.export_values();
|
||||
|
||||
@@ -527,6 +529,7 @@ void bind_python_api(pybind11::module_& m)
|
||||
PagesPluginCapability::RegisterBindings(m);
|
||||
ScriptPluginCapability::RegisterBindings(m);
|
||||
SlicingPipelinePluginCapability::RegisterBindings(m);
|
||||
ResourceTexturePluginCapability::RegisterBindings(m);
|
||||
PluginHost::RegisterBindings(m);
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, ResourceTexture, Unknown };
|
||||
|
||||
struct PluginCapabilityId
|
||||
{
|
||||
@@ -48,6 +48,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
case PluginCapabilityType::Visualization: return "visualization";
|
||||
case PluginCapabilityType::Script: return "script";
|
||||
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
|
||||
case PluginCapabilityType::ResourceTexture: return "resource-texture";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -63,6 +64,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
|
||||
case PluginCapabilityType::Visualization: return "Visualization";
|
||||
case PluginCapabilityType::Script: return "Script";
|
||||
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
|
||||
case PluginCapabilityType::ResourceTexture: return "Resource Texture";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -92,6 +94,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
|
||||
return PluginCapabilityType::Script;
|
||||
if (lowered == "slicing-pipeline")
|
||||
return PluginCapabilityType::SlicingPipeline;
|
||||
if (lowered == "resource-texture")
|
||||
return PluginCapabilityType::ResourceTexture;
|
||||
return PluginCapabilityType::Unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/Notebook.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/MainFrame.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
@@ -382,6 +383,7 @@ void PluginPages::relayout()
|
||||
m_parent->SelectPageByName(id_to_reselect);
|
||||
}
|
||||
|
||||
const bool overflow_changed = need_overflow != (m_overflow_button != nullptr);
|
||||
if (need_overflow) {
|
||||
if (m_overflow_button == nullptr) {
|
||||
auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER);
|
||||
@@ -398,6 +400,11 @@ void PluginPages::relayout()
|
||||
m_overflow_button->Destroy();
|
||||
m_overflow_button = nullptr;
|
||||
}
|
||||
|
||||
// The tab strip changed width; compact the labels again so the side tools stay visible.
|
||||
if ((!up_to_date || overflow_changed) && !GUI::wxGetApp().is_closing())
|
||||
if (GUI::MainFrame* mainframe = GUI::wxGetApp().mainframe)
|
||||
mainframe->fit_tab_labels();
|
||||
}
|
||||
|
||||
void PluginPages::select_page(const PluginCapabilityId& id)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#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
|
||||
@@ -0,0 +1,50 @@
|
||||
#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
|
||||
@@ -0,0 +1,32 @@
|
||||
#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
|
||||
@@ -0,0 +1,28 @@
|
||||
#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
@@ -0,0 +1,30 @@
|
||||
#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
|
||||
@@ -163,7 +163,7 @@ M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||
; CP TOOLCHANGE END
|
||||
M104 S240 T1 ; preheat T1 time: 30s lead 30.0s
|
||||
; CP TOOLCHANGE START
|
||||
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||
M104 S0 T0 ; set nozzle temperature ;cooldown
|
||||
T1 ; change extruder
|
||||
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||
; CP TOOLCHANGE END
|
||||
|
||||
@@ -1742,3 +1742,112 @@ TEST_CASE("Each filament cools with the fan speeds of its extruder variant", "[M
|
||||
CHECK(gcode.find("; start range high " + std::to_string(range_high) + "\n") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Shared heater on IMEX printer stays hot until all filaments on it are done", "[MultiFilament]")
|
||||
{
|
||||
DynamicPrintConfig cfg = multifilament_config(7);
|
||||
cfg.set_deserialize_strict({
|
||||
{"ooze_prevention", 1},
|
||||
{"preheat_time", 0},
|
||||
{"single_extruder_multi_material", 0},
|
||||
});
|
||||
imex_7x4_printer(cfg);
|
||||
|
||||
const std::string gcode = slice_with_object_overrides({make_cube(20., 20., 10.), make_cube(20., 20., 4.), make_cube(20., 20., 10.)},
|
||||
cfg,
|
||||
{{{"extruder", 1}, {"layer_height", 0.30}},
|
||||
{{"extruder", 2}, {"layer_height", 0.15}},
|
||||
{{"extruder", 5}}});
|
||||
|
||||
bool premature_s0_t0 = false;
|
||||
double current_z = 0.0;
|
||||
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) {
|
||||
if (line.has_z())
|
||||
current_z = line.z();
|
||||
|
||||
const std::string raw = line.raw();
|
||||
if (raw.find("M104 S0 T0") != std::string::npos && raw.find(";cooldown") != std::string::npos) {
|
||||
if (current_z < 9.0)
|
||||
premature_s0_t0 = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Filaments 1 and 2 share head 0, and filament 1 prints up to 10mm.
|
||||
// T0 must not shut down at ~4mm when filament 2 finishes.
|
||||
CHECK_FALSE(premature_s0_t0);
|
||||
}
|
||||
|
||||
TEST_CASE("Unused extruder turns off after its final layer on multi-object print", "[MultiFilament]")
|
||||
{
|
||||
const std::string gcode = slice_with_object_overrides(
|
||||
{ make_cube(20., 20., 10.), make_cube(20., 20., 4.) },
|
||||
multifilament_config(2, {
|
||||
{ "single_extruder_multi_material", 0 },
|
||||
{ "ooze_prevention", 1 },
|
||||
{ "standby_temperature_delta", -40 },
|
||||
{ "nozzle_temperature", "240,240" },
|
||||
}),
|
||||
{
|
||||
{ { "extruder", 1 }, { "layer_height", 0.20 } },
|
||||
{ { "extruder", 2 }, { "layer_height", 0.15 } }
|
||||
});
|
||||
|
||||
int t1_s0_cooldown_count = 0;
|
||||
int t0_s0_cooldown_count = 0;
|
||||
|
||||
std::istringstream stream(gcode);
|
||||
for (std::string line; std::getline(stream, line);) {
|
||||
if (line.find(";cooldown") != std::string::npos) {
|
||||
if (line.find("M104 S0 T1") != std::string::npos)
|
||||
++t1_s0_cooldown_count;
|
||||
if (line.find("M104 S0 T0") != std::string::npos)
|
||||
++t0_s0_cooldown_count;
|
||||
}
|
||||
}
|
||||
|
||||
// T1 (extruder 2, 4mm cube) gets exactly one S0 after its last layer.
|
||||
// T0 (extruder 1, 10mm cube) never gets S0 mid-print.
|
||||
CHECK(t1_s0_cooldown_count == 1);
|
||||
CHECK(t0_s0_cooldown_count == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Unused extruder turns off on non-IMEX three-tool print", "[MultiFilament]")
|
||||
{
|
||||
const std::string gcode = slice_with_object_overrides(
|
||||
{ make_cube(20., 20., 4.), make_cube(20., 20., 10.), make_cube(20., 20., 10.) },
|
||||
multifilament_config(3, {
|
||||
{ "single_extruder_multi_material", 0 },
|
||||
{ "ooze_prevention", 1 },
|
||||
{ "standby_temperature_delta", -40 },
|
||||
{ "nozzle_temperature", "240,240,240" },
|
||||
}),
|
||||
{
|
||||
{ { "extruder", 1 } },
|
||||
{ { "extruder", 2 } },
|
||||
{ { "extruder", 3 } }
|
||||
});
|
||||
|
||||
bool premature_s0_t1 = false;
|
||||
bool premature_s0_t2 = false;
|
||||
double current_z = 0.0;
|
||||
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) {
|
||||
if (line.has_z())
|
||||
current_z = line.z();
|
||||
|
||||
const std::string raw = line.raw();
|
||||
if (raw.find(";cooldown") != std::string::npos && current_z < 9.0) {
|
||||
if (raw.find("M104 S0 T1") != std::string::npos)
|
||||
premature_s0_t1 = true;
|
||||
if (raw.find("M104 S0 T2") != std::string::npos)
|
||||
premature_s0_t2 = true;
|
||||
}
|
||||
});
|
||||
|
||||
// T1 and T2 print up to 10mm — neither should get S0 before that.
|
||||
CHECK_FALSE(premature_s0_t1);
|
||||
CHECK_FALSE(premature_s0_t2);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill_plane_path.cpp
|
||||
test_fill_tpms_adaptive.cpp
|
||||
test_geometry.cpp
|
||||
test_chromastack.cpp
|
||||
test_imex_arrange.cpp
|
||||
test_imex_helpers.cpp
|
||||
test_imex_zones.cpp
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/ChromaStack.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
namespace {
|
||||
|
||||
ChromaFilament filament(int index, const ColorRGB &color, float td_mm)
|
||||
{
|
||||
ChromaFilament f;
|
||||
f.filament = index;
|
||||
f.color = color;
|
||||
f.td_mm = td_mm;
|
||||
return f;
|
||||
}
|
||||
|
||||
ChromaBand band(const ChromaFilament &f, int layers)
|
||||
{
|
||||
ChromaBand b;
|
||||
b.filament = f;
|
||||
b.layers = layers;
|
||||
return b;
|
||||
}
|
||||
|
||||
// An opaque filament (TD 1 hides what is under it within 0.1 mm) and a translucent one (TD 8 needs
|
||||
// most of a millimetre), which is the whole range the model has to behave sensibly over.
|
||||
const ChromaFilament WHITE_OPAQUE = filament(0, ColorRGB(1.f, 1.f, 1.f), 1.f);
|
||||
const ChromaFilament BLACK_OPAQUE = filament(1, ColorRGB(0.f, 0.f, 0.f), 1.f);
|
||||
const ChromaFilament BLACK_TRANSLUCENT = filament(2, ColorRGB(0.f, 0.f, 0.f), 8.f);
|
||||
|
||||
float luminance(const ColorRGB &c) { return 0.2126f * c.r() + 0.7152f * c.g() + 0.0722f * c.b(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A stack's palette has one entry per printed layer", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 4;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 10) };
|
||||
|
||||
// The base is backing under the picture, not part of a band: the bands are the 12 layers the
|
||||
// picture is built from, and the stack is those on top of the 4 the model already has.
|
||||
REQUIRE(stack.relief_layers() == 12);
|
||||
REQUIRE(stack.total_layers() == 16);
|
||||
REQUIRE_THAT(stack.depth_mm(), WithinAbs(12 * 0.08f, 1e-6f));
|
||||
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
REQUIRE(palette.size() == 17);
|
||||
// Index 0 is the base's own colour, so a column one layer tall is exactly its filament - whatever
|
||||
// that filament's transmission distance.
|
||||
REQUIRE(palette[0] == palette[1]);
|
||||
REQUIRE_THAT(luminance(palette[1]), WithinAbs(1.f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("A run of one filament gives the same palette however it is split into bands", "[ChromaStack]")
|
||||
{
|
||||
// The property that justifies compositing layer by layer at all: transmittance multiplies, so
|
||||
// cutting a run in two must not change the colours. A model without it would make the palette
|
||||
// depend on how the stack was typed in.
|
||||
ChromaStack one;
|
||||
one.layer_height_mm = 0.1f;
|
||||
one.base_layers = 0;
|
||||
one.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
|
||||
ChromaStack split = one;
|
||||
split.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 3), band(BLACK_TRANSLUCENT, 5) };
|
||||
|
||||
const std::vector<ColorRGB> a = chroma_palette(one);
|
||||
const std::vector<ColorRGB> b = chroma_palette(split);
|
||||
REQUIRE(a.size() == b.size());
|
||||
for (size_t i = 0; i < a.size(); ++i)
|
||||
REQUIRE_THAT(luminance(a[i]), WithinAbs(luminance(b[i]), 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("A translucent filament darkens gradually and an opaque one at once", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack translucent;
|
||||
translucent.layer_height_mm = 0.08f;
|
||||
translucent.base_layers = 0;
|
||||
translucent.bands = { band(WHITE_OPAQUE, 1), band(BLACK_TRANSLUCENT, 12) };
|
||||
const std::vector<ColorRGB> slow = chroma_palette(translucent);
|
||||
|
||||
// Monotonically darker, and still short of the filament's own black after a single layer: that
|
||||
// gradient is what a ChromaStack stack prints its midtones with.
|
||||
for (size_t i = 2; i < slow.size(); ++i)
|
||||
REQUIRE(luminance(slow[i]) < luminance(slow[i - 1]));
|
||||
REQUIRE(luminance(slow[2]) > 0.5f);
|
||||
// Twelve layers of a TD 8 filament pass under 3% of the light, which is the dark end of the ramp
|
||||
// rather than true black - and that is the honest prediction, not a rounding artifact.
|
||||
REQUIRE(luminance(slow.back()) < 0.25f);
|
||||
|
||||
ChromaStack opaque;
|
||||
opaque.layer_height_mm = 0.08f;
|
||||
opaque.base_layers = 0;
|
||||
opaque.bands = { band(WHITE_OPAQUE, 1), band(BLACK_OPAQUE, 12) };
|
||||
const std::vector<ColorRGB> fast = chroma_palette(opaque);
|
||||
// TD 1 is opaque within about 0.1 mm, so two layers of it leave little of the white showing.
|
||||
REQUIRE(luminance(fast[3]) < 0.2f);
|
||||
}
|
||||
|
||||
TEST_CASE("Solving a picture of the palette's own colors reproduces it exactly", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
const int relief = stack.relief_layers();
|
||||
|
||||
// One pixel per reachable height, in order.
|
||||
std::vector<uint8_t> rgb;
|
||||
for (int i = 0; i <= relief; ++i) {
|
||||
const ColorRGB &c = palette[size_t(stack.base_layers + i)];
|
||||
rgb.push_back(uint8_t(std::lround(c.r() * 255.f)));
|
||||
rgb.push_back(uint8_t(std::lround(c.g() * 255.f)));
|
||||
rgb.push_back(uint8_t(std::lround(c.b() * 255.f)));
|
||||
}
|
||||
|
||||
const ChromaSolve solve = chroma_solve(rgb.data(), relief + 1, 1, stack);
|
||||
REQUIRE_FALSE(solve.empty());
|
||||
REQUIRE(solve.width == relief + 1);
|
||||
REQUIRE(solve.levels.size() == size_t(relief + 1));
|
||||
// Every pixel comes back as the colour it was generated from, and the error is rounding only.
|
||||
//
|
||||
// The colour rather than the height: the bottom band prints in the same filament as the base, so
|
||||
// its first layers show exactly the colour the base already showed, and any of those heights is a
|
||||
// correct answer for that pixel. Asserting the height would be asserting which of several equally
|
||||
// right answers the search happens to reach first.
|
||||
for (int i = 0; i <= relief; ++i) {
|
||||
const int chosen = int(solve.levels[size_t(i)]);
|
||||
REQUIRE(chosen >= stack.base_layers);
|
||||
REQUIRE(chosen <= stack.total_layers());
|
||||
const ColorRGB &got = palette[size_t(chosen)];
|
||||
const ColorRGB &want = palette[size_t(stack.base_layers + i)];
|
||||
REQUIRE_THAT(got.r(), WithinAbs(want.r(), 2.f / 255.f));
|
||||
REQUIRE_THAT(got.g(), WithinAbs(want.g(), 2.f / 255.f));
|
||||
REQUIRE_THAT(got.b(), WithinAbs(want.b(), 2.f / 255.f));
|
||||
}
|
||||
// Only 8-bit rounding of the palette separates the two, which near black is still under one dE.
|
||||
REQUIRE(solve.max_delta_e < 2.);
|
||||
|
||||
// The height map's ends are the base and the top of the stack, which is what ties a grey level to
|
||||
// a printed layer.
|
||||
REQUIRE(solve.grey.front() == 0);
|
||||
REQUIRE(solve.grey.back() == 255);
|
||||
}
|
||||
|
||||
TEST_CASE("A picture the filaments cannot reach is reported as a poor match", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
|
||||
// Saturated red against a stack of white and black: no height can produce it, and the panel's
|
||||
// warning depends on the solve saying so rather than silently picking a grey.
|
||||
const std::vector<uint8_t> red{ 255, 0, 0 };
|
||||
const ChromaSolve solve = chroma_solve(red.data(), 1, 1, stack);
|
||||
REQUIRE_FALSE(solve.empty());
|
||||
REQUIRE(solve.mean_delta_e > 15.);
|
||||
}
|
||||
|
||||
TEST_CASE("Dithering keeps every pixel in the stack's range", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
stack.dither = true;
|
||||
|
||||
// A flat mid-grey: the error diffusion has to spread it over neighbouring heights without ever
|
||||
// leaving the stack, which is what a clamped palette index would quietly get wrong.
|
||||
std::vector<uint8_t> grey(size_t(16 * 16) * 3, 128);
|
||||
const ChromaSolve solve = chroma_solve(grey.data(), 16, 16, stack);
|
||||
REQUIRE_FALSE(solve.empty());
|
||||
REQUIRE(solve.levels.size() == size_t(16 * 16));
|
||||
const auto [lo, hi] = std::minmax_element(solve.levels.begin(), solve.levels.end());
|
||||
REQUIRE(int(*lo) >= stack.base_layers);
|
||||
REQUIRE(int(*hi) <= stack.total_layers());
|
||||
}
|
||||
|
||||
TEST_CASE("The solved height map is an 8-bit grayscale PNG of the picture's size", "[ChromaStack]")
|
||||
{
|
||||
// The displacement pipeline reads one thing from a layer: an 8-bit grayscale PNG. A solve that
|
||||
// produced anything else would leave the relief silently flat.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 6) };
|
||||
|
||||
std::vector<uint8_t> picture(size_t(8 * 4) * 3);
|
||||
for (size_t i = 0; i < picture.size(); ++i)
|
||||
picture[i] = uint8_t(i % 256);
|
||||
|
||||
const ChromaSolve solve = chroma_solve(picture.data(), 8, 4, stack);
|
||||
const std::vector<unsigned char> png = chroma_encode_height_png(solve);
|
||||
REQUIRE_FALSE(png.empty());
|
||||
|
||||
const png::ReadBuf buf{ png.data(), png.size() };
|
||||
png::ImageGreyscale decoded;
|
||||
REQUIRE(png::is_png(buf));
|
||||
REQUIRE(png::decode_png(buf, decoded));
|
||||
REQUIRE(decoded.cols == 8);
|
||||
REQUIRE(decoded.rows == 4);
|
||||
REQUIRE(decoded.buf == solve.grey);
|
||||
}
|
||||
|
||||
TEST_CASE("Each band becomes a height range, and a repeated filament is not a swap", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.1f;
|
||||
stack.base_layers = 0;
|
||||
stack.bands = { band(WHITE_OPAQUE, 5), band(BLACK_OPAQUE, 3), band(BLACK_OPAQUE, 2) };
|
||||
|
||||
const std::vector<ChromaRange> ranges = chroma_ranges(stack, 0.);
|
||||
// Three bands, two filaments: the second and third merge, because nothing is swapped between them.
|
||||
REQUIRE(ranges.size() == 2);
|
||||
REQUIRE_THAT(ranges[0].z_min, WithinAbs(0., 1e-9));
|
||||
REQUIRE_THAT(ranges[0].z_max, WithinAbs(0.5, 1e-9));
|
||||
REQUIRE(ranges[0].filament == WHITE_OPAQUE.filament);
|
||||
REQUIRE_THAT(ranges[1].z_min, WithinAbs(0.5, 1e-9));
|
||||
REQUIRE_THAT(ranges[1].z_max, WithinAbs(1.0, 1e-9));
|
||||
REQUIRE(ranges[1].filament == BLACK_OPAQUE.filament);
|
||||
}
|
||||
|
||||
TEST_CASE("A default stack puts the lightest filament at the bottom", "[ChromaStack]")
|
||||
{
|
||||
// Light to dark is what gives the long tonal ramp; the order is the one thing a seeded stack has
|
||||
// to get right, since everything else is a layer count the user can drag.
|
||||
const std::vector<ChromaFilament> loaded{ BLACK_OPAQUE, WHITE_OPAQUE,
|
||||
filament(3, ColorRGB(0.5f, 0.5f, 0.5f), 4.f) };
|
||||
const ChromaStack stack = chroma_default_stack(loaded, 0.08f, 4, 24);
|
||||
|
||||
REQUIRE(stack.bands.size() == 3);
|
||||
REQUIRE(stack.bands.front().filament.filament == WHITE_OPAQUE.filament);
|
||||
REQUIRE(stack.bands.back().filament.filament == BLACK_OPAQUE.filament);
|
||||
REQUIRE(stack.base_layers == 4);
|
||||
// The base is printed in the bottom band, so the relief is what was asked for and the base sits
|
||||
// underneath it.
|
||||
REQUIRE(stack.total_layers() == 4 + 24);
|
||||
REQUIRE(stack.relief_layers() == 24);
|
||||
REQUIRE_THAT(stack.layer_height_mm, WithinRel(0.08f));
|
||||
}
|
||||
|
||||
TEST_CASE("An empty stack or picture solves to nothing", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack empty;
|
||||
REQUIRE(chroma_palette(empty).empty());
|
||||
REQUIRE(chroma_ranges(empty, 0.).empty());
|
||||
|
||||
// A base with nothing on it: there is no picture to solve, and no filament to print the base in.
|
||||
ChromaStack base_only;
|
||||
base_only.layer_height_mm = 0.08f;
|
||||
base_only.base_layers = 4;
|
||||
REQUIRE(base_only.relief_layers() == 0);
|
||||
REQUIRE(chroma_palette(base_only).empty());
|
||||
const std::vector<uint8_t> pixel{ 128, 128, 128 };
|
||||
REQUIRE(chroma_solve(pixel.data(), 1, 1, base_only).empty());
|
||||
|
||||
ChromaStack usable;
|
||||
usable.layer_height_mm = 0.08f;
|
||||
usable.base_layers = 2;
|
||||
usable.bands = { band(WHITE_OPAQUE, 2), band(BLACK_OPAQUE, 4) };
|
||||
REQUIRE(chroma_solve(nullptr, 4, 4, usable).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("The base extends the bottom band's range down to the first layer", "[ChromaStack]")
|
||||
{
|
||||
// The base is not a swap of its own: it prints in whatever is loaded at the start, which is the
|
||||
// bottom band's filament, so the first range has to reach the build plate.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.1f;
|
||||
stack.base_layers = 20;
|
||||
stack.bands = { band(WHITE_OPAQUE, 5), band(BLACK_OPAQUE, 5) };
|
||||
|
||||
const std::vector<ChromaRange> ranges = chroma_ranges(stack, 0.);
|
||||
REQUIRE(ranges.size() == 2);
|
||||
REQUIRE_THAT(ranges[0].z_min, WithinAbs(0., 1e-9));
|
||||
REQUIRE_THAT(ranges[0].z_max, WithinAbs(2.5, 1e-9)); // 20 base + 5 band layers at 0.1 mm
|
||||
REQUIRE_THAT(ranges[1].z_min, WithinAbs(2.5, 1e-9));
|
||||
REQUIRE_THAT(ranges[1].z_max, WithinAbs(3.0, 1e-9));
|
||||
REQUIRE(ranges.back().z_max == Catch::Approx(double(stack.total_layers()) * double(stack.layer_height_mm)));
|
||||
}
|
||||
|
||||
TEST_CASE("A surface's color follows the printed layer it sits at", "[ChromaStack]")
|
||||
{
|
||||
// What the preview colours by: a swap happens at one print Z, so the layer a surface sits in is
|
||||
// what decides its colour, whatever the height map says.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.1f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 3), band(BLACK_OPAQUE, 5) };
|
||||
|
||||
REQUIRE(chroma_level_at(stack, 0.05) == 1); // inside the first layer
|
||||
REQUIRE(chroma_level_at(stack, 0.10) == 1); // exactly its top, which that layer printed
|
||||
REQUIRE(chroma_level_at(stack, 0.15) == 2);
|
||||
REQUIRE(chroma_level_at(stack, 0.50) == 5); // the top of the base plus the white band
|
||||
// Clamped at both ends: below the plate and above the stack there is nothing else to show.
|
||||
REQUIRE(chroma_level_at(stack, -1.) == 1);
|
||||
REQUIRE(chroma_level_at(stack, 99.) == stack.total_layers());
|
||||
}
|
||||
|
||||
TEST_CASE("How fast a filament covers is its TD against the layer height", "[ChromaStack]")
|
||||
{
|
||||
// The number that decides whether a band is a gradient or a step, and the one the panel reports:
|
||||
// a filament hides what is under it at about a tenth of its TD, so how many layers that is
|
||||
// depends entirely on how thick they are.
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(4.f, 0.2f), WithinRel(2.f, 1e-4f));
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(4.f, 0.08f), WithinRel(5.f, 1e-4f));
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(8.f, 0.08f), WithinRel(10.f, 1e-4f));
|
||||
// An opaque filament at a coarse layer height covers within one layer, which is where a stack
|
||||
// stops having any colours between its filaments at all.
|
||||
REQUIRE(chroma_layers_to_opaque(1.f, 0.2f) < 1.f);
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(4.f, 0.f), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("A finer layer height buys colors between the filaments", "[ChromaStack]")
|
||||
{
|
||||
// The whole of the user-visible problem in one assertion: the same stack, the same filaments, two
|
||||
// layer heights. The coarse one steps from white to black in two layers; the fine one fades.
|
||||
const auto distinct = [](float layer_height) {
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = layer_height;
|
||||
stack.base_layers = 0;
|
||||
stack.bands = { band(WHITE_OPAQUE, 1), band(filament(5, ColorRGB(0.f, 0.f, 0.f), 8.f), 10) };
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
int n = 0;
|
||||
for (size_t i = 1; i < palette.size(); ++i)
|
||||
if (std::abs(luminance(palette[i]) - luminance(palette[i - 1])) > 0.02f)
|
||||
++n;
|
||||
return n;
|
||||
};
|
||||
REQUIRE(distinct(0.08f) > distinct(0.3f));
|
||||
}
|
||||
|
||||
TEST_CASE("Layers past a filament's covering depth add no color", "[ChromaStack]")
|
||||
{
|
||||
// The band is twice as long as the filament needs to hide what is under it, so half of it shows a
|
||||
// colour the layer below already showed. That is invisible in the stack and obvious here.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 0;
|
||||
stack.bands = { band(WHITE_OPAQUE, 1), band(filament(5, ColorRGB(0.f, 0.f, 0.f), 4.f), 10) };
|
||||
|
||||
// Trimming keeps every layer that still changes the colour and drops the rest, so the stack gets
|
||||
// shorter and loses none of its colours.
|
||||
const int distinct = chroma_distinct_levels(stack);
|
||||
const int before = stack.relief_layers();
|
||||
chroma_trim_bands(stack);
|
||||
REQUIRE(stack.relief_layers() <= before);
|
||||
REQUIRE(chroma_distinct_levels(stack) == distinct);
|
||||
// The white band is a single layer and has nothing to give up.
|
||||
REQUIRE(stack.bands.front().layers == 1);
|
||||
|
||||
// A light band over a dark one is the case where layers really are wasted: it saturates within a
|
||||
// few layers, and the rest of a long band shows what the layer below already showed.
|
||||
ChromaStack light_over_dark;
|
||||
light_over_dark.layer_height_mm = 0.08f;
|
||||
light_over_dark.base_layers = 0;
|
||||
light_over_dark.bands = { band(filament(5, ColorRGB(0.f, 0.f, 0.f), 4.f), 1), band(WHITE_OPAQUE, 14) };
|
||||
const int kept = chroma_distinct_levels(light_over_dark);
|
||||
chroma_trim_bands(light_over_dark);
|
||||
REQUIRE(light_over_dark.bands.back().layers < 14);
|
||||
REQUIRE(chroma_distinct_levels(light_over_dark) == kept);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <miniz.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -176,6 +177,55 @@ TEST_CASE("Confined extraction writes nothing outside the target for Windows-spe
|
||||
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
|
||||
}
|
||||
|
||||
TEST_CASE("Confined extraction writes files under a target path longer than MAX_PATH", "[MinizExtension]")
|
||||
{
|
||||
ScopedTemporaryDir tmp;
|
||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||
// Directory entries are explicit: extract_archive_confined recreates directories only for them.
|
||||
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"},
|
||||
{"vendor/machine/printer.json", "{\"b\":2}"}});
|
||||
|
||||
// One 240-char component keeps every name below the NTFS 255 limit while pushing the full
|
||||
// target path past the 260-char MAX_PATH the Win32 APIs enforce when LongPathsEnabled is off.
|
||||
const fs::path target = tmp.path() / std::string(240, 'd');
|
||||
REQUIRE(target.string().size() > 260);
|
||||
fs::create_directories(extended_path(target));
|
||||
|
||||
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
||||
// Verify through extended-length paths, otherwise the checks themselves are bounded by MAX_PATH.
|
||||
CHECK(fs::is_directory(extended_path(target / "vendor")));
|
||||
CHECK(read_file(extended_path(target / "vendor.json")) == "{\"a\":1}");
|
||||
CHECK(read_file(extended_path(target / "vendor" / "machine" / "printer.json")) == "{\"b\":2}");
|
||||
|
||||
// The temporary dir guard cannot remove a deep tree on Windows, so drop it here.
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(extended_path(target), ec);
|
||||
CHECK_FALSE(ec);
|
||||
}
|
||||
|
||||
TEST_CASE("A zip archive under a path longer than MAX_PATH can be written and read", "[MinizExtension]")
|
||||
{
|
||||
ScopedTemporaryDir tmp;
|
||||
|
||||
// Create the long directory with the extended-length path, then exercise the archive
|
||||
// reader/writer against an archive whose own path is past MAX_PATH.
|
||||
const fs::path long_dir = tmp.path() / std::string(240, 'z');
|
||||
fs::create_directories(extended_path(long_dir));
|
||||
const fs::path zip_file = long_dir / "bundle.zip";
|
||||
REQUIRE(zip_file.string().size() > 260);
|
||||
|
||||
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
|
||||
|
||||
const fs::path target = tmp.path() / "out";
|
||||
fs::create_directories(target);
|
||||
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
||||
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(extended_path(long_dir), ec);
|
||||
CHECK_FALSE(ec);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
|
||||
{
|
||||
|
||||
@@ -521,3 +521,49 @@ TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]")
|
||||
CHECK_FALSE(ascii_iequals("[", "{"));
|
||||
CHECK_FALSE(ascii_iequals("@", "`"));
|
||||
}
|
||||
|
||||
TEST_CASE("extended_path turns a Windows path into its long-path form", "[Utils]") {
|
||||
#ifdef _WIN32
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
SECTION("a normal absolute drive path is prefixed") {
|
||||
CHECK(extended_path(fs::path(R"(C:\models\model.3mf)")).wstring() == LR"(\\?\C:\models\model.3mf)");
|
||||
}
|
||||
SECTION("separators are made native") {
|
||||
CHECK(extended_path(fs::path("C:/models/model.3mf")).wstring() == LR"(\\?\C:\models\model.3mf)");
|
||||
}
|
||||
SECTION("dot segments are collapsed before prefixing") {
|
||||
// The "\\?\" prefix disables Win32 normalisation, so "." and ".." must be gone beforehand.
|
||||
CHECK(extended_path(fs::path(R"(C:\models\..\model.3mf)")).wstring() == LR"(\\?\C:\model.3mf)");
|
||||
}
|
||||
SECTION("a trailing separator is dropped, not turned into a dot element") {
|
||||
CHECK(extended_path(fs::path(R"(C:\models\)")).wstring() == LR"(\\?\C:\models)");
|
||||
}
|
||||
SECTION("a UNC path becomes an extended UNC path") {
|
||||
CHECK(extended_path(fs::path(R"(\\server\share\model.3mf)")).wstring() == LR"(\\?\UNC\server\share\model.3mf)");
|
||||
}
|
||||
SECTION("an already extended path is returned unchanged") {
|
||||
const fs::path extended(R"(\\?\C:\models\model.3mf)");
|
||||
CHECK(extended_path(extended) == extended);
|
||||
}
|
||||
SECTION("a device path is returned unchanged") {
|
||||
const fs::path device(R"(\\.\C:\models\model.3mf)");
|
||||
CHECK(extended_path(device) == device);
|
||||
}
|
||||
SECTION("a bare device path without a root directory is returned unchanged") {
|
||||
// Not "absolute" by Boost's rules, so it must be recognized before absolutizing.
|
||||
const fs::path device(R"(\\.\PhysicalDrive0)");
|
||||
CHECK(extended_path(device) == device);
|
||||
}
|
||||
SECTION("a relative path is completed against the working directory") {
|
||||
const fs::path result = extended_path(fs::path("model.3mf"));
|
||||
CHECK(result.wstring().rfind(L"\\\\?\\", 0) == 0);
|
||||
CHECK(result.filename() == fs::path("model.3mf"));
|
||||
}
|
||||
#else
|
||||
SECTION("non-Windows returns the path unchanged") {
|
||||
const boost::filesystem::path p = boost::filesystem::temp_directory_path() / "model.3mf";
|
||||
CHECK(extended_path(p) == p);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_printer_agent.cpp
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_plugin_resource_texture.cpp
|
||||
test_plugin_printer_agent.cpp
|
||||
test_printhost.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
@@ -44,7 +45,9 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_texture_color_palette.cpp
|
||||
test_texture_island_view.cpp
|
||||
test_file_url.cpp
|
||||
test_imgui_wrapper.cpp
|
||||
test_user_manager.cpp
|
||||
test_sequential_clearance.cpp
|
||||
../fff_print/test_helpers.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
|
||||
#include <slic3r/GUI/DeviceCore/DevManager.h>
|
||||
#include <slic3r/GUI/DeviceManager.hpp>
|
||||
@@ -16,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using json = nlohmann::json;
|
||||
@@ -49,6 +51,21 @@ public:
|
||||
|
||||
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:
|
||||
AgentInfo m_info;
|
||||
};
|
||||
@@ -160,3 +177,62 @@ 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-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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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));
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <ios>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <slic3r/plugin/PluginLoader.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
@@ -10,13 +11,17 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
#include "catch2/catch_message.hpp"
|
||||
#include "plugin_test_utils.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <libslic3r/Zipper.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -31,6 +36,14 @@ fs::path write_py_file(const fs::path& dir, const std::string& filename, const s
|
||||
return p;
|
||||
}
|
||||
|
||||
// The shared production helper: makes the path absolute and lexically normalized and, on
|
||||
// Windows, prefixes it with the extended-length "\\?\" form, so the test can create, verify and
|
||||
// clean up a tree past MAX_PATH even with LongPathsEnabled off. Identity on other platforms.
|
||||
fs::path to_extended_path(const fs::path& p)
|
||||
{
|
||||
return Slic3r::extended_path(p);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[PluginInstall]")
|
||||
@@ -184,3 +197,59 @@ TEST_CASE("an unsubscribed cloud plugin follows its header version", "[PluginIns
|
||||
REQUIRE(read_install_state(plugin_dir, scanned));
|
||||
CHECK(scanned.installed_version == "1.0.0");
|
||||
}
|
||||
|
||||
TEST_CASE("extract_zip_to_directory writes entries under a destination longer than MAX_PATH",
|
||||
"[PluginInstall][PluginFsUtils]")
|
||||
{
|
||||
ScopedTemporaryDir dir("deep-extract");
|
||||
|
||||
const fs::path wheel = dir.path() / "deep_pkg-1.0-py3-none-any.whl";
|
||||
const std::string payload = "print('hello from a deep path')\n";
|
||||
{
|
||||
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
|
||||
zipper.add_entry("pkg/", "", 0); // explicit directory entry
|
||||
zipper.add_entry("pkg/module.py", payload.data(), payload.size());
|
||||
zipper.finalize();
|
||||
}
|
||||
|
||||
// A single 240-char component keeps every filename below the NTFS 255 limit while pushing the
|
||||
// full destination (temp dir + component) past the 260-char MAX_PATH the Win32 APIs enforce
|
||||
// when LongPathsEnabled is off.
|
||||
const fs::path destination = dir.path() / std::string(240, 'd');
|
||||
REQUIRE(destination.string().size() > 260);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(extract_zip_to_directory(wheel, destination, error));
|
||||
INFO("extract error: " << error);
|
||||
CHECK(error.empty());
|
||||
|
||||
// Verify through a long-path-capable path, otherwise the check itself is bounded by MAX_PATH.
|
||||
const fs::path module_py = destination / "pkg" / "module.py";
|
||||
REQUIRE(fs::exists(to_extended_path(module_py)));
|
||||
REQUIRE(fs::is_regular_file(to_extended_path(module_py)));
|
||||
|
||||
// Remove the deep subtree here: ScopedTemporaryDir's plain remove_all cannot reach past
|
||||
// MAX_PATH on Windows.
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(to_extended_path(destination), ec);
|
||||
CHECK_FALSE(ec);
|
||||
}
|
||||
|
||||
TEST_CASE("extract_zip_to_directory rejects an entry that escapes the destination",
|
||||
"[PluginInstall][PluginFsUtils]")
|
||||
{
|
||||
ScopedTemporaryDir dir("escape-extract");
|
||||
|
||||
const fs::path wheel = dir.path() / "evil-1.0-py3-none-any.whl";
|
||||
{
|
||||
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
|
||||
zipper.add_entry("pkg/module.py", "print('ok')\n", 12);
|
||||
zipper.add_entry("../escape.txt", "escaped\n", 8);
|
||||
zipper.finalize();
|
||||
}
|
||||
|
||||
std::string error;
|
||||
CHECK_FALSE(extract_zip_to_directory(wheel, dir.path() / "out", error));
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("escapes the plugin package"));
|
||||
CHECK_FALSE(fs::exists(dir.path() / "escape.txt"));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user