mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-17 04:41:21 +00:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd28f6c189 | ||
|
|
9b2fcaa53a | ||
|
|
0acf1e6680 | ||
|
|
c6804fbb68 | ||
|
|
6d40ec93f6 | ||
|
|
82aee1abdd | ||
|
|
cefdd9b609 | ||
|
|
caade80624 | ||
|
|
cbd8ce98d5 | ||
|
|
99714afc4c | ||
|
|
3d07663ab2 | ||
|
|
29b6bbb8c9 | ||
|
|
8b6c074c69 | ||
|
|
109d09e0b8 | ||
|
|
b00237807d | ||
|
|
4149401c53 | ||
|
|
f6cf18fb96 | ||
|
|
4ee1b9c6e9 | ||
|
|
bc8d0a7858 | ||
|
|
e0651ec31d | ||
|
|
0caefcf4aa | ||
|
|
86905a8a20 | ||
|
|
d4fabbf7c8 | ||
|
|
51efb24752 | ||
|
|
cf9ec18df5 | ||
|
|
99de46b9d0 | ||
|
|
258f927a0f | ||
|
|
b8d40ed61f | ||
|
|
6aa613ccf0 | ||
|
|
54e6df0235 | ||
|
|
7599868797 | ||
|
|
81ec931990 | ||
|
|
8fd97b5120 | ||
|
|
aacc36b9d2 | ||
|
|
79ed246d26 | ||
|
|
92b5301bf6 | ||
|
|
26f5fd6251 | ||
|
|
20c157b0f4 | ||
|
|
5d73410281 | ||
|
|
4cf16febf6 | ||
|
|
70e52a105f | ||
|
|
b525cb79e1 | ||
|
|
86ab598899 |
@@ -569,12 +569,18 @@ on the **first** call. So:
|
|||||||
#endif
|
#endif
|
||||||
```
|
```
|
||||||
|
|
||||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
|
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
|
||||||
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
|
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
|
||||||
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
|
||||||
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
|
`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
|
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.
|
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
|
||||||
|
|
||||||
**wx's own detectors**, for reference:
|
**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
|
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
|
||||||
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||||
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond 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)
|
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
|
||||||
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
|
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
|
||||||
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
|
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
|
||||||
@@ -567,33 +568,69 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
|
|||||||
## WebKitGTK on Linux sessions
|
## WebKitGTK on Linux sessions
|
||||||
|
|
||||||
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
|
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
|
||||||
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
|
- **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`
|
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
|
||||||
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
|
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
|
||||||
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
|
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
|
||||||
`DISPLAY` is set.
|
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
|
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
|
||||||
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
|
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
|
||||||
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
|
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
|
||||||
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
|
|
||||||
|
|
||||||
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
|
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
|
||||||
(see `references/platforms.md`), not these environment variables.
|
(see `references/platforms.md`), not these environment variables.
|
||||||
|
|
||||||
**Pitfall.**
|
**Pitfall.**
|
||||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
|
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
||||||
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
|
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
|
||||||
initialises; never unconditionally. Keep JS-side fallbacks so a page never
|
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.
|
waits forever for a C++→JS call.
|
||||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
|
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
|
||||||
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
|
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
|
||||||
acceleration; without the JS timeout the user is stuck when the signal is lost.
|
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
|
```cpp
|
||||||
// Wrong: unconditional, kills HW acceleration everywhere
|
// Wrong: unconditional, kills HW acceleration everywhere
|
||||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
||||||
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
|
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
|
||||||
if (display && *display && wayland && *wayland)
|
// 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);
|
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
|
||||||
```
|
```
|
||||||
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
||||||
|
|||||||
@@ -288,6 +288,19 @@ if (APPLE)
|
|||||||
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
|
# 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")
|
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}")
|
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
|
||||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
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 " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
|
||||||
echo " -h: prints this help text"
|
echo " -h: prints this help text"
|
||||||
echo " -i: build the Orca Slicer AppImage (optional)"
|
echo " -i: build the Orca Slicer AppImage (optional)"
|
||||||
echo " -p: 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 " -r: skip RAM and disk checks (low RAM compiling)"
|
||||||
echo " -s: build the Orca Slicer (optional)"
|
echo " -s: build the Orca Slicer (optional)"
|
||||||
echo " -t: build tests (optional), requires -s flag"
|
echo " -t: build tests (optional), requires -s flag"
|
||||||
@@ -549,6 +549,20 @@ fi
|
|||||||
if [ -n "${CMAKE_CCACHE}" ] ; then
|
if [ -n "${CMAKE_CCACHE}" ] ; then
|
||||||
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
||||||
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
|
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
|
||||||
|
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
|
||||||
|
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
|
||||||
|
# a checkout has just written. It is added to the sloppiness already configured, and
|
||||||
|
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
|
||||||
|
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
|
||||||
|
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
|
||||||
|
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
|
||||||
|
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
|
||||||
|
fi
|
||||||
|
# Hash the includes the compiler reports instead of preprocessing every miss.
|
||||||
|
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
|
||||||
|
export CCACHE_DEPEND=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -111,6 +111,54 @@ else
|
|||||||
export CMAKE_POLICY_COMPAT=""
|
export CMAKE_POLICY_COMPAT=""
|
||||||
fi
|
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 "Build params:"
|
||||||
echo " - ARCH: $ARCH"
|
echo " - ARCH: $ARCH"
|
||||||
echo " - BUILD_CONFIG: $BUILD_CONFIG"
|
echo " - BUILD_CONFIG: $BUILD_CONFIG"
|
||||||
@@ -162,6 +210,7 @@ function build_deps() {
|
|||||||
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
|
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||||
|
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||||
${CMAKE_POLICY_COMPAT}
|
${CMAKE_POLICY_COMPAT}
|
||||||
fi
|
fi
|
||||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||||
@@ -264,6 +313,7 @@ function build_slicer() {
|
|||||||
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
|
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||||
|
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||||
${CMAKE_POLICY_COMPAT}
|
${CMAKE_POLICY_COMPAT}
|
||||||
fi
|
fi
|
||||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
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
|
orcaslicer_add_cmake_project(JPEG
|
||||||
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||||
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||||
DEPENDS ${ZLIB_PKG}
|
|
||||||
CMAKE_ARGS
|
CMAKE_ARGS
|
||||||
-DENABLE_SHARED=OFF
|
-DENABLE_SHARED=OFF
|
||||||
-DENABLE_STATIC=ON
|
-DENABLE_STATIC=ON
|
||||||
${jpeg_flag}
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Color picker
|
||||||
|
|
||||||
|
The color picker is a local HTML dialog hosted by `ColorPickerDialog`, a
|
||||||
|
`WebViewHostDialog` subclass. Its header and implementation own the ordered
|
||||||
|
selection contract and shared filament conversion. Bridge validation, favorites
|
||||||
|
persistence, placement and gettext strings remain private to the implementation.
|
||||||
|
Its colors use the existing `ColorRGBA` utility.
|
||||||
|
It uses a native title bar and fixed window border, allowing dragging but no
|
||||||
|
manual resizing or maximization. Native title-bar colors use the application's
|
||||||
|
shared dialog dark-theme update on creation and theme changes.
|
||||||
|
Like SpeedDial, the host disables browser zoom accelerators, restores native
|
||||||
|
WebView focus on activation and readiness, and synchronizes layout and the macOS
|
||||||
|
viewport before repainting the backend. Activation never dismisses this modal
|
||||||
|
dialog; focus restoration preserves an active editor and its partial input.
|
||||||
|
The initialized page reports
|
||||||
|
its intrinsic content height; native code validates the current page identifier
|
||||||
|
and bounded numeric height, converts CSS pixels to device-independent window
|
||||||
|
size, and fits within the display's work area. Identical sizes are ignored. The
|
||||||
|
page observes content rather than viewport height, so resizing does not create
|
||||||
|
a feedback loop; oversized content remains scrollable.
|
||||||
|
The panel snapshots the triggering color or More Colors button's screen rectangle.
|
||||||
|
It aligns below the button, flips above when needed, and clamps to that button's
|
||||||
|
display work area on initial display. Later content and DPI changes update only
|
||||||
|
the client size, preserving the position chosen by dragging the title bar.
|
||||||
|
wxGTK clears the dialog's default
|
||||||
|
parent-centering constraint and reapplies its native position after showing,
|
||||||
|
bypassing wxGTK's requested-position cache. Absolute top-level placement remains
|
||||||
|
subject to the window manager; Wayland compositors control it. The official filament palette's
|
||||||
|
existing placement is independent of this custom panel.
|
||||||
|
A solid selection contains one color; a gradient contains exactly two ordered
|
||||||
|
endpoints. The bridge represents these as `{"type":"solid|gradient",
|
||||||
|
"colors":["#RRGGBBAA", ...]}`. RGB input is also accepted and becomes opaque.
|
||||||
|
Native validation rejects malformed hex and incorrect cardinality.
|
||||||
|
|
||||||
|
The project filament color action and the official filament picker's More Colors
|
||||||
|
action enable two-endpoint gradients and independent endpoint alpha. The preset
|
||||||
|
combo box reads project colors in their stored vector order locally and applies
|
||||||
|
changes through `sync_colour_config`,
|
||||||
|
retaining the existing RGB/RGBA profile/project formats and dirty marking.
|
||||||
|
Sidebar gradient and segmented multicolor swatches draw colors in the supplied
|
||||||
|
order without sorting them.
|
||||||
|
Opaque colors use `#RRGGBB`; nonopaque colors use `#RRGGBBAA`, including fully
|
||||||
|
transparent alpha `00`. Initial selections, alpha-only edits and cancellation
|
||||||
|
retain stored alpha. Ordered
|
||||||
|
custom results bypass the official palette's `FilamentColor` set. Choosing an
|
||||||
|
official swatch retains its existing palette behavior.
|
||||||
|
The Linux sidebar's Change extruder color menu uses this same custom editor and
|
||||||
|
the filament swatch anchor, including alpha, ordered gradients and configuration syncing.
|
||||||
|
|
||||||
|
Striped colors and gradients with more than two endpoints cannot be represented
|
||||||
|
by this editor. A notice explains that the current selection is retained; the
|
||||||
|
editor starts from the first color, and unchanged confirmation is a no-op.
|
||||||
|
Cancellation is also a no-op. If the WebView backend cannot initialize, a local
|
||||||
|
unavailable panel offers Cancel and Escape, without opening a system color picker.
|
||||||
|
|
||||||
|
Generic color option fields use an ordinary color button on every platform and
|
||||||
|
open the same panel with gradients and alpha disabled. The panel is anchored to
|
||||||
|
the field button. Empty or invalid field values remain undefined (the empty
|
||||||
|
string), distinct from opaque black. Right-click resets to undefined. Cancel does
|
||||||
|
not notify the field; confirmation and reset notify only when its RGB string
|
||||||
|
changes. Programmatic assignments remain silent, and reading a field never
|
||||||
|
writes legacy favorites.
|
||||||
|
|
||||||
|
AMS material settings retain their fixed/AMS swatches and first-level popup. Only
|
||||||
|
its custom-color action opens the solid, opaque editor, anchored to that custom
|
||||||
|
control. The transient popup releases its grab while the modal is open and returns
|
||||||
|
to the same selection on cancellation. Confirmation uses the existing default-color
|
||||||
|
and packed RGBA event path. Device permission/status checks remain at the first-level
|
||||||
|
entry.
|
||||||
|
|
||||||
|
Texture import's color-mapping Add Material action opens a solid, opaque editor
|
||||||
|
anchored to the mapping row. The existing filament limit is checked before opening.
|
||||||
|
The first-level material popup closes as an action; its callback is copied before
|
||||||
|
dismissal, so popup destruction does not invalidate the modal's completion path.
|
||||||
|
Confirmation invokes the existing virtual-filament callback once; cancellation
|
||||||
|
adds no material.
|
||||||
|
|
||||||
|
Native gettext strings are installed at document start. Static page labels and
|
||||||
|
palette names use stable English keys; RAL codes, channel symbols, and numeric
|
||||||
|
labels remain data. The palette color values do not depend on the UI language.
|
||||||
|
The application locale also sets the document language. Numeric editor validation
|
||||||
|
uses the application's gettext messages, rather than browser language defaults;
|
||||||
|
missing translations fall back to the stable English keys.
|
||||||
|
|
||||||
|
The pure color/state regressions run with
|
||||||
|
`node --test resources/web/dialog/ColorPickerDialog/colorpicker.test.js`.
|
||||||
|
Optional real-browser regressions live beside them in
|
||||||
|
`colorpicker.browser.test.js`. They use an existing Playwright installation
|
||||||
|
(`ORCA_PLAYWRIGHT_MODULE` can name its module path) and an installed browser
|
||||||
|
(`ORCA_BROWSER_EXECUTABLE` can name its executable), without adding a production
|
||||||
|
dependency. They cover bridge initialization, capability combinations, invalid
|
||||||
|
editor recovery, ordered endpoint alpha, favorites, gettext keys, and live theme
|
||||||
|
changes. These browser checks complement native contract/persistence tests; they
|
||||||
|
do not establish the full native modal confirmation and favorites journey.
|
||||||
|
|
||||||
|
Gradient and alpha capabilities are independent and default off. Unsupported
|
||||||
|
gradients are rejected; disabled alpha normalizes the selected result to opaque.
|
||||||
|
Favorites retain both capabilities independently of the current caller. The page
|
||||||
|
stores alpha as a byte and only converts to percentage when displaying or editing
|
||||||
|
opacity, so switching endpoints does not change alpha precision.
|
||||||
|
|
||||||
|
The page starts disabled. Each document supplies a page identifier in its ready
|
||||||
|
message, receives native initialization, and acknowledges it before native code
|
||||||
|
accepts confirmation or favorite updates. Repeated initialization does not reset
|
||||||
|
edits, and messages from an obsolete document are ignored. Native WebView message
|
||||||
|
handling is deferred and guarded by a lifetime token. Only a validated confirmation
|
||||||
|
sets the optional result. Cancel, Escape and native window close leave it empty.
|
||||||
|
|
||||||
|
Favorites belong to application configuration, separately from the selected result,
|
||||||
|
in the editor configuration returned by `AppConfig::config_path()` under `data_dir()`.
|
||||||
|
The `color_picker` section has string `version` equal to `1` and a `favorites`
|
||||||
|
string containing a JSON array of typed selections. Native helpers normalize and
|
||||||
|
deduplicate this array in order and limit it to 24 entries. Updates replace the
|
||||||
|
entire section and immediately call `AppConfig::save()`, so accepted favorite
|
||||||
|
changes survive cancellation. Empty arrays are saved explicitly. Core `AppConfig`
|
||||||
|
only stores the raw section; color validation belongs to the GUI boundary.
|
||||||
|
|
||||||
|
When the section is absent, valid solid colors from `custom_color_list` are imported
|
||||||
|
once, without changing that legacy section. The legacy native picker saves decimal
|
||||||
|
`r,g,b,a` byte strings; hexadecimal RGB and RGBA strings are also supported. Migration
|
||||||
|
strictly validates all four decimal channels and preserves alpha. Gradient strings
|
||||||
|
and malformed values are ignored. The new version and an empty array are
|
||||||
|
saved even when there is nothing to import. A present section with an unknown or
|
||||||
|
missing version is left untouched and disables favorite writes. Malformed version
|
||||||
|
1 data is displayed as empty and remains on disk until the user explicitly saves
|
||||||
|
a valid replacement. Other configuration sections and project/profile color
|
||||||
|
formats are independent of this storage.
|
||||||
|
|
||||||
|
Interface colors consume the host's `data-orca-theme` and `--orca-*` contract through
|
||||||
|
shared styles. The picker opts into scoped global control roles. Application theme
|
||||||
|
changes update UI surfaces without reloading the page or changing color data.
|
||||||
+1336
-56
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -325,6 +325,7 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
|
|||||||
src/slic3r/GUI/CloneDialog.cpp
|
src/slic3r/GUI/CloneDialog.cpp
|
||||||
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
||||||
src/slic3r/GUI/ColorDecomposeSupport.cpp
|
src/slic3r/GUI/ColorDecomposeSupport.cpp
|
||||||
|
src/slic3r/GUI/ColorPickerDialog.cpp
|
||||||
src/slic3r/GUI/ConfigWizard.cpp
|
src/slic3r/GUI/ConfigWizard.cpp
|
||||||
src/slic3r/GUI/DeviceCore/DevCalib.cpp
|
src/slic3r/GUI/DeviceCore/DevCalib.cpp
|
||||||
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
|
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4922
-1909
File diff suppressed because it is too large
Load Diff
+6162
-2709
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -592,6 +592,10 @@
|
|||||||
"name": "SUNLU PETG @base",
|
"name": "SUNLU PETG @base",
|
||||||
"sub_path": "filament/SUNLU/SUNLU PETG @base.json"
|
"sub_path": "filament/SUNLU/SUNLU PETG @base.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "School House Spools PETG @base",
|
||||||
|
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @base.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "eSUN PETG @base",
|
"name": "eSUN PETG @base",
|
||||||
"sub_path": "filament/eSUN/eSUN PETG @base.json"
|
"sub_path": "filament/eSUN/eSUN PETG @base.json"
|
||||||
@@ -1096,6 +1100,10 @@
|
|||||||
"name": "SUNLU Wood PLA @base",
|
"name": "SUNLU Wood PLA @base",
|
||||||
"sub_path": "filament/SUNLU/SUNLU Wood PLA @base.json"
|
"sub_path": "filament/SUNLU/SUNLU Wood PLA @base.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "School House Spools PLA @base",
|
||||||
|
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @base.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Valment PLA @base",
|
"name": "Valment PLA @base",
|
||||||
"sub_path": "filament/Valment/Valment PLA @base.json"
|
"sub_path": "filament/Valment/Valment PLA @base.json"
|
||||||
@@ -2260,6 +2268,10 @@
|
|||||||
"name": "SUNLU PETG @System",
|
"name": "SUNLU PETG @System",
|
||||||
"sub_path": "filament/SUNLU/SUNLU PETG @System.json"
|
"sub_path": "filament/SUNLU/SUNLU PETG @System.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "School House Spools PETG @System",
|
||||||
|
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @System.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "eSUN PETG @System",
|
"name": "eSUN PETG @System",
|
||||||
"sub_path": "filament/eSUN/eSUN PETG @System.json"
|
"sub_path": "filament/eSUN/eSUN PETG @System.json"
|
||||||
@@ -3812,6 +3824,10 @@
|
|||||||
"name": "SUNLU Wood PLA @System",
|
"name": "SUNLU Wood PLA @System",
|
||||||
"sub_path": "filament/SUNLU/SUNLU Wood PLA @System.json"
|
"sub_path": "filament/SUNLU/SUNLU Wood PLA @System.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "School House Spools PLA @System",
|
||||||
|
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @System.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Valment PLA @System",
|
"name": "Valment PLA @System",
|
||||||
"sub_path": "filament/Valment/Valment PLA @System.json"
|
"sub_path": "filament/Valment/Valment PLA @System.json"
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "filament",
|
||||||
|
"name": "School House Spools PETG @System",
|
||||||
|
"inherits": "School House Spools PETG @base",
|
||||||
|
"from": "system",
|
||||||
|
"setting_id": "RSu2orfhR8ktT36h",
|
||||||
|
"instantiation": "true",
|
||||||
|
"filament_max_volumetric_speed": [
|
||||||
|
"13"
|
||||||
|
],
|
||||||
|
"filament_long_retractions_when_cut": [
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
"filament_retraction_distances_when_cut": [
|
||||||
|
"18"
|
||||||
|
],
|
||||||
|
"compatible_printers": []
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"type": "filament",
|
||||||
|
"name": "School House Spools PETG @base",
|
||||||
|
"inherits": "fdm_filament_pet",
|
||||||
|
"from": "system",
|
||||||
|
"filament_id": "OFnaCGQ9",
|
||||||
|
"instantiation": "false",
|
||||||
|
"cool_plate_temp": [
|
||||||
|
"0"
|
||||||
|
],
|
||||||
|
"cool_plate_temp_initial_layer": [
|
||||||
|
"0"
|
||||||
|
],
|
||||||
|
"eng_plate_temp": [
|
||||||
|
"70"
|
||||||
|
],
|
||||||
|
"eng_plate_temp_initial_layer": [
|
||||||
|
"70"
|
||||||
|
],
|
||||||
|
"fan_cooling_layer_time": [
|
||||||
|
"30"
|
||||||
|
],
|
||||||
|
"fan_max_speed": [
|
||||||
|
"40"
|
||||||
|
],
|
||||||
|
"fan_min_speed": [
|
||||||
|
"10"
|
||||||
|
],
|
||||||
|
"filament_cost": [
|
||||||
|
"16.00"
|
||||||
|
],
|
||||||
|
"filament_density": [
|
||||||
|
"1.25"
|
||||||
|
],
|
||||||
|
"filament_flow_ratio": [
|
||||||
|
"0.95"
|
||||||
|
],
|
||||||
|
"filament_max_volumetric_speed": [
|
||||||
|
"8"
|
||||||
|
],
|
||||||
|
"filament_multitool_ramming_flow": [
|
||||||
|
"8"
|
||||||
|
],
|
||||||
|
"filament_vendor": [
|
||||||
|
"School House Spools"
|
||||||
|
],
|
||||||
|
"hot_plate_temp": [
|
||||||
|
"70"
|
||||||
|
],
|
||||||
|
"hot_plate_temp_initial_layer": [
|
||||||
|
"70"
|
||||||
|
],
|
||||||
|
"nozzle_temperature": [
|
||||||
|
"252"
|
||||||
|
],
|
||||||
|
"nozzle_temperature_initial_layer": [
|
||||||
|
"252"
|
||||||
|
],
|
||||||
|
"nozzle_temperature_range_high": [
|
||||||
|
"270"
|
||||||
|
],
|
||||||
|
"nozzle_temperature_range_low": [
|
||||||
|
"230"
|
||||||
|
],
|
||||||
|
"overhang_fan_speed": [
|
||||||
|
"90"
|
||||||
|
],
|
||||||
|
"overhang_fan_threshold": [
|
||||||
|
"10%"
|
||||||
|
],
|
||||||
|
"slow_down_layer_time": [
|
||||||
|
"12"
|
||||||
|
],
|
||||||
|
"textured_plate_temp": [
|
||||||
|
"70"
|
||||||
|
],
|
||||||
|
"textured_plate_temp_initial_layer": [
|
||||||
|
"70"
|
||||||
|
]
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "filament",
|
||||||
|
"name": "School House Spools PLA @System",
|
||||||
|
"inherits": "School House Spools PLA @base",
|
||||||
|
"from": "system",
|
||||||
|
"setting_id": "3rHJk6SVAvFzyHI0",
|
||||||
|
"instantiation": "true",
|
||||||
|
"filament_max_volumetric_speed": [
|
||||||
|
"21"
|
||||||
|
],
|
||||||
|
"filament_long_retractions_when_cut": [
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
"filament_retraction_distances_when_cut": [
|
||||||
|
"18"
|
||||||
|
],
|
||||||
|
"compatible_printers": []
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"type": "filament",
|
||||||
|
"name": "School House Spools PLA @base",
|
||||||
|
"inherits": "fdm_filament_pla",
|
||||||
|
"from": "system",
|
||||||
|
"filament_id": "OFHzwmT9",
|
||||||
|
"instantiation": "false",
|
||||||
|
"filament_cost": [
|
||||||
|
"17.00"
|
||||||
|
],
|
||||||
|
"filament_density": [
|
||||||
|
"1.26"
|
||||||
|
],
|
||||||
|
"filament_flow_ratio": [
|
||||||
|
"0.98"
|
||||||
|
],
|
||||||
|
"filament_max_volumetric_speed": [
|
||||||
|
"12"
|
||||||
|
],
|
||||||
|
"filament_vendor": [
|
||||||
|
"School House Spools"
|
||||||
|
],
|
||||||
|
"filament_scarf_seam_type": [
|
||||||
|
"none"
|
||||||
|
],
|
||||||
|
"filament_scarf_height": [
|
||||||
|
"10%"
|
||||||
|
],
|
||||||
|
"filament_scarf_gap": [
|
||||||
|
"0%"
|
||||||
|
],
|
||||||
|
"filament_scarf_length": [
|
||||||
|
"10"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "RatRig",
|
"name": "RatRig",
|
||||||
"version": "02.04.00.06",
|
"version": "02.04.00.07",
|
||||||
"force_update": "0",
|
"force_update": "0",
|
||||||
"description": "RatRig configurations",
|
"description": "RatRig configurations",
|
||||||
"machine_model_list": [
|
"machine_model_list": [
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"z_hop": [
|
"z_hop": [
|
||||||
"0.2"
|
"0.2"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||||
"change_filament_gcode": "M600",
|
"change_filament_gcode": "M600",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"bed_exclude_area": [
|
"bed_exclude_area": [
|
||||||
"0x0"
|
"0x0"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single]",
|
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single]",
|
||||||
"machine_end_gcode": "END_PRINT",
|
"machine_end_gcode": "END_PRINT",
|
||||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
|
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/* Pure color/state helpers shared by the page and its node:test contract tests. */
|
||||||
|
(function (root, factory) {
|
||||||
|
"use strict";
|
||||||
|
const api = factory();
|
||||||
|
if (typeof module === "object" && module.exports) module.exports = api;
|
||||||
|
else root.ColorPickerModel = api;
|
||||||
|
})(typeof globalThis === "object" ? globalThis : this, function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function parseHex(hex) {
|
||||||
|
if (typeof hex !== "string" || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return null;
|
||||||
|
return [1, 3, 5, 7].map((offset) => offset === 7 && hex.length === 7 ? 255 : parseInt(hex.slice(offset, offset + 2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHex(color) {
|
||||||
|
return "#" + color.map((byte) => Math.round(byte).toString(16).padStart(2, "0")).join("").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_GRADIENT_COLORS = 7; // Most colors a gradient may have. A caller allows 2 (the default) up to this many.
|
||||||
|
const NEW_GRADIENT_HUE_SHIFT = 75; // Hue step used for every newly created gradient color.
|
||||||
|
const gradientLimit = (value) => Math.max(2, Math.min(MAX_GRADIENT_COLORS, Math.floor(Number(value)) || 2));
|
||||||
|
|
||||||
|
// `maxGradientColors` caps how many colors a gradient may have (2 when omitted).
|
||||||
|
function normalizeSelection(value, options = {}, maxGradientColors = 2) {
|
||||||
|
if (!value || !["solid", "gradient"].includes(value.type) || !Array.isArray(value.colors)) return null;
|
||||||
|
if (value.type === "gradient" && !options.allow_gradient) return null;
|
||||||
|
const count = value.colors.length;
|
||||||
|
if (value.type === "solid" ? count !== 1 : count < 2 || count > gradientLimit(maxGradientColors)) return null;
|
||||||
|
const colors = value.colors.map(parseHex);
|
||||||
|
if (colors.some((color) => color === null)) return null;
|
||||||
|
if (!options.allow_alpha) colors.forEach((color) => { color[3] = 255; });
|
||||||
|
return {type: value.type, colors: colors.map(toHex)};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHsl(r, g, b) {
|
||||||
|
r /= 255; g /= 255; b /= 255;
|
||||||
|
const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
|
||||||
|
let h = 0, s = 0;
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
if (delta) {
|
||||||
|
s = delta / (1 - Math.abs(2 * l - 1));
|
||||||
|
if (max === r) h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
|
||||||
|
else if (max === g) h = ((b - r) / delta + 2) / 6;
|
||||||
|
else h = ((r - g) / delta + 4) / 6;
|
||||||
|
}
|
||||||
|
return [h * 360, s * 100, l * 100];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslToRgb(h, s, l) {
|
||||||
|
h = ((h % 360) + 360) % 360 / 60; s /= 100; l /= 100;
|
||||||
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||||
|
const x = c * (1 - Math.abs(h % 2 - 1)), m = l - c / 2;
|
||||||
|
const sectors = [[c,x,0], [x,c,0], [0,c,x], [0,x,c], [x,0,c], [c,0,x]];
|
||||||
|
return sectors[Math.floor(h)].map((channel) => Math.round((channel + m) * 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
function contrastColor(color, hueShift = 90) {
|
||||||
|
if (!Number.isFinite(hueShift)) hueShift = 90;
|
||||||
|
const degrees = Math.max(-180, Math.min(180, hueShift));
|
||||||
|
const amount = Math.abs(degrees) / 180;
|
||||||
|
const [h, s, l] = rgbToHsl(color[0], color[1], color[2]);
|
||||||
|
const grey = s < 10;
|
||||||
|
const target = grey ? (l < 50 ? 100 : 0) : 100 - l;
|
||||||
|
const rgb = hslToRgb(h + degrees, grey ? 0 : s, l + (target - l) * amount);
|
||||||
|
return [...rgb, color[3]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// options.max_gradient_colors (2..7, default 2) is how many colors this page lets a gradient have.
|
||||||
|
function createState(options = {}, selection = {type: "solid", colors: ["#808080"]}) {
|
||||||
|
const capabilities = {allow_gradient: options.allow_gradient === true, allow_alpha: options.allow_alpha === true};
|
||||||
|
// gradient stays null until a selection provides one or the user switches to gradient mode.
|
||||||
|
const state = {options: capabilities, maxGradientColors: gradientLimit(options.max_gradient_colors),
|
||||||
|
mode: "solid", active: 0, solid: [128,128,128,255], gradient: null};
|
||||||
|
if (!applySelection(state, selection)) return null;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySelection(state, value) {
|
||||||
|
const selection = normalizeSelection(value, state.options, state.maxGradientColors);
|
||||||
|
if (!selection) return false;
|
||||||
|
state.mode = selection.type;
|
||||||
|
const colors = selection.colors.map(parseHex);
|
||||||
|
if (state.mode === "solid") state.solid = colors[0];
|
||||||
|
else {
|
||||||
|
state.gradient = colors;
|
||||||
|
state.active = Math.min(state.active, colors.length - 1); // A gradient with fewer colors must not leave the selected stop past its end.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeColor(state) { return state.mode === "solid" ? state.solid : state.gradient[state.active]; }
|
||||||
|
|
||||||
|
function setMode(state, mode) {
|
||||||
|
if (mode !== "solid" && (mode !== "gradient" || !state.options.allow_gradient)) return false;
|
||||||
|
// A new gradient starts at the current solid color and ends at its contrast color.
|
||||||
|
if (mode === "gradient" && !state.gradient) state.gradient = [[...state.solid], contrastColor(state.solid, NEW_GRADIENT_HUE_SHIFT)];
|
||||||
|
state.mode = mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGradientColor(state) {
|
||||||
|
if (state.mode !== "gradient" || state.gradient.length >= state.maxGradientColors) return false;
|
||||||
|
state.gradient.splice(state.active + 1, 0, contrastColor(state.gradient[state.active], NEW_GRADIENT_HUE_SHIFT));
|
||||||
|
state.active += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGradientColor(state) {
|
||||||
|
if (state.mode !== "gradient" || state.gradient.length <= 2) return false;
|
||||||
|
state.gradient.splice(state.active, 1);
|
||||||
|
state.active = Math.min(state.active, state.gradient.length - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRgb(state, rgb) {
|
||||||
|
if (rgb.length !== 3 || rgb.some((v) => !Number.isFinite(v) || v < 0 || v > 255)) return false;
|
||||||
|
const color = activeColor(state);
|
||||||
|
rgb.forEach((v, i) => { color[i] = Math.round(v); });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAlphaPercent(state, value) {
|
||||||
|
if (!state.options.allow_alpha || !Number.isFinite(value) || value < 0 || value > 100) return false;
|
||||||
|
activeColor(state)[3] = Math.round(value * 255 / 100);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHex(state, hex) {
|
||||||
|
const color = parseHex(hex);
|
||||||
|
if (!color) return false;
|
||||||
|
if (hex.length === 7) color[3] = activeColor(state)[3];
|
||||||
|
if (!state.options.allow_alpha) color[3] = 255;
|
||||||
|
if (state.mode === "solid") state.solid = color;
|
||||||
|
else state.gradient[state.active] = color;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSelection(state) {
|
||||||
|
const colors = state.mode === "solid" ? [state.solid] : state.gradient;
|
||||||
|
return normalizeSelection({type: state.mode, colors: colors.map(toHex)}, state.options, state.maxGradientColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favorites retain full capabilities regardless of what this caller can select.
|
||||||
|
function normalizeFavorites(values) {
|
||||||
|
if (!Array.isArray(values)) return [];
|
||||||
|
return values.slice(0, 24).map((value) => normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, MAX_GRADIENT_COLORS)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionBackground(value) {
|
||||||
|
return value.type === "gradient" ? `linear-gradient(to right, ${value.colors.join(", ")})` : `linear-gradient(${value.colors[0]}, ${value.colors[0]})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {MAX_GRADIENT_COLORS, NEW_GRADIENT_HUE_SHIFT, contrastColor, parseHex, toHex, normalizeSelection, rgbToHsl, hslToRgb, createState, applySelection, activeColor, setMode,
|
||||||
|
addGradientColor, removeGradientColor, setRgb, setAlphaPercent, setHex, exportSelection, normalizeFavorites, selectionBackground};
|
||||||
|
});
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
// Optional real-DOM regressions. Use an existing Playwright installation:
|
||||||
|
// ORCA_PLAYWRIGHT_MODULE=<module path> ORCA_BROWSER_EXECUTABLE=<browser path>
|
||||||
|
// ORCA_BROWSER_ENGINE=webkit exercises the macOS WebView layout engine.
|
||||||
|
// node --test resources/web/dialog/ColorPickerDialog/colorpicker.browser.test.js
|
||||||
|
const {test, before, after} = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const {readFileSync} = require("node:fs");
|
||||||
|
const {resolve} = require("node:path");
|
||||||
|
const {pathToFileURL} = require("node:url");
|
||||||
|
const engineName = process.env.ORCA_BROWSER_ENGINE || "chromium";
|
||||||
|
let engine;
|
||||||
|
try { engine = require(process.env.ORCA_PLAYWRIGHT_MODULE || "playwright")[engineName]; }
|
||||||
|
catch (error) { if (error.code !== "MODULE_NOT_FOUND") throw error; }
|
||||||
|
const options = {skip: engine ? false : "Use an existing Playwright installation via ORCA_PLAYWRIGHT_MODULE"};
|
||||||
|
const url = pathToFileURL(resolve(__dirname, "index.html")).href;
|
||||||
|
let browser;
|
||||||
|
before(async () => { if (engine) browser = await engine.launch({headless: true, ...(process.env.ORCA_BROWSER_EXECUTABLE ? {executablePath: process.env.ORCA_BROWSER_EXECUTABLE} : {})}); });
|
||||||
|
after(async () => { if (browser) await browser.close(); });
|
||||||
|
async function open(t, strings = {}, language = "") {
|
||||||
|
const page = await browser.newPage({viewport: {width: 600, height: 660}}), errors = [];
|
||||||
|
page.on("pageerror", error => errors.push(error.message));
|
||||||
|
t.after(async () => { await page.close(); assert.deepEqual(errors, []); });
|
||||||
|
await page.addInitScript(({table, language}) => {
|
||||||
|
window.ORCA_UI_STRINGS = table;
|
||||||
|
window.ORCA_COLOR_PICKER_LANGUAGE = language;
|
||||||
|
window.messages = [];
|
||||||
|
window.wx = {postMessage: text => messages.push(JSON.parse(text))};
|
||||||
|
}, {table: strings, language});
|
||||||
|
await page.goto(url);
|
||||||
|
const ready = await page.evaluate(() => messages[0]);
|
||||||
|
assert.equal(ready.command, "ready");
|
||||||
|
return {page, ready, send: payload => page.evaluate(value => ColorPickerDialog.handleMessage(value), payload)};
|
||||||
|
}
|
||||||
|
function init(ready, gradient = false, alpha = false) {
|
||||||
|
return {command: "init", page_id: ready.page_id, options: {allow_gradient: gradient, allow_alpha: alpha},
|
||||||
|
selection: {type: "solid", colors: ["#01234567"]},
|
||||||
|
ui: {sidebar_open: true, palette: "basic"},
|
||||||
|
favorites: [{type: "gradient", colors: ["#FF000040", "#0000FF90"]}, {type: "solid", colors: ["#ABCDEF40"]}]};
|
||||||
|
}
|
||||||
|
async function count(page, command) { return page.evaluate(value => messages.filter(message => message.command === value).length, command); }
|
||||||
|
|
||||||
|
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||||
|
test(`Native capabilities gate gradient=${gradient} and alpha=${alpha}`, options, async t => {
|
||||||
|
const {page, ready, send} = await open(t);
|
||||||
|
assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||||
|
assert.equal(await send({...init(ready), options: {allow_gradient: "yes", allow_alpha: alpha}}), false);
|
||||||
|
assert.equal(await send({...init(ready), page_id: "stale"}), false);
|
||||||
|
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||||
|
assert.equal(await page.locator("#modeControls").isVisible(), gradient);
|
||||||
|
assert.equal(await page.locator("#alphaControls").isVisible(), alpha);
|
||||||
|
assert.equal(await page.locator("#userColors button:not(.empty)").count(), gradient ? 2 : 1);
|
||||||
|
await page.locator("#userColors button:not(.empty)").last().click();
|
||||||
|
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||||
|
assert.deepEqual(selection, {type: "solid", colors: [alpha ? "#ABCDEF40" : "#ABCDEFFF"]});
|
||||||
|
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||||
|
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection, "repeated ready/init retains edits");
|
||||||
|
await page.locator("#colorPreview").click();
|
||||||
|
const favorites = await page.evaluate(() => messages.at(-1).favorites);
|
||||||
|
assert(favorites.some(favorite => favorite.colors[0] === "#ABCDEF40"), "original favorites retain their alpha");
|
||||||
|
assert(favorites.some(favorite => favorite.type === "gradient"), "filtered gradients remain stored");
|
||||||
|
await page.locator("#confirmBtn").click();
|
||||||
|
assert.deepEqual(await page.evaluate(() => messages.at(-1).selection), selection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Visible incomplete editors block confirmation and favorites until recovery", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||||
|
for (const [id, value, valid] of [["rValue", "", "21"], ["oValue", "", "50"], ["hexInput", "GG", "12345680"]]) {
|
||||||
|
await page.locator("#" + id).fill(value);
|
||||||
|
const confirms = await count(page, "confirm"), saves = await count(page, "update_favorites");
|
||||||
|
await page.locator("#confirmBtn").evaluate(element => element.click()); await page.locator("#colorPreview").click();
|
||||||
|
assert.equal(await count(page, "confirm"), confirms); assert.equal(await count(page, "update_favorites"), saves);
|
||||||
|
await page.locator("#" + id).fill(valid); await page.locator("#" + id).press("Enter");
|
||||||
|
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||||
|
}
|
||||||
|
await page.locator("#hValue").fill(""); // HSL is now visible beside RGB, so incomplete HSL must block.
|
||||||
|
const hslConfirms = await count(page, "confirm");
|
||||||
|
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||||
|
assert.equal(await count(page, "confirm"), hslConfirms);
|
||||||
|
await page.locator("#hValue").fill("100");
|
||||||
|
const confirms = await count(page, "confirm"); await page.locator("#confirmBtn").click();
|
||||||
|
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||||
|
await page.locator("#rValue").fill(""); await page.locator("#rValue").press("Escape");
|
||||||
|
assert.equal(await page.evaluate(() => messages.at(-1).command), "cancel");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Numeric channel and opacity edits clamp to their field bounds", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||||
|
for (const [id, input, expected] of [["rValue", "999", "255"], ["rValue", "-1", "0"],
|
||||||
|
["hValue", "361", "360"], ["oValue", "101", "100"], ["oValue", "-1", "0"]]) {
|
||||||
|
await page.locator("#" + id).fill(input);
|
||||||
|
assert.equal(await page.locator("#" + id).inputValue(), expected);
|
||||||
|
assert.equal(await page.locator("#" + id).evaluate(element => element.checkValidity()), true);
|
||||||
|
}
|
||||||
|
assert.equal((await page.evaluate(() => ColorPickerDialog.exportSelection())).colors[0].slice(-2), "00");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Endpoint edits and favorites retain ordered byte alpha", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t);
|
||||||
|
await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||||
|
await page.locator(".gradient-thumb[data-index='1']").click();
|
||||||
|
await page.locator('.color-item[title="Red"]').first().click();
|
||||||
|
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "gradient", colors: ["#FF000001", "#FF000080"]});
|
||||||
|
await page.locator("#hexInput").fill("0000FF80"); await page.locator(".gradient-thumb[data-index='0']").click();
|
||||||
|
assert.equal(await page.locator("#hexInput").inputValue(), "FF000001");
|
||||||
|
await page.locator("#colorPreview").click();
|
||||||
|
assert.deepEqual(await page.evaluate(() => messages.at(-1).favorites[0]), {type: "gradient", colors: ["#FF000001", "#0000FF80"]});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Read-only favorites remain selectable and late documents cannot initialize", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t); await send({...init(ready), favorites_writable: false});
|
||||||
|
assert.equal(await page.locator("#colorPreview").isDisabled(), true);
|
||||||
|
await page.locator("#userColors button:not(.empty)").first().click();
|
||||||
|
assert.equal(await page.locator("#hexInput").inputValue(), "ABCDEF");
|
||||||
|
await page.reload();
|
||||||
|
assert.equal(await send(init(ready)), false); assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Document strings translate palette names and editor errors while retaining RAL codes and numeric labels", options, async t => {
|
||||||
|
const source = readFileSync(resolve(__dirname, "../../../../src/slic3r/GUI/ColorPickerDialog.cpp"), "utf8");
|
||||||
|
const keys = [...source.matchAll(/_u8L\("([^"\n]+)"\)/g)].map(match => match[1]);
|
||||||
|
const strings = Object.fromEntries(keys.map(key => [key, "Translated: " + key]));
|
||||||
|
const {page, ready, send} = await open(t, strings); await send({...init(ready, true, true), options: {allow_gradient: true, allow_alpha: true, max_gradient_colors: 7}, preserve_multi_color: true});
|
||||||
|
assert.equal(await page.title(), strings["Color Picker"]);
|
||||||
|
for (const element of await page.locator("[data-i18n]").all()) assert.equal(await element.textContent(), strings[await element.getAttribute("data-i18n")]);
|
||||||
|
for (const attribute of ["aria-label", "title"]) for (const element of await page.locator(`[data-i18n-${attribute}]`).all())
|
||||||
|
assert.equal((await element.getAttribute(attribute)).split("\n")[0], strings[await element.getAttribute(`data-i18n-${attribute}`)]);
|
||||||
|
assert.equal(await page.locator("#selectionNotice").textContent(), strings["The current multi-color selection is kept until you choose a different color."]);
|
||||||
|
for (const palette of ["basic", "orca", "ral", "gradient"]) {
|
||||||
|
await page.selectOption("#paletteCombo", palette);
|
||||||
|
const names = await page.evaluate(value => ColorPickerPalettes[value].map(entry => entry.name), palette);
|
||||||
|
assert.deepEqual(await page.locator(".color-name").allTextContents(), names.map(name => {
|
||||||
|
const ral = name.match(/^(\d{4}) (.+)$/); return ral ? `${ral[1]} ${strings[ral[2]]}` : /^%\d+$/.test(name) ? name : strings[name];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await page.locator("#hexInput").fill("bad");
|
||||||
|
assert.equal(await page.locator("#hexInput").evaluate(element => element.validationMessage), strings["Enter a complete hexadecimal color"]);
|
||||||
|
await page.locator("#hexInput").fill("12345680");
|
||||||
|
for (const [id, invalid, valid, key] of [["rValue", "", "18", "Invalid input"],
|
||||||
|
["oValue", "", "50", "Invalid input"]]) {
|
||||||
|
const confirms = await count(page, "confirm");
|
||||||
|
await page.locator("#" + id).fill(invalid);
|
||||||
|
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||||
|
await page.locator("#colorPreview").click(); // Saving invokes the same translated validation while OK is disabled.
|
||||||
|
assert.equal(await count(page, "confirm"), confirms);
|
||||||
|
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), strings[key]);
|
||||||
|
await page.locator("#" + id).fill(valid);
|
||||||
|
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), "");
|
||||||
|
await page.locator("#confirmBtn").click();
|
||||||
|
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||||
|
}
|
||||||
|
await page.locator("#hValue").fill("361");
|
||||||
|
await page.locator("#colorPreview").click();
|
||||||
|
assert.equal(await page.locator("#hValue").inputValue(), "360", "out-of-range hue clamps at its boundary");
|
||||||
|
await page.locator(".color-item").first().click();
|
||||||
|
assert.equal(await page.locator("#hValue").evaluate(element => element.validationMessage), "", "a new palette selection clears the previous editor error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Document language and escaped labels preserve untranslated English fallback", options, async t => {
|
||||||
|
const cancel = '取消 "Cancel" \\ /\n下一行';
|
||||||
|
const {page, ready, send} = await open(t, {Cancel: cancel}, "zh-CN");
|
||||||
|
await send(init(ready));
|
||||||
|
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
|
||||||
|
assert.equal(await page.locator("#cancelBtn").textContent(), cancel);
|
||||||
|
assert.equal(await page.title(), "Color Picker");
|
||||||
|
assert.equal(await page.locator(".color-name").first().textContent(), "Red");
|
||||||
|
assert.deepEqual(await page.locator("label[for=hValue],label[for=sValue],label[for=lValue],label[for=rValue],label[for=gValue],label[for=bValue]").allTextContents(), ["H", "S", "L", "R", "G", "B"]);
|
||||||
|
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "solid", colors: ["#012345FF"]});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Global control colors follow the system while host semantic colors follow the app", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t); await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||||
|
const initial = await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()}));
|
||||||
|
for (const theme of ["light", "dark"]) for (const system of ["light", "dark"]) {
|
||||||
|
await page.emulateMedia({colorScheme: system});
|
||||||
|
await page.evaluate(value => {
|
||||||
|
document.documentElement.dataset.orcaTheme = value;
|
||||||
|
const colors = value === "light" ? {bg: "#ffffff", fg: "#262e30", muted: "#363636", border: "#dbdbdb", accent: "#009688"} : {bg: "#2d2d31", fg: "#efeff0", muted: "#b2b3b5", border: "#36363b", accent: "#00675b"};
|
||||||
|
for (const [key, color] of Object.entries(colors)) document.documentElement.style.setProperty("--orca-" + key, color);
|
||||||
|
document.documentElement.style.setProperty("--orca-accent-fg", "#ffffff");
|
||||||
|
}, theme);
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||||
|
});
|
||||||
|
const colors = await page.evaluate(() => {
|
||||||
|
const body = getComputedStyle(document.body);
|
||||||
|
const tokenColor = token => {
|
||||||
|
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||||
|
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||||
|
};
|
||||||
|
const mode = document.querySelector("label[for=modeSwitch]");
|
||||||
|
return {background: body.backgroundColor, foreground: body.color,
|
||||||
|
modeBackground: getComputedStyle(mode).backgroundColor,
|
||||||
|
accent: getComputedStyle(mode, "::before").backgroundColor,
|
||||||
|
hostPanel: tokenColor("--panel"), hostText: tokenColor("--text")};
|
||||||
|
});
|
||||||
|
assert.equal(colors.background, system === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)", `${theme} app / ${system} system body`);
|
||||||
|
assert.equal(colors.foreground, system === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||||
|
assert.equal(colors.modeBackground, system === "light" ? "rgb(240, 240, 240)" : "rgb(36, 36, 40)");
|
||||||
|
assert.equal(colors.accent, theme === "light" ? "rgb(0, 150, 136)" : "rgb(0, 103, 91)", "shared accent follows the injected app theme");
|
||||||
|
assert.equal(colors.hostPanel, theme === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)");
|
||||||
|
assert.equal(colors.hostText, theme === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||||
|
assert.deepEqual(await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()})), initial);
|
||||||
|
for (const id of ["modeSwitch"]) for (const checked of [false, true]) {
|
||||||
|
if (await page.locator("#" + id).isChecked() !== checked) await page.locator(`label[for=${id}]`).click();
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||||
|
});
|
||||||
|
const result = await page.locator(`label[for=${id}]`).evaluate((element, selected) => {
|
||||||
|
const tokenColor = token => {
|
||||||
|
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||||
|
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||||
|
};
|
||||||
|
const labels = element.querySelectorAll(".switch-label");
|
||||||
|
return {selected: getComputedStyle(labels[selected ? 1 : 0]).color,
|
||||||
|
unselected: getComputedStyle(labels[selected ? 0 : 1]).color,
|
||||||
|
accentForeground: tokenColor("--button-fg-light"), mutedForeground: tokenColor("--fg-color-label")};
|
||||||
|
}, checked);
|
||||||
|
assert.equal(result.selected, result.accentForeground, `${theme} app / ${system} system ${id} selected text uses the accent foreground`);
|
||||||
|
assert.equal(result.selected, "rgb(254, 254, 254)", "global selected text stays light in both system schemes");
|
||||||
|
assert.equal(result.unselected, result.mutedForeground, `${theme} app / ${system} system ${id} unselected text stays muted`);
|
||||||
|
assert.equal(result.unselected, system === "light" ? "rgb(54, 54, 54)" : "rgb(178, 179, 181)");
|
||||||
|
const selected = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||||
|
assert.equal(selected.type, checked ? "gradient" : "solid");
|
||||||
|
if (checked) assert.deepEqual(selected, initial.selection, "returning to gradient keeps ordered endpoint byte alpha");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sidebar resizes from a compact viewport and shrinks after every collapse", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t);
|
||||||
|
await page.setViewportSize({width: 340, height: 380});
|
||||||
|
await send({...init(ready, true, true), ui: {sidebar_open: false, palette: "basic"}});
|
||||||
|
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||||
|
for (const [index, sidebarOpen] of [false, true, false, true, false].entries()) {
|
||||||
|
const width = sidebarOpen ? 510 : 340;
|
||||||
|
await page.waitForFunction(expected => messages.filter(message => message.command === "resize").at(-1)?.width === expected, width);
|
||||||
|
const size = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1));
|
||||||
|
assert.equal(size.height, 380);
|
||||||
|
await page.setViewportSize({width: size.width, height: size.height});
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
assert(await page.evaluate(() => {
|
||||||
|
const body = document.body.getBoundingClientRect();
|
||||||
|
const picker = document.querySelector(".right-panel").getBoundingClientRect();
|
||||||
|
return body.width === innerWidth && picker.right <= body.right && picker.bottom <= body.bottom;
|
||||||
|
}), "the host viewport contains the full picker");
|
||||||
|
const resizes = await count(page, "resize");
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
assert.equal(await count(page, "resize"), resizes, "fitting the host does not cause a resize loop");
|
||||||
|
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||||
|
if (index < 4) await page.locator("#paletteToggle").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Floating panel fits capabilities and wrapped translations without a resize feedback loop", options, async t => {
|
||||||
|
const strings = {Color: "A long translated color label", Gradient: "A long translated gradient label", Cancel: "A longer translated cancel action", OK: "A longer translated confirmation action",
|
||||||
|
"The current multi-color selection is kept until you choose a different color.": "A translated explanation that wraps across the compact floating panel and keeps the original multi-color selection until another color is chosen."};
|
||||||
|
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||||
|
const {page, ready, send} = await open(t, strings);
|
||||||
|
await page.setViewportSize({width: 550, height: 520});
|
||||||
|
await send({...init(ready, gradient, alpha), preserve_multi_color: true});
|
||||||
|
await page.waitForFunction(() => messages.some(message => message.command === "resize"));
|
||||||
|
const height = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1).height);
|
||||||
|
assert(height >= 300 && height <= 900);
|
||||||
|
assert.equal(height, await page.locator("body").evaluate(element => Math.ceil(element.getBoundingClientRect().height)));
|
||||||
|
assert(await page.evaluate(() => messages.findIndex(message => message.command === "initialized") < messages.findIndex(message => message.command === "resize")));
|
||||||
|
await page.setViewportSize({width: 550, height});
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
const resizes = await count(page, "resize");
|
||||||
|
await page.setViewportSize({width: 550, height: height + 100});
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
assert.equal(await count(page, "resize"), resizes, "extra viewport height never changes intrinsic content height");
|
||||||
|
const controls = await page.evaluate(() => {
|
||||||
|
const body = document.body.getBoundingClientRect();
|
||||||
|
return ["confirmBtn", "cancelBtn", ...(document.getElementById("alphaControls").classList.contains("hidden") ? [] : ["oValue"])].map(id => {
|
||||||
|
const rect = document.getElementById(id).getBoundingClientRect();
|
||||||
|
return rect.left >= body.left && rect.right <= body.right && rect.bottom <= body.bottom;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert(controls.every(Boolean), "wrapped buttons and opacity fit inside the panel");
|
||||||
|
const edited = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.getElementById("selectionNotice").textContent += " Additional wrapped text. ".repeat(15);
|
||||||
|
});
|
||||||
|
await page.waitForFunction(previous => messages.filter(message => message.command === "resize").length > previous, resizes);
|
||||||
|
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), edited, "content resize leaves the selection untouched");
|
||||||
|
// The compact layout now puts saved swatches to the right of both previews.
|
||||||
|
assert(await page.evaluate(() => {
|
||||||
|
const favorites = document.getElementById("userColors").getBoundingClientRect();
|
||||||
|
const preview = document.getElementById("colorPreview").getBoundingClientRect();
|
||||||
|
return favorites.left >= preview.right;
|
||||||
|
}), "favorites do not overlap the preview");
|
||||||
|
await page.locator("#confirmBtn").scrollIntoViewIfNeeded();
|
||||||
|
assert.equal(await page.locator("#confirmBtn").isVisible(), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Native focus hook waits for initialization and preserves active partial edits", options, async t => {
|
||||||
|
const {page, ready, send} = await open(t);
|
||||||
|
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||||
|
assert.notEqual(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||||
|
await send(init(ready, true, true));
|
||||||
|
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||||
|
assert.equal(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||||
|
await page.locator("#rValue").fill("");
|
||||||
|
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||||
|
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||||
|
assert.equal(await page.evaluate(() => document.activeElement.id), "rValue");
|
||||||
|
assert.equal(await page.locator("#rValue").inputValue(), "");
|
||||||
|
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||||
|
assert.equal(await count(page, "confirm"), 0);
|
||||||
|
await page.locator("#rValue").press("Escape");
|
||||||
|
assert.equal(await count(page, "cancel"), 1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
"use strict";
|
||||||
|
(() => {
|
||||||
|
if (window.ORCA_COLOR_PICKER_LANGUAGE) document.documentElement.lang = window.ORCA_COLOR_PICKER_LANGUAGE;
|
||||||
|
const T = (key) => window.ORCA_UI_STRINGS?.[key] || key;
|
||||||
|
document.title = T("Color Picker");
|
||||||
|
for (const element of document.querySelectorAll("[data-i18n]")) element.textContent = T(element.dataset.i18n);
|
||||||
|
for (const attribute of ["aria-label", "title", "placeholder"])
|
||||||
|
for (const element of document.querySelectorAll(`[data-i18n-${attribute}]`))
|
||||||
|
element.setAttribute(attribute, T(element.getAttribute(`data-i18n-${attribute}`)));
|
||||||
|
function paletteName(name) {
|
||||||
|
const ral = name.match(/^(\d{4}) (.+)$/);
|
||||||
|
return ral ? `${ral[1]} ${T(ral[2])}` : /^%\d+$/.test(name) ? name : T(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAVORITE_SLOTS = 16;
|
||||||
|
const SPECTRUM_FOLLOWS_SATURATION = true; // true: the spectrum is drawn at the current saturation, false: it always shows full saturation
|
||||||
|
const MULTI_COLOR_GRADIENTS = true; // true: gradients may have more than 2 colors (− / + buttons and the multi-color palette entries), false: exactly 2 colors.
|
||||||
|
// up to the native limit the host sends as options.max_gradient_colors.
|
||||||
|
let maxGradientColors = 2; // Stays 2 until init, and with a host that does not send a limit.
|
||||||
|
|
||||||
|
const model = window.ColorPickerModel;
|
||||||
|
const palettes = window.ColorPickerPalettes;
|
||||||
|
const byId = (id) => document.getElementById(id);
|
||||||
|
const canvas = byId("spectrum"), context = canvas.getContext("2d");
|
||||||
|
const paletteCombo = byId("paletteCombo"), palette = byId("palette"), preview = byId("colorPreview"), confirmBtn = byId("confirmBtn");
|
||||||
|
const modeSwitch = byId("modeSwitch"), satSlider = byId("satSlider"), opacitySlider = byId("opacitySlider");
|
||||||
|
const gradientBar = byId("gradientCtrlBar"), gradientStops = byId("gradientStops"), gradientAdd = byId("gradientAdd"), gradientRemove = byId("gradientRemove");
|
||||||
|
const hostGradientLimit = (options) => options.max_gradient_colors === undefined ? 2 : options.max_gradient_colors;
|
||||||
|
const originalPreview = byId("originalPreview"), palettePanel = byId("palettePanel"), paletteToggle = byId("paletteToggle");
|
||||||
|
const paletteSelect = byId("paletteSelect"), paletteFilter = byId("paletteFilter"), paletteSearchToggle = byId("paletteSearchToggle");
|
||||||
|
const rgbInputs = ["r", "g", "b"].map((name) => byId(name + "Value"));
|
||||||
|
const hslInputs = ["h", "s", "l"].map((name) => byId(name + "Value"));
|
||||||
|
const hexInput = byId("hexInput"), opacityInput = byId("oValue");
|
||||||
|
const pageId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||||||
|
let state = model.createState(), favorites = [], initialized = false, favoritesWritable = true;
|
||||||
|
// The selection the dialog opened with; the original swatch shows it and restores it on click.
|
||||||
|
let originalSelection = model.exportSelection(state);
|
||||||
|
let lastContentHeight = 0, lastContentWidth = 0, resizePending = false;
|
||||||
|
// Last HSL shown or set. Kept while it still produces the current color: hue 360 and 0 are
|
||||||
|
// both red, and greys, black and white have no hue or saturation in RGB.
|
||||||
|
let hslMemory = [0, 0, 50];
|
||||||
|
function displayHsl(color) {
|
||||||
|
const rgb = model.hslToRgb(...hslMemory);
|
||||||
|
if (!rgb.every((value, index) => value === color[index])) hslMemory = model.rgbToHsl(...color.slice(0, 3));
|
||||||
|
return hslMemory;
|
||||||
|
}
|
||||||
|
// Copy of `values` with one entry replaced.
|
||||||
|
const replaced = (values, index, value) => values.map((current, i) => i === index ? value : current);
|
||||||
|
function setHslChannel(index, value) {
|
||||||
|
hslMemory = replaced(displayHsl(model.activeColor(state)), index, value);
|
||||||
|
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||||
|
}
|
||||||
|
const round1 = (value) => Math.round(value * 10) / 10;
|
||||||
|
|
||||||
|
function emit(command, data = {}) {
|
||||||
|
window.dispatchEvent(new CustomEvent("color-picker-message", {detail: {command, ...data}}));
|
||||||
|
if (window.wx && typeof window.wx.postMessage === "function")
|
||||||
|
window.wx.postMessage(JSON.stringify({command, page_id: pageId, ...data}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleResize() {
|
||||||
|
if (!initialized || resizePending) return;
|
||||||
|
resizePending = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
resizePending = false;
|
||||||
|
const rect = document.body.getBoundingClientRect();
|
||||||
|
const height = Math.min(900 , Math.max(300, Math.ceil(rect.height)));
|
||||||
|
const width = Math.min(1200, Math.max(300, Math.ceil(rect.width )));
|
||||||
|
if (height !== lastContentHeight || width !== lastContentWidth) {
|
||||||
|
lastContentHeight = height; lastContentWidth = width;
|
||||||
|
emit("resize", {width, height});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
new ResizeObserver(scheduleResize).observe(document.body);
|
||||||
|
|
||||||
|
function setSwatch(element, selection) {
|
||||||
|
element.style.setProperty("--swatch-color", model.selectionBackground(selection));
|
||||||
|
}
|
||||||
|
|
||||||
|
// `editing` is the control the user is typing in. Its text is left alone so the caret,
|
||||||
|
// partial input like "05" and fine steps that round-trip through bytes aren't overwritten.
|
||||||
|
function render(resetEditors = false, editing = null) {
|
||||||
|
const color = model.activeColor(state), hsl = displayHsl(color);
|
||||||
|
const show = (input, value) => {
|
||||||
|
if (input === editing) return;
|
||||||
|
if (resetEditors) input.setCustomValidity("");
|
||||||
|
if (resetEditors || input.checkValidity()) input.value = value;
|
||||||
|
};
|
||||||
|
show(hexInput, model.toHex(color).slice(1, state.options.allow_alpha ? 9 : 7));
|
||||||
|
rgbInputs.forEach((input, index) => show(input, color[index]));
|
||||||
|
// Shown in whole numbers; decimals can still be typed, and hslMemory keeps exact values.
|
||||||
|
hslInputs.forEach((input, index) => show(input, Math.round(hsl[index])));
|
||||||
|
// Percentage is a display only until explicitly edited. Never round it back
|
||||||
|
// into the alpha byte on endpoint switches, RGB edits, or confirmation.
|
||||||
|
show(opacityInput, Math.round(color[3] / 255 * 100));
|
||||||
|
if (opacitySlider !== editing) opacitySlider.value = color[3] / 255 * 100;
|
||||||
|
const rgb = color.slice(0, 3).join(",");
|
||||||
|
opacitySlider.style.setProperty("--track-gradient", `linear-gradient(to right, rgba(${rgb},0), rgb(${rgb}))`);
|
||||||
|
opacitySlider.style.setProperty("--thumb-color", `rgba(${rgb},${color[3] / 255})`);
|
||||||
|
if (satSlider !== editing) satSlider.value = hsl[1];
|
||||||
|
satSlider.style.setProperty("--track-gradient", `linear-gradient(to right, hsl(${hsl[0]},0%,${hsl[2]}%), hsl(${hsl[0]},100%,${hsl[2]}%))`);
|
||||||
|
satSlider.style.setProperty("--thumb-color", `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`);
|
||||||
|
modeSwitch.checked = state.mode === "gradient";
|
||||||
|
const selection = model.exportSelection(state);
|
||||||
|
// Kept in the layout (visibility only) so the grid doesn't shift between Solid and Gradient.
|
||||||
|
gradientBar.classList.toggle("invisible", state.mode !== "gradient");
|
||||||
|
if (state.mode === "gradient") renderGradient(selection);
|
||||||
|
setSwatch(preview, selection);
|
||||||
|
preview.title = `${T("Save current color")}\n${selection.colors.join(" → ")}`;
|
||||||
|
if (SPECTRUM_FOLLOWS_SATURATION && hsl[1] !== spectrumSaturation) { spectrumSaturation = hsl[1]; paintSpectrum(); }
|
||||||
|
updateIndicator(hsl);
|
||||||
|
updateConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One thumb per gradient color, evenly spaced; rebuilt only when the number of colors changes.
|
||||||
|
function renderGradient(selection) {
|
||||||
|
const count = selection.colors.length;
|
||||||
|
setSwatch(gradientStops.querySelector(".gradient-track"), selection);
|
||||||
|
let thumbs = [...gradientStops.querySelectorAll(".gradient-thumb")];
|
||||||
|
if (thumbs.length !== count) {
|
||||||
|
thumbs.forEach((thumb) => thumb.remove());
|
||||||
|
thumbs = selection.colors.map((_, index) => {
|
||||||
|
const thumb = document.createElement("button");
|
||||||
|
thumb.type = "button"; thumb.className = "gradient-thumb"; thumb.dataset.index = index;
|
||||||
|
thumb.style.setProperty("--stop", index / (count - 1));
|
||||||
|
thumb.setAttribute("aria-label", count === 2 ? T(index ? "Gradient end" : "Gradient start") : `${T("Gradient color")} ${index + 1}`);
|
||||||
|
gradientStops.appendChild(thumb);
|
||||||
|
return thumb;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
thumbs.forEach((thumb, index) => {
|
||||||
|
thumb.style.setProperty("--thumb-color", selection.colors[index]);
|
||||||
|
thumb.setAttribute("aria-pressed", String(state.active === index));
|
||||||
|
});
|
||||||
|
|
||||||
|
const multi = maxGradientColors > 2; // −/+ buttons only exist when more than 2 colors are allowed; − also needs a color to spare.
|
||||||
|
gradientAdd.parentElement.classList.toggle("hidden", !multi);
|
||||||
|
gradientRemove.classList.toggle("hidden", !multi || count <= 2);
|
||||||
|
gradientAdd.classList.toggle("hidden", !multi);
|
||||||
|
// aria-disabled instead of disabled: a disabled button gets no hover, so its tooltip would never show.
|
||||||
|
const full = count >= maxGradientColors;
|
||||||
|
gradientAdd.setAttribute("aria-disabled", String(full));
|
||||||
|
gradientAdd.title = full ? T("Up to %d colors supported.").replace("%d", maxGradientColors) : T("Add color");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every editor the user can see; alpha only when this caller allows it.
|
||||||
|
function editors() {
|
||||||
|
const inputs = [hexInput, ...rgbInputs, ...hslInputs];
|
||||||
|
if (state.options.allow_alpha) inputs.push(opacityInput);
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
// OK stays disabled while any visible editor holds an invalid or incomplete value.
|
||||||
|
function updateConfirm() {
|
||||||
|
confirmBtn.disabled = !editors().every((input) => input.checkValidity());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches the filter against the shown (translated) name, the English name and the hex digits.
|
||||||
|
function paletteMatches(entry, query) {
|
||||||
|
if (!query) return true;
|
||||||
|
const haystack = [paletteName(entry.name), entry.name, entry.hex || "", entry.grad || ""].join(" ").toLowerCase();
|
||||||
|
return query.split(/\s+/).every((word) => haystack.includes(word.replace(/^#/, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function populatePalette() {
|
||||||
|
palette.replaceChildren();
|
||||||
|
const query = paletteSearchToggle.getAttribute("aria-pressed") === "true" ? paletteFilter.value.trim().toLowerCase() : "";
|
||||||
|
for (const entry of palettes[paletteCombo.value] || []) {
|
||||||
|
const selection = {type: entry.grad ? "gradient" : "solid", colors: entry.grad ? entry.grad.split("-") : [entry.hex]};
|
||||||
|
if (!model.normalizeSelection(selection, state.options, maxGradientColors) || !paletteMatches(entry, query)) continue;
|
||||||
|
const row = document.createElement("button"); row.type = "button"; row.className = "color-item";
|
||||||
|
const swatch = document.createElement("span"); swatch.className = "color-swatch";
|
||||||
|
const name = document.createElement("span"); name.className = "color-name"; name.textContent = paletteName(entry.name);
|
||||||
|
setSwatch(swatch, model.normalizeSelection(selection, {allow_gradient: true, allow_alpha: true}, maxGradientColors));
|
||||||
|
row.append(swatch, name); row.title = paletteName(entry.name);
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
// Solid palette colors edit the active endpoint while gradient mode is on.
|
||||||
|
const changed = entry.grad ? model.applySelection(state, selection) : model.setHex(state, entry.hex);
|
||||||
|
if (changed) render(true);
|
||||||
|
});
|
||||||
|
palette.appendChild(row);
|
||||||
|
}
|
||||||
|
byId("paletteEmpty").classList.toggle("hidden", palette.childElementCount > 0);
|
||||||
|
palette.classList.toggle("hidden", palette.childElementCount === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar state the host keeps in the app config: open/closed and the chosen palette.
|
||||||
|
const uiState = () => ({sidebar_open: palettePanel.classList.contains("open"), palette: paletteCombo.value});
|
||||||
|
function setSidebarOpen(open) {
|
||||||
|
palettePanel.classList.toggle("open", open);
|
||||||
|
paletteToggle.setAttribute("aria-expanded", String(open));
|
||||||
|
}
|
||||||
|
// The search button swaps the combo box for a filter box; closing it clears the filter.
|
||||||
|
function setPaletteSearch(open) {
|
||||||
|
paletteSearchToggle.setAttribute("aria-pressed", String(open));
|
||||||
|
paletteSelect.classList.toggle("hidden", open);
|
||||||
|
paletteFilter.classList.toggle("hidden", !open);
|
||||||
|
if (open) paletteFilter.focus();
|
||||||
|
else paletteFilter.value = "";
|
||||||
|
populatePalette();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFavorites() {
|
||||||
|
const grid = byId("userColors"); grid.replaceChildren();
|
||||||
|
// Favorites keep every color, but only those this page can select are shown.
|
||||||
|
const visible = favorites.filter((favorite) => model.normalizeSelection(favorite, {...state.options, allow_alpha: true}, maxGradientColors));
|
||||||
|
for (let index = 0; index < FAVORITE_SLOTS; ++index) {
|
||||||
|
const swatch = document.createElement("button"); swatch.type = "button"; swatch.className = "saved-swatch";
|
||||||
|
const favorite = visible[index];
|
||||||
|
if (favorite) {
|
||||||
|
setSwatch(swatch, favorite); swatch.title = favorite.colors.join(" → ");
|
||||||
|
swatch.addEventListener("click", () => { if (model.applySelection(state, favorite)) render(true); });
|
||||||
|
} else { swatch.classList.add("empty"); swatch.disabled = true; }
|
||||||
|
grid.appendChild(swatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(payload) {
|
||||||
|
if (!payload || typeof payload !== "object" || (payload.options !== undefined && (!payload.options || typeof payload.options !== "object"))) return false;
|
||||||
|
maxGradientColors = Math.min(MULTI_COLOR_GRADIENTS ? model.MAX_GRADIENT_COLORS : 2, hostGradientLimit(payload.options || {}));
|
||||||
|
// A gradient the host allows but this page does not (switch off) opens as its first color, like
|
||||||
|
// other multi-color selections the picker cannot edit; OK then replaces it, Cancel keeps it.
|
||||||
|
let selection = payload.selection, preserve = payload.preserve_multi_color === true;
|
||||||
|
if (selection?.type === "gradient" && selection.colors?.length > maxGradientColors) {
|
||||||
|
selection = {type: "solid", colors: [selection.colors[0]]};
|
||||||
|
preserve = true;
|
||||||
|
}
|
||||||
|
const next = model.createState({...payload.options, max_gradient_colors: maxGradientColors}, selection);
|
||||||
|
if (!next) return false;
|
||||||
|
byId("selectionNotice").hidden = !preserve;
|
||||||
|
state = next; favorites = model.normalizeFavorites(payload.favorites || []);
|
||||||
|
originalSelection = model.exportSelection(state);
|
||||||
|
setSwatch(originalPreview, originalSelection);
|
||||||
|
originalPreview.title = `${T("Original color")}\n${originalSelection.colors.join(" → ")}`;
|
||||||
|
favoritesWritable = payload.favorites_writable !== false;
|
||||||
|
preview.disabled = !favoritesWritable;
|
||||||
|
byId("modeControls").classList.toggle("hidden", !state.options.allow_gradient);
|
||||||
|
for (const option of paletteCombo.options) option.hidden = option.value === "gradient" && !state.options.allow_gradient;
|
||||||
|
const ui = payload.ui || {}; // Restore the sidebar the way it was left; a palette this caller can't use falls back to basic.
|
||||||
|
const saved = [...paletteCombo.options].find((option) => option.value === ui.palette && !option.hidden);
|
||||||
|
if (saved) paletteCombo.value = saved.value;
|
||||||
|
if (!state.options.allow_gradient && paletteCombo.value === "gradient") paletteCombo.value = "basic";
|
||||||
|
setSidebarOpen(ui.sidebar_open === true);
|
||||||
|
byId("alphaControls").classList.toggle("hidden", !state.options.allow_alpha);
|
||||||
|
hexInput.maxLength = state.options.allow_alpha ? 9 : 7;
|
||||||
|
populatePalette(); drawFavorites(); render(true);
|
||||||
|
initialized = true;
|
||||||
|
byId("pickerEditors").disabled = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
if (!initialized) return;
|
||||||
|
// Restore initial keyboard focus without replacing an editor's in-progress input.
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (!active || active === document.body || active === document.documentElement)
|
||||||
|
hexInput.focus({preventScroll: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(payload) {
|
||||||
|
if (!payload || typeof payload !== "object" || payload.page_id !== pageId) return false;
|
||||||
|
if (payload.command === "init") {
|
||||||
|
const options = payload.options;
|
||||||
|
const hostLimit = options && hostGradientLimit(options);
|
||||||
|
if (!options || typeof options.allow_gradient !== "boolean" || typeof options.allow_alpha !== "boolean" ||
|
||||||
|
!Number.isInteger(hostLimit) || hostLimit < 2 || hostLimit > model.MAX_GRADIENT_COLORS ||
|
||||||
|
!model.normalizeSelection(payload.selection, options, hostLimit) || !validFavorites(payload.favorites) ||
|
||||||
|
(payload.favorites_writable !== undefined && typeof payload.favorites_writable !== "boolean") ||
|
||||||
|
(payload.preserve_multi_color !== undefined && typeof payload.preserve_multi_color !== "boolean") ||
|
||||||
|
(payload.ui !== undefined && (!payload.ui || typeof payload.ui !== "object" ||
|
||||||
|
(payload.ui.sidebar_open !== undefined && typeof payload.ui.sidebar_open !== "boolean") ||
|
||||||
|
(payload.ui.palette !== undefined && typeof payload.ui.palette !== "string")))) return false;
|
||||||
|
if (initialized) return true;
|
||||||
|
if (!init(payload)) return false;
|
||||||
|
emit("initialized");
|
||||||
|
scheduleResize();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (payload.command === "favorites" && initialized && validFavorites(payload.favorites)) {
|
||||||
|
favorites = model.normalizeFavorites(payload.favorites); drawFavorites(); return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validFavorites(values) {
|
||||||
|
return Array.isArray(values) && values.length <= 24 && values.every((value) => model.normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, model.MAX_GRADIENT_COLORS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saturation the spectrum is currently painted at (always 100 when the option is off).
|
||||||
|
let spectrumSaturation = 100;
|
||||||
|
function drawSpectrum() {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
canvas.width = Math.max(1, Math.round(rect.width)); canvas.height = Math.max(1, Math.round(rect.height));
|
||||||
|
paintSpectrum();
|
||||||
|
}
|
||||||
|
function paintSpectrum() {
|
||||||
|
const hue = context.createLinearGradient(0, 0, canvas.width, 0);
|
||||||
|
for (let i = 0; i <= 6; ++i) hue.addColorStop(i / 6, `hsl(${i * 60},100%,50%)`);
|
||||||
|
context.fillStyle = hue; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
const shade = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||||
|
shade.addColorStop(0, "#FFFFFF"); shade.addColorStop(0.5, "#FFFFFF00"); shade.addColorStop(0.5, "#00000000"); shade.addColorStop(1, "#000000");
|
||||||
|
context.fillStyle = shade; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
// HSL desaturation blends each color toward the grey of the same lightness, and on this
|
||||||
|
// canvas that grey only depends on the row (white at the top, black at the bottom).
|
||||||
|
if (spectrumSaturation < 100) {
|
||||||
|
const grey = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||||
|
grey.addColorStop(0, "#FFFFFF"); grey.addColorStop(1, "#000000");
|
||||||
|
context.globalAlpha = 1 - spectrumSaturation / 100;
|
||||||
|
context.fillStyle = grey; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicator = byId("colorIndicator");
|
||||||
|
function updateIndicator(hsl) {
|
||||||
|
indicator.style.left = hsl[0] / 360 * (canvas.width - 1) + "px";
|
||||||
|
indicator.style.top = (1 - hsl[2] / 100) * (canvas.height - 1) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spectrum position maps straight to hue (x, 0..360) and lightness (y, 100..0); saturation is
|
||||||
|
// the third dimension and comes from the slider below. hslMemory keeps these exact values,
|
||||||
|
// including hue 360 at the right edge and the hue of the white and black corners.
|
||||||
|
function sampleSpectrum(event) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const fraction = (client, start, size, pixels) =>
|
||||||
|
Math.max(0, Math.min(pixels - 1, Math.floor((client - start) * pixels / size))) / Math.max(1, pixels - 1);
|
||||||
|
const saturation = displayHsl(model.activeColor(state))[1];
|
||||||
|
hslMemory = [round1(fraction(event.clientX, rect.left, rect.width, canvas.width) * 360), saturation,
|
||||||
|
round1((1 - fraction(event.clientY, rect.top, rect.height, canvas.height)) * 100)];
|
||||||
|
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||||
|
// A pick replaces every editor's value, so clear any invalid state left from typing.
|
||||||
|
render(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (control, value) => Math.min(Number(control.max), Math.max(Number(control.min), value));
|
||||||
|
function bindValue(control, apply) {
|
||||||
|
// Number fields are required, so an empty field is invalid; a range input always has a value.
|
||||||
|
if (control.type === "number") control.required = true;
|
||||||
|
const commit = (value) => { apply(value); render(false, control); };
|
||||||
|
control.addEventListener("input", () => {
|
||||||
|
control.setCustomValidity("");
|
||||||
|
// Out-of-range numbers snap to the field's min/max instead of becoming invalid.
|
||||||
|
if (control.value !== "") {
|
||||||
|
const clamped = clamp(control, Number(control.value));
|
||||||
|
if (clamped !== Number(control.value)) control.value = clamped;
|
||||||
|
}
|
||||||
|
if (control.checkValidity()) commit(Number(control.value));
|
||||||
|
});
|
||||||
|
control.addEventListener("wheel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const step = control.type === "range" ? 5 : 1;
|
||||||
|
control.value = clamp(control, (Number(control.value) || 0) + (event.deltaY > 0 ? -step : step));
|
||||||
|
control.setCustomValidity("");
|
||||||
|
commit(Number(control.value));
|
||||||
|
}, {passive: false});
|
||||||
|
// Normalize the text (e.g. "05" → "5") once the user leaves the field.
|
||||||
|
control.addEventListener("blur", () => { if (control.checkValidity()) render(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
rgbInputs.forEach((input, index) => bindValue(input, (value) => model.setRgb(state, replaced(model.activeColor(state).slice(0, 3), index, value))));
|
||||||
|
hslInputs.forEach((input, index) => bindValue(input, (value) => setHslChannel(index, value)));
|
||||||
|
bindValue(satSlider, (value) => setHslChannel(1, value));
|
||||||
|
bindValue(opacityInput, (value) => model.setAlphaPercent(state, value));
|
||||||
|
bindValue(opacitySlider, (value) => model.setAlphaPercent(state, value));
|
||||||
|
// The opacity slider takes the gradient bar's row while the opacity field or the slider is in
|
||||||
|
// use: it opens when the field is clicked or focused, and closes on a click or focus elsewhere.
|
||||||
|
const thirdRow = byId("thirdRow"), alphaControls = byId("alphaControls");
|
||||||
|
const inOpacityControls = (target) => target instanceof Element && target.closest("#alphaControls, #opacitySliderBar");
|
||||||
|
const setOpacitySliderOpen = (open) => thirdRow.classList.toggle("opacity-open", open);
|
||||||
|
for (const type of ["focusin", "click"]) alphaControls.addEventListener(type, () => setOpacitySliderOpen(true));
|
||||||
|
for (const type of ["pointerdown", "focusin"])
|
||||||
|
document.addEventListener(type, (event) => { if (!inOpacityControls(event.target)) setOpacitySliderOpen(false); });
|
||||||
|
hexInput.addEventListener("input", () => {
|
||||||
|
const text = hexInput.value;
|
||||||
|
const valid = model.setHex(state, text.startsWith("#") ? text : "#" + text);
|
||||||
|
hexInput.setCustomValidity(valid ? "" : T("Enter a complete hexadecimal color"));
|
||||||
|
// render() leaves the typed text alone, so alpha isn't appended after the sixth digit.
|
||||||
|
if (valid) render(false, hexInput);
|
||||||
|
});
|
||||||
|
hexInput.addEventListener("blur", () => { if (hexInput.checkValidity()) render(); });
|
||||||
|
modeSwitch.addEventListener("change", () => {
|
||||||
|
|
||||||
|
setOpacitySliderOpen(false); // Close the opacity slider before the row changes, so it doesn't linger over the gradient bar.
|
||||||
|
model.setMode(state, modeSwitch.checked ? "gradient" : "solid");
|
||||||
|
render(true);
|
||||||
|
});
|
||||||
|
// Click on the track selects the nearest thumb. Keyboard users tab to a thumb and press Enter or Space.
|
||||||
|
gradientStops.addEventListener("click", (event) => {
|
||||||
|
if (state.mode !== "gradient") return;
|
||||||
|
// A thumb is read from its index: a keyboard-triggered click has no pointer position.
|
||||||
|
const thumb = event.target.closest(".gradient-thumb"), rect = gradientStops.getBoundingClientRect();
|
||||||
|
const last = state.gradient.length - 1;
|
||||||
|
// Thumbs sit at 10px + (width - 20px) * i / last, matching the CSS.
|
||||||
|
const fraction = Math.max(0, Math.min(1, (event.clientX - rect.left - 10) / Math.max(1, rect.width - 20)));
|
||||||
|
const index = thumb ? Number(thumb.dataset.index) : Math.round(fraction * last);
|
||||||
|
if (index !== state.active) { state.active = index; render(true); }
|
||||||
|
});
|
||||||
|
gradientAdd.addEventListener("click", () => { if (gradientAdd.getAttribute("aria-disabled") !== "true" && model.addGradientColor(state)) render(true); });
|
||||||
|
gradientRemove.addEventListener("click", () => { if (model.removeGradientColor(state)) render(true); });
|
||||||
|
originalPreview.addEventListener("click", () => { if (model.applySelection(state, originalSelection)) render(true); });
|
||||||
|
paletteToggle.addEventListener("click", () => {
|
||||||
|
setSidebarOpen(!palettePanel.classList.contains("open"));
|
||||||
|
if (initialized) emit("update_ui", uiState());
|
||||||
|
});
|
||||||
|
paletteCombo.addEventListener("change", () => {
|
||||||
|
populatePalette();
|
||||||
|
if (initialized) emit("update_ui", uiState());
|
||||||
|
});
|
||||||
|
paletteSearchToggle.addEventListener("click", () => setPaletteSearch(paletteSearchToggle.getAttribute("aria-pressed") !== "true"));
|
||||||
|
paletteFilter.addEventListener("input", populatePalette);
|
||||||
|
// Pointer capture ends by itself on pointerup/pointercancel, so it doubles as the drag state.
|
||||||
|
canvas.addEventListener("pointerdown", (event) => { canvas.setPointerCapture(event.pointerId); sampleSpectrum(event); });
|
||||||
|
canvas.addEventListener("pointermove", (event) => { if (canvas.hasPointerCapture(event.pointerId)) sampleSpectrum(event); });
|
||||||
|
// The spectrum takes the panel's free height, which also changes with the notice, alpha and
|
||||||
|
// gradient controls, so redraw on any size change rather than on window resize only.
|
||||||
|
new ResizeObserver(() => { drawSpectrum(); updateIndicator(displayHsl(model.activeColor(state))); }).observe(canvas.parentElement);
|
||||||
|
// Bubble phase on document runs after each editor's own handler has set its validity.
|
||||||
|
document.addEventListener("input", updateConfirm);
|
||||||
|
// Hex keeps its own message from its input handler; number fields get a translated one.
|
||||||
|
function validateEditors() {
|
||||||
|
const inputs = editors();
|
||||||
|
for (const input of inputs.filter((input) => input !== hexInput)) {
|
||||||
|
input.setCustomValidity("");
|
||||||
|
const {valid, rangeUnderflow, rangeOverflow} = input.validity;
|
||||||
|
if (!valid) input.setCustomValidity(T(rangeUnderflow || rangeOverflow ? "Value is out of range." : "Invalid input"));
|
||||||
|
}
|
||||||
|
return inputs.every((input) => input.reportValidity());
|
||||||
|
}
|
||||||
|
function confirm() {
|
||||||
|
if (initialized && validateEditors()) emit("confirm", {selection: model.exportSelection(state)});
|
||||||
|
}
|
||||||
|
preview.addEventListener("click", () => {
|
||||||
|
if (!initialized || !favoritesWritable || !validateEditors()) return;
|
||||||
|
const selection = model.exportSelection(state);
|
||||||
|
const key = JSON.stringify(selection);
|
||||||
|
favorites = [selection, ...favorites.filter((favorite) => JSON.stringify(favorite) !== key)].slice(0, FAVORITE_SLOTS);
|
||||||
|
drawFavorites(); emit("update_favorites", {favorites});
|
||||||
|
});
|
||||||
|
confirmBtn.addEventListener("click", confirm);
|
||||||
|
byId("cancelBtn").addEventListener("click", () => emit("cancel"));
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
// Escape in the filter box closes the search first; a second Escape cancels the dialog.
|
||||||
|
if (event.key === "Escape" && event.target === paletteFilter) { event.preventDefault(); setPaletteSearch(false); paletteSearchToggle.focus(); }
|
||||||
|
else if (event.key === "Escape") { event.preventDefault(); emit("cancel"); }
|
||||||
|
else if (event.key === "Enter" && event.target instanceof HTMLInputElement && ["text", "number"].includes(event.target.type)) {
|
||||||
|
event.preventDefault(); confirm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ColorPickerDialog = {
|
||||||
|
init, handleMessage, focusInput, exportSelection: () => model.exportSelection(state),
|
||||||
|
setFavorites: (values) => { favorites = model.normalizeFavorites(values); drawFavorites(); }
|
||||||
|
};
|
||||||
|
// The host passes the saved sidebar state before the page loads (the same values come again in
|
||||||
|
// init), so the first paint already matches the window size the host opened with.
|
||||||
|
const earlyUi = window.ORCA_COLOR_PICKER_UI;
|
||||||
|
if (earlyUi && typeof earlyUi === "object") {
|
||||||
|
if (typeof earlyUi.palette === "string" && [...paletteCombo.options].some((option) => option.value === earlyUi.palette))
|
||||||
|
paletteCombo.value = earlyUi.palette;
|
||||||
|
if (typeof earlyUi.sidebar_open === "boolean") setSidebarOpen(earlyUi.sidebar_open);
|
||||||
|
}
|
||||||
|
setSwatch(originalPreview, originalSelection);
|
||||||
|
drawSpectrum(); populatePalette(); drawFavorites(); render(true); emit("ready");
|
||||||
|
})();
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"use strict";
|
||||||
|
const {test} = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const model = require("./colorpicker-model.js");
|
||||||
|
|
||||||
|
test("complete RGB/RGBA hex accepts mixed case and rejects partial or unbounded input", () => {
|
||||||
|
assert.deepEqual(model.parseHex("#Ab12cD80"), [171,18,205,128]);
|
||||||
|
for (const hex of ["", "123456", "#123", "#1234567", "#123456789", "#12345Z", "#123456GG", " #123456", null]) assert.equal(model.parseHex(hex), null);
|
||||||
|
for (let byte = 0; byte <= 255; ++byte) {
|
||||||
|
const hex = model.toHex([byte, byte, byte, byte]);
|
||||||
|
assert.deepEqual(model.parseHex(hex), [byte,byte,byte,byte]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("HSL conversion preserves primary channels and hue wraps at 360", () => {
|
||||||
|
for (const rgb of [[255,0,0], [0,255,0], [0,0,255], [128,128,128], [37,129,240]]) assert.deepEqual(model.hslToRgb(...model.rgbToHsl(...rgb)), rgb);
|
||||||
|
assert.deepEqual(model.hslToRgb(360,100,50), [255,0,0]);
|
||||||
|
assert.deepEqual(model.hslToRgb(240,100,50), [0,0,255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("capabilities default off and reject gradient through every model entry", () => {
|
||||||
|
const gradient = {type: "gradient", colors: ["#FF000080", "#0000FF20"]};
|
||||||
|
const state = model.createState();
|
||||||
|
assert.deepEqual(state.options, {allow_gradient: false, allow_alpha: false});
|
||||||
|
assert.equal(model.createState({}, gradient), null);
|
||||||
|
assert.equal(model.applySelection(state, gradient), false);
|
||||||
|
assert.equal(model.setMode(state, "gradient"), false);
|
||||||
|
assert.equal(model.setAlphaPercent(state, 20), false);
|
||||||
|
assert.equal(model.createState({allow_alpha: true}, gradient), null);
|
||||||
|
const opaque = model.createState({allow_gradient: true}, gradient);
|
||||||
|
assert.deepEqual(model.exportSelection(opaque), {type: "gradient", colors: ["#FF0000FF", "#0000FFFF"]});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gradient endpoint edits keep order and preserve unedited byte alpha", () => {
|
||||||
|
const initial = {type: "gradient", colors: ["#FF000001", "#0000FF80"]};
|
||||||
|
const state = model.createState({allow_gradient: true, allow_alpha: true}, initial);
|
||||||
|
state.active = 0; model.setRgb(state, [0,255,0]);
|
||||||
|
state.active = 1; model.setHex(state, "#112233");
|
||||||
|
assert.deepEqual(model.exportSelection(state), {type: "gradient", colors: ["#00FF0001", "#11223380"]});
|
||||||
|
model.setAlphaPercent(state, 0);
|
||||||
|
assert.deepEqual(model.exportSelection(state).colors, ["#00FF0001", "#11223300"]);
|
||||||
|
model.setAlphaPercent(state, 100);
|
||||||
|
assert.equal(model.exportSelection(state).colors[1], "#112233FF");
|
||||||
|
assert.deepEqual(initial.colors, ["#FF000001", "#0000FF80"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("favorites retain alpha and gradient even when the selection disables them", () => {
|
||||||
|
const favorite = {type: "solid", colors: ["#ABCDEF80"]};
|
||||||
|
const gradient = {type: "gradient", colors: ["#FFFFFF00", "#00000010"]};
|
||||||
|
const favorites = model.normalizeFavorites([favorite, gradient]);
|
||||||
|
const state = model.createState();
|
||||||
|
assert.equal(model.applySelection(state, favorites[0]), true);
|
||||||
|
assert.deepEqual(model.exportSelection(state), {type: "solid", colors: ["#ABCDEFFF"]});
|
||||||
|
assert.deepEqual(favorites, [favorite, gradient]);
|
||||||
|
assert.equal(model.applySelection(state, favorites[1]), false);
|
||||||
|
assert.equal(model.normalizeFavorites(Array(25).fill(favorite)).length, 24);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid cardinality and channel edits cannot change a selection", () => {
|
||||||
|
const state = model.createState({allow_alpha: true});
|
||||||
|
const before = model.exportSelection(state);
|
||||||
|
for (const value of [{type: "solid", colors: []}, {type: "solid", colors: ["#112233", "#445566"]}, {type: "gradient", colors: ["#112233"]}, {type: "other", colors: ["#112233"]}]) assert.equal(model.applySelection(state, value), false);
|
||||||
|
assert.equal(model.setHex(state, "#GG2233"), false);
|
||||||
|
for (const value of [-1, 101, NaN, Infinity]) assert.equal(model.setAlphaPercent(state, value), false);
|
||||||
|
assert.equal(model.setRgb(state, [256,0,0]), false);
|
||||||
|
assert.deepEqual(model.exportSelection(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multi-color gradients stay at 2 colors unless the caller raises the limit", () => {
|
||||||
|
const five = {type: "gradient", colors: ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"]};
|
||||||
|
assert.equal(model.normalizeSelection(five, {allow_gradient: true}), null);
|
||||||
|
assert.equal(model.createState({allow_gradient: true}, five), null);
|
||||||
|
const state = model.createState({allow_gradient: true, max_gradient_colors: 7}, five);
|
||||||
|
assert.equal(state.gradient.length, 5);
|
||||||
|
assert.equal(model.normalizeSelection({...five, colors: Array(8).fill("#000000")}, {allow_gradient: true}, 7), null);
|
||||||
|
assert.equal(model.createState({allow_gradient: true, max_gradient_colors: 99}).maxGradientColors, model.MAX_GRADIENT_COLORS);
|
||||||
|
assert.equal(model.normalizeFavorites([five]).length, 1, "favorites keep every color");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adding and removing gradient colors keeps order, selection and the 2..max limits", () => {
|
||||||
|
const state = model.createState({allow_gradient: true, allow_alpha: true, max_gradient_colors: 4}, {type: "gradient", colors: ["#FF000080", "#0000FFFF"]});
|
||||||
|
assert.equal(model.removeGradientColor(state), false, "never fewer than 2");
|
||||||
|
assert.equal(model.addGradientColor(state), true);
|
||||||
|
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080", "#0000FFFF"], "selected color turned 75° in hue, alpha kept");
|
||||||
|
assert.equal(model.NEW_GRADIENT_HUE_SHIFT, 75);
|
||||||
|
assert.equal(state.active, 1);
|
||||||
|
state.active = 2; assert.equal(model.addGradientColor(state), true);
|
||||||
|
assert.equal(state.active, 3); assert.equal(state.gradient.length, 4);
|
||||||
|
assert.equal(model.addGradientColor(state), false, "limit reached");
|
||||||
|
state.active = 1; assert.equal(model.removeGradientColor(state), true);
|
||||||
|
assert.deepEqual(model.exportSelection(state).colors.slice(0, 2), ["#FF000080", "#0000FFFF"]);
|
||||||
|
assert.equal(state.active, 1);
|
||||||
|
assert.equal(model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]}), true);
|
||||||
|
state.active = 1; model.addGradientColor(state); model.addGradientColor(state);
|
||||||
|
model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]});
|
||||||
|
assert.equal(state.active, 1, "a shorter gradient clamps the selected color");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a new gradient starts at the solid color and its 75° hue neighbour", () => {
|
||||||
|
const state = model.createState({allow_gradient: true, allow_alpha: true}, {type: "solid", colors: ["#FF000080"]});
|
||||||
|
assert.equal(model.setMode(state, "gradient"), true);
|
||||||
|
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080"]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-orca-page="color-picker">
|
||||||
|
<head><title data-i18n="Color Picker">Color Picker</title>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../include/global.css">
|
||||||
|
<link rel="stylesheet" href="../css/theme.css">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p id="selectionNotice" hidden data-i18n="The current multi-color selection is kept until you choose a different color.">The current multi-color selection is kept until you choose a different color.</p>
|
||||||
|
<fieldset id="pickerEditors" disabled>
|
||||||
|
<div class="main-wrapper">
|
||||||
|
|
||||||
|
<div class="left-panel" id="palettePanel">
|
||||||
|
<div class="palette thin-scroll" id="palette" tabindex="-1"></div>
|
||||||
|
<p class="palette-empty hidden" id="paletteEmpty" data-i18n="No matching colors">No matching colors</p>
|
||||||
|
<div class="palette-toolbar">
|
||||||
|
<div class="palette-select ComboBox" id="paletteSelect">
|
||||||
|
<div class="arrow-icon"></div>
|
||||||
|
<select id="paletteCombo" tabindex="-1">
|
||||||
|
<option value="basic" data-i18n="Basic Colors">Basic Colors</option>
|
||||||
|
<option value="orca" data-i18n="Orca Palette">Orca Palette</option>
|
||||||
|
<option value="ral" data-i18n="RAL Classic" >RAL Classic</option>
|
||||||
|
<option value="gradient" hidden data-i18n="Gradients" >Gradients</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input type="search" id="paletteFilter" class="palette-filter hidden" autocomplete="off" spellcheck="false"
|
||||||
|
placeholder="Filter colors" data-i18n-placeholder="Filter colors" aria-label="Filter colors" data-i18n-aria-label="Filter colors">
|
||||||
|
<button type="button" id="paletteSearchToggle" class="palette-search-button" aria-pressed="false" title="Search colors" data-i18n-title="Search colors">
|
||||||
|
<div class="icon-search icon16"></div>
|
||||||
|
<div class="icon-close icon16"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="spectrum-row">
|
||||||
|
<div class="saturation-slider">
|
||||||
|
<input class="valueSlider" type="range" id="satSlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Saturation" data-i18n-aria-label="Saturation">
|
||||||
|
</div>
|
||||||
|
<div class="spectrum-container">
|
||||||
|
<canvas id="spectrum"></canvas>
|
||||||
|
<div id="colorIndicator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-grid">
|
||||||
|
<div class="editor-column">
|
||||||
|
<div id="modeControls" class="hidden">
|
||||||
|
<input class="switch-check" type="checkbox" id="modeSwitch">
|
||||||
|
<label class="switch-slider" for="modeSwitch">
|
||||||
|
<div class="switch-label" data-i18n="Solid">Solid</div>
|
||||||
|
<div class="switch-label" data-i18n="Gradient">Gradient</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hex-row">
|
||||||
|
<div class="inline-field field-hex">
|
||||||
|
<label for="hexInput">#</label>
|
||||||
|
<input type="text" id="hexInput" required value="808080" maxlength="8" autocomplete="off" spellcheck="false" aria-label="Hex color" data-i18n-aria-label="Hex color">
|
||||||
|
</div>
|
||||||
|
<div class="inline-field value-field hidden" id="alphaControls" title="Opacity percent">
|
||||||
|
<label for="oValue">
|
||||||
|
<svg viewBox="0 0 12 12" width="12" height="12">
|
||||||
|
<path d="M0,1v10c0,.55.45,1,1,1h10c.55,0,1-.45,1-1V1c0-.55-.45-1-1-1H1C.45,0,0,.45,0,1ZM6,6v5H1v-5h5V1h5v5h-5Z" style="fill:#999"/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<input type="number" id="oValue" min="0" max="100" aria-label="Opacity percent" data-i18n-aria-label="Opacity percent">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Third row: gradient endpoints, replaced by the opacity slider while the opacity field is in use. -->
|
||||||
|
<div id="thirdRow" class="third-row">
|
||||||
|
<!-- Gradient colors: evenly spaced thumbs (built by colorpicker.js); a click selects the nearest. -->
|
||||||
|
<div id="gradientCtrlBar" class="invisible" role="group" aria-label="Gradient" data-i18n-aria-label="Gradient">
|
||||||
|
<div id="gradientStops" class="gradient-stops">
|
||||||
|
<div class="gradient-track"></div>
|
||||||
|
</div>
|
||||||
|
<div class="gradient-step-buttons hidden">
|
||||||
|
<button type="button" id="gradientRemove" class="gradient-step-button hidden" title="Remove color" data-i18n-title="Remove color">
|
||||||
|
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="gradientAdd" class="gradient-step-button hidden" title="Add color" data-i18n-title="Add color">
|
||||||
|
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/><rect x="4" y="0" width="2" height="10" fill="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="opacitySliderBar" hidden>
|
||||||
|
<input class="valueSlider" type="range" id="opacitySlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Opacity" data-i18n-aria-label="Opacity">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-column">
|
||||||
|
<div class="inline-field value-field" title="Hue">
|
||||||
|
<label for="hValue">H</label>
|
||||||
|
<input type="number" id="hValue" min="0" max="360" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="inline-field value-field" title="Saturation">
|
||||||
|
<label for="sValue">S</label>
|
||||||
|
<input type="number" id="sValue" min="0" max="100" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="inline-field value-field" title="Lightness">
|
||||||
|
<label for="lValue">L</label>
|
||||||
|
<input type="number" id="lValue" min="0" max="100" step="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-column">
|
||||||
|
<div class="inline-field value-field" title="Red">
|
||||||
|
<label for="rValue">R</label>
|
||||||
|
<input type="number" id="rValue" min="0" max="255" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="inline-field value-field" title="Green">
|
||||||
|
<label for="gValue">G</label>
|
||||||
|
<input type="number" id="gValue" min="0" max="255" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="inline-field value-field" title="Blue">
|
||||||
|
<label for="bValue">B</label>
|
||||||
|
<input type="number" id="bValue" min="0" max="255" step="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-container">
|
||||||
|
<button id="originalPreview" type="button" class="color-preview original-preview" title="Original color" data-i18n-title="Original color">
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#ff6f00;"/>
|
||||||
|
<g style="fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;">
|
||||||
|
<polyline points="5.5 5.5 5.5 9.5 9.5 9.5" />
|
||||||
|
<path d="M5.84,9.5c.82-2.33,3.05-4,5.66-4,3.31,0,6,2.69,6,6s-2.69,6-6,6c-2.95,0-5.41-2.13-5.91-4.94"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="colorPreview" class="color-preview" title="Save current color" data-i18n-title="Save current color">
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#009688"/>
|
||||||
|
<path d="M11.85,5.11l1.43,4.39h4.61c.35,0,.5.45.22.66l-3.73,2.71,1.43,4.39c.11.34-.28.62-.56.41l-3.73-2.71-3.73,2.71c-.29.21-.67-.07-.56-.41l1.43-4.39-3.73-2.71c-.29-.21-.14-.66.22-.66h4.61s1.43-4.39,1.43-4.39c.11-.34.59-.34.7,0Z" style="fill:none; stroke:#fff; stroke-linecap:round; stroke-linejoin:round;"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="saved-grid" id="userColors"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="button" id="paletteToggle" class="icon-button" aria-expanded="false" aria-controls="palettePanel" title="Palettes" data-i18n-title="Palettes">
|
||||||
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="2" fill="none" stroke="currentColor"/><path d="M5.5 2.5v11" stroke="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="stretch-spacer"></div>
|
||||||
|
<button id="confirmBtn" class="ButtonTypeChoice ButtonStyleConfirm" data-i18n="OK">OK</button>
|
||||||
|
<button id="cancelBtn" class="ButtonTypeChoice ButtonStyleRegular" data-i18n="Cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<script src="palettes.js"></script><script src="colorpicker-model.js"></script><script src="colorpicker.js"></script></body></html>
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
"use strict";
|
||||||
|
(() => {
|
||||||
|
const cssColors = [
|
||||||
|
{hex: "#FF0000", name: "Red"},
|
||||||
|
{hex: "#DC143C", name: "Crimson"},
|
||||||
|
{hex: "#FF4500", name: "Orange red"},
|
||||||
|
{hex: "#FF6347", name: "Tomato"},
|
||||||
|
{hex: "#FFA500", name: "Orange"},
|
||||||
|
{hex: "#FF8C00", name: "Dark orange"},
|
||||||
|
{hex: "#FFD700", name: "Gold"},
|
||||||
|
{hex: "#DEB887", name: "Burly wood"},
|
||||||
|
{hex: "#FFE4C4", name: "Bisque"},
|
||||||
|
{hex: "#FFE4E1", name: "Misty rose"},
|
||||||
|
{hex: "#FFFF00", name: "Yellow"},
|
||||||
|
{hex: "#FFFFE0", name: "Light yellow"},
|
||||||
|
{hex: "#FFFACD", name: "Lemon chiffon"},
|
||||||
|
{hex: "#F5F5DC", name: "Beige"},
|
||||||
|
{hex: "#FAFAD2", name: "Light golden rod yellow"},
|
||||||
|
{hex: "#EEE8AA", name: "Pale golden rod"},
|
||||||
|
{hex: "#F0E68C", name: "Khaki"},
|
||||||
|
{hex: "#BDB76B", name: "Dark khaki"},
|
||||||
|
{hex: "#FFDAB9", name: "Peach puff"},
|
||||||
|
{hex: "#FFE4B5", name: "Moccasin"},
|
||||||
|
{hex: "#FFEFD5", name: "Papaya whip"},
|
||||||
|
{hex: "#FFEBCD", name: "Blanched almond"},
|
||||||
|
{hex: "#FAEBD7", name: "Antique white"},
|
||||||
|
{hex: "#FFDEAD", name: "Navajo white"},
|
||||||
|
{hex: "#F5DEB3", name: "Wheat"},
|
||||||
|
{hex: "#DAA520", name: "Golden rod"},
|
||||||
|
{hex: "#D2691E", name: "Chocolate"},
|
||||||
|
{hex: "#CD853F", name: "Peru"},
|
||||||
|
{hex: "#B22222", name: "Fire brick"},
|
||||||
|
{hex: "#A0522D", name: "Sienna"},
|
||||||
|
{hex: "#8B4513", name: "Saddle brown"},
|
||||||
|
{hex: "#A52A2A", name: "Brown"},
|
||||||
|
{hex: "#BC8F8F", name: "Rosy brown"},
|
||||||
|
{hex: "#FA8072", name: "Salmon"},
|
||||||
|
{hex: "#FFA07A", name: "Light salmon"},
|
||||||
|
{hex: "#E9967A", name: "Dark salmon"},
|
||||||
|
{hex: "#F4A460", name: "Sandy brown"},
|
||||||
|
{hex: "#FF7F50", name: "Coral"},
|
||||||
|
{hex: "#F08080", name: "Light coral"},
|
||||||
|
{hex: "#CD5C5C", name: "Indian red"},
|
||||||
|
{hex: "#8B0000", name: "Dark red"},
|
||||||
|
{hex: "#800000", name: "Maroon"},
|
||||||
|
{hex: "#ADFF2F", name: "Green yellow"},
|
||||||
|
{hex: "#9ACD32", name: "Yellow green"},
|
||||||
|
{hex: "#7FFF00", name: "Chartreuse"},
|
||||||
|
{hex: "#7CFC00", name: "Lawn green"},
|
||||||
|
{hex: "#32CD32", name: "Lime green"},
|
||||||
|
{hex: "#00FF7F", name: "Spring green"},
|
||||||
|
{hex: "#00FA9A", name: "Medium spring green"},
|
||||||
|
{hex: "#00FF00", name: "Lime"},
|
||||||
|
{hex: "#008000", name: "Green"},
|
||||||
|
{hex: "#228B22", name: "Forest green"},
|
||||||
|
{hex: "#006400", name: "Dark green"},
|
||||||
|
{hex: "#6B8E23", name: "Olive drab"},
|
||||||
|
{hex: "#808000", name: "Olive"},
|
||||||
|
{hex: "#556B2F", name: "Dark olive green"},
|
||||||
|
{hex: "#98FB98", name: "Pale green"},
|
||||||
|
{hex: "#90EE90", name: "Light green"},
|
||||||
|
{hex: "#3CB371", name: "Medium sea green"},
|
||||||
|
{hex: "#2E8B57", name: "Sea green"},
|
||||||
|
{hex: "#8FBC8F", name: "Dark sea green"},
|
||||||
|
{hex: "#5F9EA0", name: "Cadet blue"},
|
||||||
|
{hex: "#66CDAA", name: "Medium aqua marine"},
|
||||||
|
{hex: "#40E0D0", name: "Turquoise"},
|
||||||
|
{hex: "#48D1CC", name: "Medium turquoise"},
|
||||||
|
{hex: "#00CED1", name: "Dark turquoise"},
|
||||||
|
{hex: "#AFEEEE", name: "Pale turquoise"},
|
||||||
|
{hex: "#00FFFF", name: "Aqua"},
|
||||||
|
{hex: "#00FFFF", name: "Cyan"},
|
||||||
|
{hex: "#E0FFFF", name: "Light cyan"},
|
||||||
|
{hex: "#F5FFFA", name: "Mint cream"},
|
||||||
|
{hex: "#F0FFF0", name: "Honey dew"},
|
||||||
|
{hex: "#7FFFD4", name: "Aquamarine"},
|
||||||
|
{hex: "#20B2AA", name: "Light sea green"},
|
||||||
|
{hex: "#008B8B", name: "Dark cyan"},
|
||||||
|
{hex: "#008080", name: "Teal"},
|
||||||
|
{hex: "#2F4F4F", name: "Dark slate gray"},
|
||||||
|
{hex: "#778899", name: "Light slate gray"},
|
||||||
|
{hex: "#708090", name: "Slate gray"},
|
||||||
|
{hex: "#00BFFF", name: "Deep sky blue"},
|
||||||
|
{hex: "#1E90FF", name: "Dodger Blue"},
|
||||||
|
{hex: "#87CEEB", name: "Sky blue"},
|
||||||
|
{hex: "#87CEFA", name: "Light sky blue"},
|
||||||
|
{hex: "#ADD8E6", name: "Light blue"},
|
||||||
|
{hex: "#B0E0E6", name: "Powder blue"},
|
||||||
|
{hex: "#B0C4DE", name: "Light steel blue"},
|
||||||
|
{hex: "#4682B4", name: "Steel blue"},
|
||||||
|
{hex: "#6495ED", name: "Cornflower blue"},
|
||||||
|
{hex: "#4169E1", name: "Royal blue"},
|
||||||
|
{hex: "#0000FF", name: "Blue"},
|
||||||
|
{hex: "#0000CD", name: "Medium blue"},
|
||||||
|
{hex: "#191970", name: "Midnight blue"},
|
||||||
|
{hex: "#00008B", name: "Dark blue"},
|
||||||
|
{hex: "#000080", name: "Navy"},
|
||||||
|
{hex: "#6A5ACD", name: "Slate blue"},
|
||||||
|
{hex: "#483D8B", name: "Dark slate blue"},
|
||||||
|
{hex: "#7B68EE", name: "Medium slate blue"},
|
||||||
|
{hex: "#8A2BE2", name: "Blue violet"},
|
||||||
|
{hex: "#4B0082", name: "Indigo"},
|
||||||
|
{hex: "#9400D3", name: "Dark violet"},
|
||||||
|
{hex: "#9932CC", name: "Dark orchid"},
|
||||||
|
{hex: "#BA55D3", name: "Medium orchid"},
|
||||||
|
{hex: "#DA70D6", name: "Orchid"},
|
||||||
|
{hex: "#EE82EE", name: "Violet"},
|
||||||
|
{hex: "#9370DB", name: "Medium purple"},
|
||||||
|
{hex: "#800080", name: "Purple"},
|
||||||
|
{hex: "#663399", name: "Rebecca purple"},
|
||||||
|
{hex: "#C71585", name: "Medium violet red"},
|
||||||
|
{hex: "#DB7093", name: "PaleV violet red"},
|
||||||
|
{hex: "#FF1493", name: "Deep pink"},
|
||||||
|
{hex: "#FF69B4", name: "Hot pink"},
|
||||||
|
{hex: "#FFB6C1", name: "Light pink"},
|
||||||
|
{hex: "#FFC0CB", name: "Pink"},
|
||||||
|
{hex: "#DDA0DD", name: "Plum"},
|
||||||
|
{hex: "#D8BFD8", name: "Thistle"},
|
||||||
|
{hex: "#FF00FF", name: "Magenta"},
|
||||||
|
{hex: "#FF00FF", name: "Fuchsia"},
|
||||||
|
{hex: "#8B008B", name: "Dark magenta"},
|
||||||
|
|
||||||
|
{hex: "#FAF0E6", name: "Linen"},
|
||||||
|
{hex: "#FDF5E6", name: "Old lace"},
|
||||||
|
{hex: "#FFF5EE", name: "Sea shell"},
|
||||||
|
{hex: "#FFFFF0", name: "Ivory"},
|
||||||
|
{hex: "#FFFAF0", name: "Floral white"},
|
||||||
|
{hex: "#FFF0F5", name: "Lavender blush"},
|
||||||
|
{hex: "#F8F8FF", name: "Ghost white"},
|
||||||
|
{hex: "#F0F8FF", name: "Alice blue"},
|
||||||
|
{hex: "#E6E6FA", name: "Lavender"},
|
||||||
|
{hex: "#F5F5F5", name: "White smoke"},
|
||||||
|
{hex: "#DCDCDC", name: "Gainsboro"},
|
||||||
|
];
|
||||||
|
|
||||||
|
const palettes = {
|
||||||
|
basic: [
|
||||||
|
{hex: "#FF0000", name: "Red"},
|
||||||
|
{hex: "#FF8000", name: "Orange"},
|
||||||
|
{hex: "#FFFF00", name: "Yellow"},
|
||||||
|
{hex: "#80FF00", name: "Lime"},
|
||||||
|
{hex: "#00FF00", name: "Green"},
|
||||||
|
{hex: "#00FFFF", name: "Cyan"},
|
||||||
|
{hex: "#0000FF", name: "Blue"},
|
||||||
|
{hex: "#8000FF", name: "Purple"},
|
||||||
|
{hex: "#FF00FF", name: "Magenta"},
|
||||||
|
{hex: "#FF8080", name: "Pink"},
|
||||||
|
{hex: "#804000", name: "Brown"},
|
||||||
|
{hex: "#000000", name: "Black"},
|
||||||
|
{hex: "#191919", name: "%90"},
|
||||||
|
{hex: "#333333", name: "%80"},
|
||||||
|
{hex: "#4c4c4c", name: "%70"},
|
||||||
|
{hex: "#666666", name: "%60"},
|
||||||
|
{hex: "#808080", name: "%50"},
|
||||||
|
{hex: "#999999", name: "%40"},
|
||||||
|
{hex: "#b3b3b3", name: "%30"},
|
||||||
|
{hex: "#cccccc", name: "%20"},
|
||||||
|
{hex: "#e6e6e6", name: "%10"},
|
||||||
|
{hex: "#FFFFFF", name: "White"},
|
||||||
|
...cssColors
|
||||||
|
],
|
||||||
|
orca: [
|
||||||
|
{hex: "#ED1C24", name: "Coral fire"},
|
||||||
|
{hex: "#C41E3A", name: "Sea anemone"},
|
||||||
|
{hex: "#800000", name: "Deep abyss"},
|
||||||
|
{hex: "#B22222", name: "Lobster red"},
|
||||||
|
{hex: "#A0522D", name: "Sandy clay"},
|
||||||
|
{hex: "#8B4513", name: "Driftwood"},
|
||||||
|
{hex: "#D2691E", name: "Sea buckthorn"},
|
||||||
|
{hex: "#F26722", name: "Sunset reef"},
|
||||||
|
{hex: "#FF8C00", name: "Tiger coral"},
|
||||||
|
{hex: "#FA8173", name: "Pink coral"},
|
||||||
|
{hex: "#FFA07A", name: "Seashell glow"},
|
||||||
|
{hex: "#F4E2C1", name: "Sand"},
|
||||||
|
{hex: "#FFD700", name: "Golden kelp"},
|
||||||
|
{hex: "#F7B763", name: "Sunlit dune"},
|
||||||
|
{hex: "#FFAA33", name: "Amber nautilus"},
|
||||||
|
{hex: "#FFEB31", name: "Sea lemon"},
|
||||||
|
{hex: "#D4ED91", name: "Pale starfish"},
|
||||||
|
{hex: "#00FF7F", name: "Spring tide"},
|
||||||
|
{hex: "#32CD32", name: "Lime algae"},
|
||||||
|
{hex: "#A4C41E", name: "Sea grass"},
|
||||||
|
{hex: "#7CFC00", name: "Neon plankton"},
|
||||||
|
{hex: "#228B22", name: "Kelp forest"},
|
||||||
|
{hex: "#008000", name: "Emerald wave"},
|
||||||
|
{hex: "#345B2F", name: "Whale shadow"},
|
||||||
|
{hex: "#2E8B57", name: "Sea turtle"},
|
||||||
|
{hex: "#00C1AE", name: "Lagoon"},
|
||||||
|
{hex: "#20B2AA", name: "Tropical current"},
|
||||||
|
{hex: "#115877", name: "Midnight trench"},
|
||||||
|
{hex: "#008B8B", name: "Dark kelp"},
|
||||||
|
{hex: "#00FFFF", name: "Crystal surf"},
|
||||||
|
{hex: "#40E0D0", name: "Turquoise bay"},
|
||||||
|
{hex: "#2EBDEF", name: "Sky over reef"},
|
||||||
|
{hex: "#87CEEB", name: "Shallow sky"},
|
||||||
|
{hex: "#1E90FF", name: "Electric bel"},
|
||||||
|
{hex: "#4169E1", name: "Dolphin"},
|
||||||
|
{hex: "#0000CD", name: "Ocean depths"},
|
||||||
|
{hex: "#191970", name: "Whale"},
|
||||||
|
{hex: "#4B0082", name: "Indigo abyss"},
|
||||||
|
{hex: "#7841CE", name: "Sea urchin"},
|
||||||
|
{hex: "#9370DB", name: "Lavender coral"},
|
||||||
|
{hex: "#800080", name: "Violet tide"},
|
||||||
|
{hex: "#8B008B", name: "Deep sea orchid"},
|
||||||
|
{hex: "#FF00FF", name: "Neon jelly"},
|
||||||
|
{hex: "#ED1E79", name: "Fuchsia coral"},
|
||||||
|
{hex: "#FF69B4", name: "Pink jellyfish"},
|
||||||
|
{hex: "#FFFFFF", name: "Foam"},
|
||||||
|
{hex: "#D3D3D3", name: "Seashell white"},
|
||||||
|
{hex: "#808080", name: "Pebble gray"},
|
||||||
|
{hex: "#696969", name: "Storm cloud"},
|
||||||
|
{hex: "#000000", name: "Ink black"}
|
||||||
|
],
|
||||||
|
ral: [
|
||||||
|
{hex: "#716F52", name: "1000 Green beige"},
|
||||||
|
{hex: "#9B8E79", name: "1001 Beige"},
|
||||||
|
{hex: "#D4BB87", name: "1002 Sand yellow"},
|
||||||
|
{hex: "#F4A460", name: "1003 Signal yellow"},
|
||||||
|
{hex: "#EFBF04", name: "1004 Golden yellow"},
|
||||||
|
{hex: "#D7A500", name: "1005 Honey yellow"},
|
||||||
|
{hex: "#F2D185", name: "1006 Maize yellow"},
|
||||||
|
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
|
||||||
|
{hex: "#8B7D6B", name: "1011 Brown beige"},
|
||||||
|
{hex: "#F5DB50", name: "1012 Lemon yellow"},
|
||||||
|
{hex: "#F3E5AB", name: "1013 Oyster white"},
|
||||||
|
{hex: "#F8E4BC", name: "1014 Ivory"},
|
||||||
|
{hex: "#FAE6D3", name: "1015 Light ivory"},
|
||||||
|
{hex: "#E4B922", name: "1018 Zinc yellow"},
|
||||||
|
{hex: "#A19D94", name: "1019 Grey beige"},
|
||||||
|
{hex: "#B9975B", name: "1020 Olive yellow"},
|
||||||
|
{hex: "#B8860B", name: "1021 Rape yellow"},
|
||||||
|
{hex: "#FF8C00", name: "1023 Traffic yellow"},
|
||||||
|
{hex: "#CC7722", name: "1024 Ochre yellow"},
|
||||||
|
{hex: "#FDBA37", name: "1026 Traffic orange yellow"},
|
||||||
|
{hex: "#F3A55C", name: "1027 Curry"},
|
||||||
|
{hex: "#E87C33", name: "1028 Melon yellow"},
|
||||||
|
{hex: "#F2A641", name: "1032 Broom yellow"},
|
||||||
|
{hex: "#E09F39", name: "1033 Dahlia yellow"},
|
||||||
|
{hex: "#F0B76E", name: "1034 Pastel yellow"},
|
||||||
|
{hex: "#D1BCA0", name: "1035 Pearl beige"},
|
||||||
|
{hex: "#D4AF37", name: "1036 Pearl gold"},
|
||||||
|
{hex: "#F7C64F", name: "1037 Sun yellow"},
|
||||||
|
{hex: "#F28C38", name: "2000 Yellow orange"},
|
||||||
|
{hex: "#E55B2B", name: "2001 Deep orange"},
|
||||||
|
{hex: "#E34234", name: "2002 Vermilion"},
|
||||||
|
{hex: "#F27B33", name: "2003 Pearl orange"},
|
||||||
|
{hex: "#F37322", name: "2004 Pure orange"},
|
||||||
|
{hex: "#F75C03", name: "2005 Luminous orange"},
|
||||||
|
{hex: "#F46C1F", name: "2007 Luminous bright orange"},
|
||||||
|
{hex: "#E14423", name: "2008 Bright red orange"},
|
||||||
|
{hex: "#F28500", name: "2009 Traffic orange"},
|
||||||
|
{hex: "#F46A0A", name: "2010 Signal orange"},
|
||||||
|
{hex: "#E0490C", name: "2011 Deep orange"},
|
||||||
|
{hex: "#F46F42", name: "2012 Salmon orange"},
|
||||||
|
{hex: "#E6761C", name: "2013 Pearl orange"},
|
||||||
|
{hex: "#E8A18B", name: "2015 Pearl pink"},
|
||||||
|
{hex: "#B8612C", name: "2016 Copper orange"},
|
||||||
|
{hex: "#8B4513", name: "2017 Signal brown"},
|
||||||
|
{hex: "#C14A0F", name: "2018 Fir orange"},
|
||||||
|
{hex: "#BC2E1F", name: "2020 Signal red"},
|
||||||
|
{hex: "#D52713", name: "2021 Luminous red"},
|
||||||
|
{hex: "#9F261E", name: "2022 Carmine red"},
|
||||||
|
{hex: "#D53832", name: "2023 Pearl carmine red"},
|
||||||
|
{hex: "#C2341C", name: "2024 Pure red"},
|
||||||
|
{hex: "#B32D1C", name: "2025 Luminous deep red"},
|
||||||
|
{hex: "#A62320", name: "2027 Currant red"},
|
||||||
|
{hex: "#B9301E", name: "2028 Pearl ruby red"},
|
||||||
|
{hex: "#A91D2E", name: "3000 Fire red"},
|
||||||
|
{hex: "#A1262A", name: "3001 Signal red"},
|
||||||
|
{hex: "#8F2626", name: "3002 Carmine red"},
|
||||||
|
{hex: "#9B2428", name: "3003 Ruby red"},
|
||||||
|
{hex: "#872241", name: "3004 Purple red"},
|
||||||
|
{hex: "#722F37", name: "3005 Wine red"},
|
||||||
|
{hex: "#3D1818", name: "3007 Black red"},
|
||||||
|
{hex: "#8A3324", name: "3009 Oxide red"},
|
||||||
|
{hex: "#A22C29", name: "3011 Brown red"},
|
||||||
|
{hex: "#B22D20", name: "3012 Pine red"},
|
||||||
|
{hex: "#D23741", name: "3013 Tomato red"},
|
||||||
|
{hex: "#A8282E", name: "3014 Antique pink"},
|
||||||
|
{hex: "#C8283C", name: "3015 Light pink"},
|
||||||
|
{hex: "#D23F39", name: "3016 Coral red"},
|
||||||
|
{hex: "#D1425E", name: "3017 Rose"},
|
||||||
|
{hex: "#C4232F", name: "3018 Strawberry red"},
|
||||||
|
{hex: "#C8102E", name: "3020 Traffic red"},
|
||||||
|
{hex: "#E36A5A", name: "3022 Salmon pink"},
|
||||||
|
{hex: "#E63E62", name: "3024 Luminous rose"},
|
||||||
|
{hex: "#E02A27", name: "3026 Luminous bright red"},
|
||||||
|
{hex: "#B8315F", name: "3027 Raspberry red"},
|
||||||
|
{hex: "#C61C23", name: "3028 Pure red"},
|
||||||
|
{hex: "#AD2932", name: "3031 Orient red"},
|
||||||
|
{hex: "#D32F2F", name: "3032 Pearl ruby"},
|
||||||
|
{hex: "#D13C6F", name: "3033 Pearl pink"},
|
||||||
|
{hex: "#7B2B5F", name: "4001 Red purple"},
|
||||||
|
{hex: "#912E8C", name: "4002 Red violet"},
|
||||||
|
{hex: "#9C366F", name: "4003 Heather violet"},
|
||||||
|
{hex: "#6A1F49", name: "4004 Bordeaux violet"},
|
||||||
|
{hex: "#4B2663", name: "4005 Blue violet"},
|
||||||
|
{hex: "#5C2D3C", name: "4006 Mahogany"},
|
||||||
|
{hex: "#5E3559", name: "4007 Grey purple"},
|
||||||
|
{hex: "#9C3568", name: "4008 Signal violet"},
|
||||||
|
{hex: "#5E6E3A", name: "4009 Fir green"},
|
||||||
|
{hex: "#5B5B8A", name: "4010 Sabine grey"},
|
||||||
|
{hex: "#8F3A70", name: "4011 Pearl violet"},
|
||||||
|
{hex: "#5E3252", name: "4012 Pearl blackberry"},
|
||||||
|
{hex: "#1B4F72", name: "5001 Green blue"},
|
||||||
|
{hex: "#18669E", name: "5002 Ultramarine blue"},
|
||||||
|
{hex: "#0F4C8A", name: "5003 Sapphire blue"},
|
||||||
|
{hex: "#0D324A", name: "5004 Black blue"},
|
||||||
|
{hex: "#1B5583", name: "5005 Signal blue"},
|
||||||
|
{hex: "#2C5F9D", name: "5007 Brilliant blue"},
|
||||||
|
{hex: "#3B5AA9", name: "5008 Grey blue"},
|
||||||
|
{hex: "#0077A3", name: "5009 Azure blue"},
|
||||||
|
{hex: "#0A558C", name: "5010 Gentian blue"},
|
||||||
|
{hex: "#2B547E", name: "5011 Steel blue"},
|
||||||
|
{hex: "#0066A1", name: "5012 Light blue"},
|
||||||
|
{hex: "#124780", name: "5013 Cobalt blue"},
|
||||||
|
{hex: "#1F4E79", name: "5014 Pigeon blue"},
|
||||||
|
{hex: "#0384A7", name: "5015 Sky blue"},
|
||||||
|
{hex: "#006A9E", name: "5017 Traffic blue"},
|
||||||
|
{hex: "#005F87", name: "5018 Turquoise blue"},
|
||||||
|
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
|
||||||
|
{hex: "#04749F", name: "5020 Ocean blue"},
|
||||||
|
{hex: "#009AAA", name: "5021 Water blue"},
|
||||||
|
{hex: "#1A3D5F", name: "5022 Night blue"},
|
||||||
|
{hex: "#047AA6", name: "5023 Distant blue"},
|
||||||
|
{hex: "#5B9BB8", name: "5024 Pastel blue"},
|
||||||
|
{hex: "#3C6A98", name: "5025 Pearl gentian blue"},
|
||||||
|
{hex: "#1C3F5F", name: "5026 Pearl midnight blue"},
|
||||||
|
{hex: "#36673A", name: "6000 Patina green"},
|
||||||
|
{hex: "#0E5C34", name: "6001 Emerald green"},
|
||||||
|
{hex: "#437034", name: "6002 Leaf green"},
|
||||||
|
{hex: "#3F3F2A", name: "6003 Olive green"},
|
||||||
|
{hex: "#2B4E52", name: "6004 Blue green"},
|
||||||
|
{hex: "#3B5F3F", name: "6005 Moss green"},
|
||||||
|
{hex: "#4A563C", name: "6006 Grey olive"},
|
||||||
|
{hex: "#2B4638", name: "6007 Pine green"},
|
||||||
|
{hex: "#4A4139", name: "6008 Pine brown"},
|
||||||
|
{hex: "#3B3A2E", name: "6009 Fir green"},
|
||||||
|
{hex: "#3E7A5F", name: "6010 Grass green"},
|
||||||
|
{hex: "#5E716A", name: "6011 Reseda green"},
|
||||||
|
{hex: "#3A4E48", name: "6012 Brown green"},
|
||||||
|
{hex: "#6B8E23", name: "6013 Reed green"},
|
||||||
|
{hex: "#6B8E23", name: "6014 Yellow green"},
|
||||||
|
{hex: "#1E3E2F", name: "6015 Black green"},
|
||||||
|
{hex: "#1E4D4B", name: "6016 Turquoise green"},
|
||||||
|
{hex: "#4C7C47", name: "6017 May green"},
|
||||||
|
{hex: "#9CBF60", name: "6018 Yellow green"},
|
||||||
|
{hex: "#2E5A3A", name: "6019 Pine green"},
|
||||||
|
{hex: "#2E5A3A", name: "6020 Chrome oxide green"},
|
||||||
|
{hex: "#9DC0A5", name: "6021 Pale green"},
|
||||||
|
{hex: "#4A563C", name: "6022 Brown green"},
|
||||||
|
{hex: "#009E60", name: "6024 Traffic green"},
|
||||||
|
{hex: "#3F7044", name: "6025 Fern green"},
|
||||||
|
{hex: "#4F7942", name: "6026 Opal green"},
|
||||||
|
{hex: "#3B5F3F", name: "6027 Pine green"},
|
||||||
|
{hex: "#3B5F3F", name: "6028 Pine green"},
|
||||||
|
{hex: "#00A550", name: "6029 Mint green"},
|
||||||
|
{hex: "#3E5A40", name: "6031 Bronze green"},
|
||||||
|
{hex: "#3D7E5A", name: "6032 Pearl green"},
|
||||||
|
{hex: "#007D6E", name: "6033 Sax turquoise"},
|
||||||
|
{hex: "#3D7E5A", name: "6034 Pearl turquoise"},
|
||||||
|
{hex: "#3D7E5A", name: "6035 Pearl green"},
|
||||||
|
{hex: "#3D7E5A", name: "6036 Pearl jade"},
|
||||||
|
{hex: "#00A550", name: "6037 Pure green"},
|
||||||
|
{hex: "#00A550", name: "6038 Luminous green"},
|
||||||
|
{hex: "#00A550", name: "6039 Mint green"},
|
||||||
|
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
|
||||||
|
{hex: "#A8A9AD", name: "7001 Silver grey"},
|
||||||
|
{hex: "#6F7368", name: "7002 Olivestone"},
|
||||||
|
{hex: "#8F9080", name: "7003 Moss grey"},
|
||||||
|
{hex: "#8E9291", name: "7004 Signal grey"},
|
||||||
|
{hex: "#8B8788", name: "7005 Mouse grey"},
|
||||||
|
{hex: "#9A8373", name: "7006 Beige grey"},
|
||||||
|
{hex: "#828C6A", name: "7008 Khaki grey"},
|
||||||
|
{hex: "#6F7368", name: "7009 Green grey"},
|
||||||
|
{hex: "#5F6563", name: "7010 Tarpaulin grey"},
|
||||||
|
{hex: "#4A5656", name: "7011 Iron grey"},
|
||||||
|
{hex: "#4D5656", name: "7012 Basalt grey"},
|
||||||
|
{hex: "#635147", name: "7013 Brown grey"},
|
||||||
|
{hex: "#6B6B83", name: "7015 Slate grey"},
|
||||||
|
{hex: "#32373D", name: "7016 Anthracite grey"},
|
||||||
|
{hex: "#8B8B83", name: "7021 Light grey"},
|
||||||
|
{hex: "#716347", name: "7022 Umber grey"},
|
||||||
|
{hex: "#76787A", name: "7023 Concrete grey"},
|
||||||
|
{hex: "#5E6565", name: "7024 Graphite grey"},
|
||||||
|
{hex: "#5E6565", name: "7026 Granite grey"},
|
||||||
|
{hex: "#8F9494", name: "7030 Stone grey"},
|
||||||
|
{hex: "#4A5656", name: "7031 Iron grey"},
|
||||||
|
{hex: "#B2BFC0", name: "7032 Pebble grey"},
|
||||||
|
{hex: "#9B9B9B", name: "7033 Cement grey"},
|
||||||
|
{hex: "#8F9D9B", name: "7034 Yellow grey"},
|
||||||
|
{hex: "#CBD0CC", name: "7035 Light grey"},
|
||||||
|
{hex: "#6D7A80", name: "7036 Platinum grey"},
|
||||||
|
{hex: "#8A9191", name: "7037 Dusty grey"},
|
||||||
|
{hex: "#7E8181", name: "7038 Agate grey"},
|
||||||
|
{hex: "#6E7D7E", name: "7039 Quartz grey"},
|
||||||
|
{hex: "#8A9191", name: "7040 Window grey"},
|
||||||
|
{hex: "#8A9191", name: "7042 Traffic grey A"},
|
||||||
|
{hex: "#6E7D7E", name: "7043 Traffic grey B"},
|
||||||
|
{hex: "#9B9B9B", name: "7044 Silk grey"},
|
||||||
|
{hex: "#A1A7B0", name: "7045 Telegrey 1"},
|
||||||
|
{hex: "#7E848C", name: "7046 Telegrey 2"},
|
||||||
|
{hex: "#7E848C", name: "7047 Telegrey 4"},
|
||||||
|
{hex: "#6D7A80", name: "7048 Pearl mouse grey"},
|
||||||
|
{hex: "#786D5F", name: "8000 Green brown"},
|
||||||
|
{hex: "#8B7355", name: "8001 Ochre brown"},
|
||||||
|
{hex: "#8B6F47", name: "8003 Clay brown"},
|
||||||
|
{hex: "#8B6F47", name: "8004 Copper brown"},
|
||||||
|
{hex: "#5C4033", name: "8007 Chocolate brown"},
|
||||||
|
{hex: "#6E5C4A", name: "8008 Olive brown"},
|
||||||
|
{hex: "#8B4513", name: "8011 Nut brown"},
|
||||||
|
{hex: "#8B4513", name: "8012 Red brown"},
|
||||||
|
{hex: "#5C3317", name: "8014 Sepia brown"},
|
||||||
|
{hex: "#854636", name: "8015 Chestnut brown"},
|
||||||
|
{hex: "#8B4513", name: "8016 Mahogany brown"},
|
||||||
|
{hex: "#5C3317", name: "8017 Chocolate brown"},
|
||||||
|
{hex: "#786D5F", name: "8019 Grey brown"},
|
||||||
|
{hex: "#854636", name: "8022 Chestnut brown"},
|
||||||
|
{hex: "#8B6F47", name: "8023 Pearl copper"},
|
||||||
|
{hex: "#8B7355", name: "8024 Beige brown"},
|
||||||
|
{hex: "#8B6F47", name: "8025 Pearl mocha brown"},
|
||||||
|
{hex: "#8B6F47", name: "8027 Clay brown"},
|
||||||
|
{hex: "#8B6F47", name: "8028 Earth brown"},
|
||||||
|
{hex: "#8B6F47", name: "8029 Pearl brown"},
|
||||||
|
{hex: "#FAF0E6", name: "9001 Cream"},
|
||||||
|
{hex: "#EDECE6", name: "9002 Grey white"},
|
||||||
|
{hex: "#FFFFFF", name: "9003 Signal white"},
|
||||||
|
{hex: "#000000", name: "9004 Signal black"},
|
||||||
|
{hex: "#1D1D1D", name: "9005 Jet black"},
|
||||||
|
{hex: "#C0C0C0", name: "9006 White aluminium"},
|
||||||
|
{hex: "#8C7853", name: "9007 Grey aluminium"},
|
||||||
|
{hex: "#FFFFFF", name: "9010 Pure white"},
|
||||||
|
{hex: "#1D1D1D", name: "9011 Graphite black"},
|
||||||
|
{hex: "#FFFFFF", name: "9016 Traffic white"},
|
||||||
|
{hex: "#000000", name: "9017 Traffic black"},
|
||||||
|
{hex: "#FAF0E6", name: "9018 Papyrus white"},
|
||||||
|
{hex: "#B2BFC0", name: "9022 Pearl light grey"}
|
||||||
|
],
|
||||||
|
gradient: [
|
||||||
|
{grad: "#FFF47B-#CD9800", name: "Metal: Gold"},
|
||||||
|
{grad: "#373737-#F5F4F4", name: "Metal: Silver"},
|
||||||
|
{grad: "#C39292-#C73725", name: "Metal: Copper"},
|
||||||
|
{grad: "#DED699-#C99620", name: "Metal: Brass"},
|
||||||
|
{grad: "#6E4316-#B0742E", name: "Metal: Bronze"},
|
||||||
|
// More than two colors: only listed when the page enables multi-color gradients.
|
||||||
|
{grad: "#FF0000-#FFFF00-#00FF00-#00FFFF-#0000FF-#FF00FF-#FF0000", name: "Nature: Rainbow"},
|
||||||
|
|
||||||
|
{grad: "#D8FEE7-#FFF5BD", name: "Nature: Pearl"},
|
||||||
|
{grad: "#1E5AA8-#3D8BD9-#7DB9F0-#C9E6FF", name: "Nature: Clear Sky"},
|
||||||
|
{grad: "#2B1B5A-#7A2E7E-#D9466E-#F57C3A-#FFC857", name: "Nature: Sunset"},
|
||||||
|
{grad: "#3B2416-#5A3A24-#45291A-#6B4630-#3B2416", name: "Wood: Dark"},
|
||||||
|
{grad: "#8B5A2B-#A8733F-#946235-#B5844F-#8B5A2B", name: "Wood: Medium"},
|
||||||
|
{grad: "#D9B98C-#E8CFA6-#D3AE7C-#EBD5B0-#D9B98C", name: "Wood: Light"},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
window.ColorPickerPalettes = palettes;
|
||||||
|
})();
|
||||||
@@ -0,0 +1,534 @@
|
|||||||
|
:root {
|
||||||
|
--thumb-size : 20px;
|
||||||
|
--field-height: 28px;
|
||||||
|
--number-width: 52px;
|
||||||
|
--preview-size: 52px;
|
||||||
|
--checkerboard: conic-gradient(var(--checker-light) 25%, var(--checker-dark) 0 50%, var(--checker-light) 0 75%, var(--checker-dark) 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
/* Report the full content width even before the native host expands its viewport.
|
||||||
|
WebKit's fit-content can clip the sidebar and retain the expanded width on close. */
|
||||||
|
width: max-content;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--orca-font, system-ui, sans-serif);
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--fg-color-text);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#selectionNotice {
|
||||||
|
width: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--fg-color-label);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Palette sidebar ---------- */
|
||||||
|
.left-panel {
|
||||||
|
flex: 0 0 160px;
|
||||||
|
width: 160px;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.left-panel.open {
|
||||||
|
display: flex;
|
||||||
|
padding-right: 0px;
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#paletteCombo {
|
||||||
|
font-size: 13px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Combo box or filter box, with the search / close button on the right. */
|
||||||
|
.palette-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 25px;
|
||||||
|
margin-right:5px;
|
||||||
|
}
|
||||||
|
.palette-toolbar > .palette-select { flex: 1 }
|
||||||
|
.palette-filter {
|
||||||
|
flex: 1;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--fg-color-text);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--main-color);
|
||||||
|
outline: none;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
.palette-filter::-webkit-search-cancel-button { -webkit-appearance: none }
|
||||||
|
.palette-filter:focus-visible{
|
||||||
|
background-color: var(--focus-bg-box);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-search-button {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
background-color: var(--button-bg-normal);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
.palette-search-button:hover {
|
||||||
|
background-color: var(--button-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-search-button .icon-search {
|
||||||
|
--icon-url: var(--icon-search);
|
||||||
|
}
|
||||||
|
.palette-search-button .icon-close {
|
||||||
|
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g style="fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round"><line x1="3.5" y1="2.5" x2="13.5" y2="12.5"/><line x1="3.5" y1="12.5" x2="13.5" y2="2.5"/></g></svg>');;
|
||||||
|
}
|
||||||
|
.palette-search-button .icon-close,
|
||||||
|
.palette-search-button[aria-pressed="true"] .icon-search { display: none }
|
||||||
|
.palette-search-button[aria-pressed="true"] .icon-close { display: block }
|
||||||
|
.palette-empty {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--fg-color-label);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 1px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 2px 1px 0;
|
||||||
|
border:1px solid transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--fg-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-item:hover {
|
||||||
|
border-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch {
|
||||||
|
flex: 0 0 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin: 0 5px 0 1px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Picker ---------- */
|
||||||
|
.right-panel {
|
||||||
|
flex: 0 0 330px;
|
||||||
|
/* WebKit uses width, rather than flex-basis, for the parent's intrinsic width. */
|
||||||
|
width: 330px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-row {
|
||||||
|
--spectrum-height: 150px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
height: var(--spectrum-height);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spectrum {
|
||||||
|
position: absolute; /* Absolute so the canvas's pixel size never feeds back into the flex layout */
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pickerEditors:disabled #spectrum { pointer-events: none }
|
||||||
|
|
||||||
|
#colorIndicator {
|
||||||
|
position: absolute;
|
||||||
|
width: var(--thumb-size);
|
||||||
|
height: var(--thumb-size);
|
||||||
|
border: 2px solid var(--color-marker-outline);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saturation-slider {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 var(--thumb-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#satSlider {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
flex: none;
|
||||||
|
width: var(--spectrum-height);
|
||||||
|
transform: translate(-50%, -50%) rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueSlider {
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueSlider::-webkit-slider-runnable-track {
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--track-gradient);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueSlider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: var(--thumb-size);
|
||||||
|
height: var(--thumb-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--color-marker-outline);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||||
|
background: var(--thumb-color);
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left column: mode switch, hex + alpha, gradient endpoints. Right: H S L and R G B columns. */
|
||||||
|
.editor-grid {
|
||||||
|
--editor-grid-gap: 5px;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--editor-grid-gap);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-column:first-of-type {flex:1}
|
||||||
|
.editor-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(3, var(--field-height));
|
||||||
|
gap: var(--editor-grid-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--editor-grid-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-check {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.switch-label {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 18px;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.switch-slider {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--bg-color-alt);
|
||||||
|
border: 2px solid var(--bg-color-alt);
|
||||||
|
height:26px;
|
||||||
|
}
|
||||||
|
.switch-slider:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0%;
|
||||||
|
border-radius:20px;
|
||||||
|
background: var(--main-color);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.switch-check:checked + .switch-slider:before {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.switch-check:checked + .switch-slider div:first-child,
|
||||||
|
.switch-check + .switch-slider div:last-child{
|
||||||
|
color: var(--fg-color-label);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.switch-check:checked + .switch-slider div:last-child,
|
||||||
|
.switch-check + .switch-slider div:first-child{
|
||||||
|
color: var(--button-fg-light);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* H S L / R G B / Hex / Alpha input boxes */
|
||||||
|
.inline-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 5px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
.inline-field > label {
|
||||||
|
width: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--fg-color-text);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.inline-field > input {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0 2px 0 5px;
|
||||||
|
color: var(--fg-color-text);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: var(--field-height);
|
||||||
|
appearance: textfield; /* No spinner buttons; Up/Down keys and the wheel still step by 1. */
|
||||||
|
}
|
||||||
|
.inline-field:focus-within {
|
||||||
|
border-color: var(--main-color);
|
||||||
|
background: var(--focus-bg-box);
|
||||||
|
}
|
||||||
|
.inline-field:has(input:invalid) {
|
||||||
|
border-color: var(--control-invalid);
|
||||||
|
}
|
||||||
|
.inline-field > input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-field > input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
.inline-field > input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none }
|
||||||
|
|
||||||
|
.value-field { width: var(--number-width)}
|
||||||
|
.field-hex { flex: 1 }
|
||||||
|
#alphaControls {
|
||||||
|
width: calc(var(--number-width) + 2px)
|
||||||
|
}
|
||||||
|
#alphaControls > input {
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Third row holds the gradient bar and the opacity slider in the same cell. The slider only
|
||||||
|
shows while the opacity field or the slider itself is in use, and hides the gradient bar. */
|
||||||
|
.third-row { position: relative }
|
||||||
|
.third-row > * {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.third-row.opacity-open > #opacitySliderBar { display: flex }
|
||||||
|
.third-row.opacity-open > #gradientCtrlBar { display: none }
|
||||||
|
|
||||||
|
#opacitySlider { width: 100%; }
|
||||||
|
#opacitySlider::-webkit-slider-runnable-track {
|
||||||
|
background: var(--track-gradient), var(--checkerboard); /* Transparent end shows the checkerboard through the color. */
|
||||||
|
background-size: auto, 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient colors: thumbs are evenly spaced and fixed; a click anywhere selects the nearest. */
|
||||||
|
#gradientCtrlBar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.gradient-stops {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* Kept 10px away from the gradient bar so a click near its right end can't hit − or +. */
|
||||||
|
.gradient-step-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.gradient-step-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--thumb-size);
|
||||||
|
height: var(--thumb-size);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--button-bg-normal);
|
||||||
|
color: var(--icon-color);
|
||||||
|
}
|
||||||
|
.gradient-step-button svg { display: block; flex: none }
|
||||||
|
.gradient-step-button:hover:not([aria-disabled="true"]) { background: var(--button-bg-hover) }
|
||||||
|
.gradient-step-button[aria-disabled="true"] { opacity: 0.4; cursor: default }
|
||||||
|
.gradient-track {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
top: 50%;
|
||||||
|
height: var(--thumb-size);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||||
|
background-size: auto, 8px 8px;
|
||||||
|
}
|
||||||
|
.gradient-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
border: 1px solid var(--color-marker-outline);
|
||||||
|
box-shadow: 0 0 0 1px var(--color-marker-shadow);
|
||||||
|
background: var(--thumb-color);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
/* Thumb i of n sits at 10px + (100% - 20px) * i / (n - 1), set inline as --stop. */
|
||||||
|
.gradient-thumb { left: calc(10px + (100% - 20px) * var(--stop, 0)) }
|
||||||
|
.gradient-thumb[aria-pressed="true"] {
|
||||||
|
width: var(--thumb-size);
|
||||||
|
height: var(--thumb-size);
|
||||||
|
border: 2px solid var(--color-marker-outline);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-thumb:hover {
|
||||||
|
box-shadow: 0 0 0 3px var(--color-marker-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Original / current / favorites ---------- */
|
||||||
|
.bottom-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: var(--preview-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--preview-size);
|
||||||
|
height: var(--preview-size);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview svg {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview:hover svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, minmax(0, 24px));
|
||||||
|
grid-template-rows: repeat(2, minmax(0, 24px));
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-swatch {
|
||||||
|
position: relative;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-swatch.empty {
|
||||||
|
background: var(--bg-color);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each layer is separate so transparent RGBA shows the checkerboard beneath it. */
|
||||||
|
.color-swatch, .saved-swatch, .color-preview {
|
||||||
|
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||||
|
background-size: auto, 12px 12px;
|
||||||
|
}
|
||||||
|
.color-item { width: 100%; background: transparent; text-align: left; font-family: inherit; }
|
||||||
|
|
||||||
|
/* ---------- Actions ---------- */
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
.dialog-actions button { margin: 0 }
|
||||||
|
#confirmBtn[disabled] { opacity: 0.5 }
|
||||||
|
.stretch-spacer { flex: 1 }
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--button-bg-normal);
|
||||||
|
color: var(--fg-color-text);
|
||||||
|
}
|
||||||
|
.icon-button:hover {
|
||||||
|
background: var(--button-bg-hover);
|
||||||
|
}
|
||||||
|
.icon-button[aria-expanded="true"] {
|
||||||
|
background: var(--main-color);
|
||||||
|
color: var(--button-fg-light);
|
||||||
|
}
|
||||||
|
.icon-button[aria-expanded="true"]:hover {
|
||||||
|
background: var(--main-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden { display: none !important }
|
||||||
|
.invisible { visibility: hidden }
|
||||||
|
|
||||||
|
.color-item:focus-visible, .saved-swatch:focus-visible,
|
||||||
|
select:focus-visible, button:focus-visible { outline: 2px solid var(--main-color); outline-offset: -1px; }
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<script src="../js/plugin-config-ui.js"></script>
|
<script src="../js/plugin-config-ui.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
<script src="./plugin-sort.js"></script>
|
<script src="./plugin-sort.js"></script>
|
||||||
<script src="../js/fuzzy-search.js"></script>
|
<script src="../../js/fuzzy-search.js"></script>
|
||||||
<script src="./plugin-search.js"></script>
|
<script src="./plugin-search.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body onLoad="OnInit()">
|
<body onLoad="OnInit()">
|
||||||
|
|||||||
@@ -55,6 +55,13 @@
|
|||||||
/* Speed dial icon tile treatment */
|
/* Speed dial icon tile treatment */
|
||||||
--speed-tile-bg: #efefef;
|
--speed-tile-bg: #efefef;
|
||||||
--speed-tile-border: #d8d8d8;
|
--speed-tile-border: #d8d8d8;
|
||||||
|
|
||||||
|
/* Transparency and control markers are UI surfaces, never color data. */
|
||||||
|
--checker-light: #eeeeee;
|
||||||
|
--checker-dark: #aaaaaa;
|
||||||
|
--color-marker-outline: #ffffff;
|
||||||
|
--color-marker-shadow: rgba(0, 0, 0, 0.7);
|
||||||
|
--control-invalid: var(--plugin-status-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-orca-theme="dark"] {
|
:root[data-orca-theme="dark"] {
|
||||||
@@ -93,6 +100,8 @@
|
|||||||
/* Speed dial icon tile treatment */
|
/* Speed dial icon tile treatment */
|
||||||
--speed-tile-bg: #34343b;
|
--speed-tile-bg: #34343b;
|
||||||
--speed-tile-border: #50505a;
|
--speed-tile-border: #50505a;
|
||||||
|
--checker-light: #68686f;
|
||||||
|
--checker-dark: #414148;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ function OnInit()
|
|||||||
// Orca: fallback timeout in case the C++ -> JS signal fails (e.g., WebKit issues).
|
// Orca: fallback timeout in case the C++ -> JS signal fails (e.g., WebKit issues).
|
||||||
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
|
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
|
||||||
setTimeout("JumpToTarget()",180*1000);
|
setTimeout("JumpToTarget()",180*1000);
|
||||||
|
|
||||||
|
var tSend={};
|
||||||
|
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||||
|
tSend['command']="userguide_loading_page_ready";
|
||||||
|
SendWXMessage(JSON.stringify(tSend));
|
||||||
}
|
}
|
||||||
|
|
||||||
function HandleStudio( pVal )
|
function HandleStudio( pVal )
|
||||||
|
|||||||
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
|
|||||||
|
|
||||||
m_CloudMode=bOn;
|
m_CloudMode=bOn;
|
||||||
$("body").toggleClass("cloud-mode", bOn);
|
$("body").toggleClass("cloud-mode", bOn);
|
||||||
|
|
||||||
|
if( bOn )
|
||||||
|
{
|
||||||
|
$(".BtnItem").removeClass("BtnItemSelected");
|
||||||
|
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function GotoCloud()
|
function GotoCloud()
|
||||||
{
|
{
|
||||||
$(".BtnItem").removeClass("BtnItemSelected");
|
|
||||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
|
||||||
SetCloudMode(true);
|
SetCloudMode(true);
|
||||||
SendSimpleCommand("homepage_open_cloud");
|
SendSimpleCommand("homepage_open_cloud");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,10 +336,10 @@ modules:
|
|||||||
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
|
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
|
||||||
dest: external-packages/PNG
|
dest: external-packages/PNG
|
||||||
|
|
||||||
# libjpeg-turbo 3.0.1
|
# libjpeg-turbo 3.2.0
|
||||||
- type: file
|
- type: file
|
||||||
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
|
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
|
||||||
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
|
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
|
||||||
dest: external-packages/JPEG
|
dest: external-packages/JPEG
|
||||||
|
|
||||||
# Freetype 2.12.1
|
# Freetype 2.12.1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
|
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
|
||||||
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||||
|
|
||||||
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
||||||
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
||||||
|
|||||||
+21
-15
@@ -163,6 +163,7 @@ using json = nlohmann::json;
|
|||||||
#include <X11/Xlib.h>
|
#include <X11/Xlib.h>
|
||||||
#endif
|
#endif
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SLIC3R_GUI
|
#ifdef SLIC3R_GUI
|
||||||
@@ -1156,8 +1157,10 @@ int CLI::run(int argc, char **argv)
|
|||||||
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
||||||
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
||||||
// unset preserves WebKit hardware acceleration on Device / Setup
|
// unset preserves WebKit hardware acceleration on Device / Setup
|
||||||
// Wizard / login / store. The default path still applies it on
|
// Wizard / login / store. The default path applies it only where it
|
||||||
// XWayland sessions as a conservative fallback for older WebKit.
|
// 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");
|
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).
|
// honours the first backend in the comma-separated list).
|
||||||
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
|
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
|
||||||
|
|
||||||
|
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
|
||||||
|
// Force the shared-memory transport, which keeps compositing and threaded
|
||||||
|
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
|
||||||
|
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||||
|
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
|
||||||
|
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
|
||||||
|
}
|
||||||
|
|
||||||
if (x11_opt_in) {
|
if (x11_opt_in) {
|
||||||
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
||||||
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
||||||
@@ -1204,23 +1215,18 @@ int CLI::run(int argc, char **argv)
|
|||||||
if (wayland_env && *wayland_env) {
|
if (wayland_env && *wayland_env) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||||
::setenv("GDK_BACKEND", "x11", true);
|
::setenv("GDK_BACKEND", "x11", true);
|
||||||
|
|
||||||
|
// GTK will use X11 in a Wayland session here, which is the
|
||||||
|
// one default-path case that really runs through XWayland.
|
||||||
|
// WebKit2GTK accelerated compositing could fail silently
|
||||||
|
// there on releases before 2.42, so force it off. Disabling
|
||||||
|
// it also turns off threaded scrolling, hence the gate.
|
||||||
|
if (WebView::NeedsCompositingWorkaround())
|
||||||
|
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// WebKit2GTK compositing can fail under XWayland on older
|
|
||||||
// WebKit releases. Disable it only when both DISPLAY and
|
|
||||||
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
|
|
||||||
// use). On pure X11 or native Wayland, compositing is left
|
|
||||||
// enabled so WebKit retains HW acceleration.
|
|
||||||
{
|
|
||||||
const char* display_env_wk = ::getenv("DISPLAY");
|
|
||||||
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
|
|
||||||
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
|
|
||||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// XInitThreads is needed before GStreamer may use Xlib. On
|
// XInitThreads is needed before GStreamer may use Xlib. On
|
||||||
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
||||||
// (no Xlib involved), so the call is skipped.
|
// (no Xlib involved), so the call is skipped.
|
||||||
|
|||||||
@@ -429,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 __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
|
||||||
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||||
export GALLIUM_DRIVER=zink
|
export GALLIUM_DRIVER=zink
|
||||||
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
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
|
|||||||
* Get the order constraints of the insets when printing walls per region / hole.
|
* Get the order constraints of the insets when printing walls per region / hole.
|
||||||
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
|
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
|
||||||
*
|
*
|
||||||
* Odd walls should always go after their enclosing wall polygons.
|
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
|
||||||
|
* like any other when printing inner to outer.
|
||||||
*
|
*
|
||||||
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
|
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
|
||||||
*/
|
*/
|
||||||
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
|
|||||||
continue; // not directly adjacent
|
continue; // not directly adjacent
|
||||||
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
|
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
|
||||||
continue; // points are too far away from each other
|
continue; // points are too far away from each other
|
||||||
if (here->is_odd || nearby->is_odd) {
|
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
|
||||||
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
|
|
||||||
order_requirements.emplace(std::make_pair(nearby, here));
|
|
||||||
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
|
|
||||||
order_requirements.emplace(std::make_pair(here, nearby));
|
|
||||||
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
|
|
||||||
order_requirements.emplace(std::make_pair(nearby, here));
|
order_requirements.emplace(std::make_pair(nearby, here));
|
||||||
} else {
|
} else {
|
||||||
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
|
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
|
||||||
|
|||||||
+37
-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 OozePrevention::pre_toolchange(GCode& gcodegen)
|
||||||
{
|
{
|
||||||
std::string gcode;
|
|
||||||
|
|
||||||
unsigned int extruder_id = gcodegen.writer().filament()->id();
|
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) {
|
// Check if this tool will ever be used again in this print.
|
||||||
// There is no idle temperature defined in filament settings.
|
bool is_last_use = false;
|
||||||
// Use the delta value from print config.
|
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
|
||||||
if (gcodegen.config().standby_temperature_delta.value != 0) {
|
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
|
||||||
// we assume that heating is always slower than cooling, so no need to block
|
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
|
||||||
gcode += gcodegen.writer().set_temperature
|
const auto& cfg = gcodegen.config();
|
||||||
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
|
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
|
||||||
gcode.pop_back();
|
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
|
||||||
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
|
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)
|
||||||
} else {
|
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3976,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||||||
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
||||||
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
||||||
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||||
|
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
|
||||||
|
// The running extruded_* totals are left alone; profiles use them as numbers.
|
||||||
|
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||||
|
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||||
|
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||||
|
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
|
||||||
|
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
|
||||||
|
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
|
||||||
|
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
|
||||||
|
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
|
||||||
|
|
||||||
// Sync variant-mapped params into placeholder_parser before processing start gcode
|
// Sync variant-mapped params into placeholder_parser before processing start gcode
|
||||||
update_placeholder_parser_with_variant_params();
|
update_placeholder_parser_with_variant_params();
|
||||||
|
|||||||
@@ -1004,6 +1004,7 @@ protected:
|
|||||||
size_t num_objects,
|
size_t num_objects,
|
||||||
size_t num_islands);
|
size_t num_islands);
|
||||||
|
|
||||||
|
friend class OozePrevention;
|
||||||
friend class Wipe;
|
friend class Wipe;
|
||||||
friend class WipeTowerIntegration;
|
friend class WipeTowerIntegration;
|
||||||
friend class PressureEqualizer;
|
friend class PressureEqualizer;
|
||||||
|
|||||||
@@ -120,7 +120,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
|
|||||||
"@PRINT_TIME_HOUR@",
|
"@PRINT_TIME_HOUR@",
|
||||||
"@PRINT_TIME_MINUTE@",
|
"@PRINT_TIME_MINUTE@",
|
||||||
"@PRINT_TIME_SEC@",
|
"@PRINT_TIME_SEC@",
|
||||||
"@USED_FILAMENT_LENGTH@"
|
"@USED_FILAMENT_LENGTH@",
|
||||||
|
"@NORMAL_PRINT_TIME@",
|
||||||
|
"@SILENT_PRINT_TIME@",
|
||||||
|
"@TOTAL_WEIGHT@",
|
||||||
|
"@TOTAL_COST@",
|
||||||
|
"@TOTAL_WIPE_TOWER_COST@",
|
||||||
|
"@TOTAL_WIPE_TOWER_FILAMENT@"
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
||||||
@@ -147,7 +153,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
|||||||
"@PRINT_TIME_HOUR@",
|
"@PRINT_TIME_HOUR@",
|
||||||
"@PRINT_TIME_MINUTE@",
|
"@PRINT_TIME_MINUTE@",
|
||||||
"@PRINT_TIME_SEC@",
|
"@PRINT_TIME_SEC@",
|
||||||
"@USED_FILAMENT_LENGTH@"
|
"@USED_FILAMENT_LENGTH@",
|
||||||
|
"@NORMAL_PRINT_TIME@",
|
||||||
|
"@SILENT_PRINT_TIME@",
|
||||||
|
"@TOTAL_WEIGHT@",
|
||||||
|
"@TOTAL_COST@",
|
||||||
|
"@TOTAL_WIPE_TOWER_COST@",
|
||||||
|
"@TOTAL_WIPE_TOWER_FILAMENT@"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process()
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
|
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
|
||||||
auto process_inline_placeholders = [&](std::string& gcode_line) {
|
auto process_inline_placeholders = [&](std::string& gcode_line) {
|
||||||
bool processed = false;
|
bool processed = false;
|
||||||
// Every inline placeholder contains '@', so a line without one has nothing to replace.
|
// Every inline placeholder contains '@', so a line without one has nothing to replace.
|
||||||
if (gcode_line.find('@') == std::string::npos)
|
if (gcode_line.find('@') == std::string::npos)
|
||||||
return processed;
|
return processed;
|
||||||
|
|
||||||
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
|
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
|
||||||
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
|
const std::string& placeholder = reserved_tag(tag);
|
||||||
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
|
size_t pos = gcode_line.find(placeholder);
|
||||||
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
|
while (pos != std::string::npos) {
|
||||||
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
|
gcode_line.replace(pos, placeholder.length(), value);
|
||||||
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
|
processed = true;
|
||||||
|
pos = gcode_line.find(placeholder, pos + value.length());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
|
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
|
||||||
if (print_time_total_sec < 0.0)
|
if (print_time_total_sec < 0.0)
|
||||||
@@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process()
|
|||||||
int print_time_minute = (day_remainder_seconds % 3600) / 60;
|
int print_time_minute = (day_remainder_seconds % 3600) / 60;
|
||||||
int print_time_sec = day_remainder_seconds % 60;
|
int print_time_sec = day_remainder_seconds % 60;
|
||||||
|
|
||||||
// Replace print_time_total_sec
|
double total_filament_mm = 0.0;
|
||||||
size_t pos = gcode_line.find(print_time_total_placeholder);
|
for (const auto& mm : filament_mm) {
|
||||||
while (pos != std::string::npos) {
|
total_filament_mm += mm;
|
||||||
char buf[64];
|
|
||||||
sprintf(buf, "%.2f", print_time_total_sec);
|
|
||||||
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
|
|
||||||
processed = true;
|
|
||||||
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace print_time_day
|
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
|
||||||
pos = gcode_line.find(print_time_day_placeholder);
|
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
|
||||||
while (pos != std::string::npos) {
|
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
|
||||||
char buf[64];
|
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
|
||||||
sprintf(buf, "%d", print_time_day);
|
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
|
||||||
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
|
replace_tag(ETags::Used_Filament_Length_Placeholder,
|
||||||
processed = true;
|
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
|
||||||
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
|
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
|
||||||
}
|
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
|
||||||
|
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
|
||||||
// Replace print_time_hour
|
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
|
||||||
pos = gcode_line.find(print_time_hour_placeholder);
|
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
|
||||||
while (pos != std::string::npos) {
|
short_time("N/A"));
|
||||||
char buf[64];
|
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
|
||||||
sprintf(buf, "%d", print_time_hour);
|
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
|
||||||
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
|
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
|
||||||
processed = true;
|
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
|
||||||
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
|
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
|
||||||
}
|
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
|
||||||
|
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
|
||||||
// Replace print_time_minute
|
|
||||||
pos = gcode_line.find(print_time_minute_placeholder);
|
|
||||||
while (pos != std::string::npos) {
|
|
||||||
char buf[64];
|
|
||||||
sprintf(buf, "%d", print_time_minute);
|
|
||||||
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
|
|
||||||
processed = true;
|
|
||||||
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace print_time_sec
|
|
||||||
pos = gcode_line.find(print_time_sec_placeholder);
|
|
||||||
while (pos != std::string::npos) {
|
|
||||||
char buf[64];
|
|
||||||
sprintf(buf, "%d", print_time_sec);
|
|
||||||
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
|
|
||||||
processed = true;
|
|
||||||
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace used_filament_length
|
|
||||||
pos = gcode_line.find(used_filament_placeholder);
|
|
||||||
while (pos != std::string::npos) {
|
|
||||||
double total_filament_mm = 0.0;
|
|
||||||
for (const auto& mm : filament_mm) {
|
|
||||||
total_filament_mm += mm;
|
|
||||||
}
|
|
||||||
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
|
|
||||||
char buf[64];
|
|
||||||
sprintf(buf, "%.2f", used_filament_length);
|
|
||||||
gcode_line.replace(pos, used_filament_placeholder.length(), buf);
|
|
||||||
processed = true;
|
|
||||||
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf));
|
|
||||||
}
|
|
||||||
|
|
||||||
return processed;
|
return processed;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
|||||||
Print_Time_Minute_Placeholder,
|
Print_Time_Minute_Placeholder,
|
||||||
Print_Time_Sec_Placeholder,
|
Print_Time_Sec_Placeholder,
|
||||||
Used_Filament_Length_Placeholder,
|
Used_Filament_Length_Placeholder,
|
||||||
|
Normal_Print_Time_Placeholder,
|
||||||
|
Silent_Print_Time_Placeholder,
|
||||||
|
Total_Weight_Placeholder,
|
||||||
|
Total_Cost_Placeholder,
|
||||||
|
Total_Wipe_Tower_Cost_Placeholder,
|
||||||
|
Total_Wipe_Tower_Filament_Placeholder,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
|
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
|
||||||
|
|||||||
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
|
|||||||
sort_remove_duplicates(m_all_printing_extruders);
|
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()) {
|
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.
|
// 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.
|
// 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;
|
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
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ public:
|
|||||||
std::vector<int> & initial_filaments);
|
std::vector<int> & initial_filaments);
|
||||||
|
|
||||||
bool has_non_support_filament(const PrintConfig &config);
|
bool has_non_support_filament(const PrintConfig &config);
|
||||||
|
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initialize_layers(std::vector<coordf_t> &zs);
|
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
|
// 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).
|
// 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;
|
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;
|
const PrintObject* m_print_object_ptr = nullptr;
|
||||||
Print* m_print;
|
Print* m_print;
|
||||||
bool m_sorted = false;
|
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);
|
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.
|
// 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/
|
// 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);
|
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 {
|
struct ReadBufStream: public IStream {
|
||||||
const ReadBuf &rbuf_ref;
|
const ReadBuf &rbuf_ref;
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ public:
|
|||||||
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
|
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
|
||||||
unsigned short depth;
|
unsigned short depth;
|
||||||
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
|
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
|
||||||
|
// E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's.
|
||||||
bool is_thin_wall_hole = false;
|
bool is_thin_wall_hole = false;
|
||||||
|
// ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall
|
||||||
|
// hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill
|
||||||
|
// between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner.
|
||||||
|
bool is_narrow_wall_hole = false;
|
||||||
// Children contour, may be both CCW and CW oriented (outer contours or holes).
|
// Children contour, may be both CCW and CW oriented (outer contours or holes).
|
||||||
std::vector<PerimeterGeneratorLoop> children;
|
std::vector<PerimeterGeneratorLoop> children;
|
||||||
|
|
||||||
@@ -572,7 +577,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
|||||||
extrusion_coll.append(std::move(extrusion_loop));
|
extrusion_coll.append(std::move(extrusion_loop));
|
||||||
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
|
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
|
||||||
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
|
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
|
||||||
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner)
|
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
|
||||||
|
!perimeter_generator.outer_walls_first_for_brim())
|
||||||
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
|
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1690,13 +1696,24 @@ void PerimeterGenerator::process_classic()
|
|||||||
|
|
||||||
last = std::move(offsets);
|
last = std::move(offsets);
|
||||||
|
|
||||||
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
|
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
|
||||||
|
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
|
||||||
if (i == 0 && ! holes[0].empty()) {
|
if (i == 0 && ! holes[0].empty()) {
|
||||||
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
|
auto reach = [&last](double room) {
|
||||||
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
|
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
|
||||||
for (PerimeterGeneratorLoop &hole : holes[0])
|
};
|
||||||
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
|
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
|
||||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
|
return intersection_pl(Polylines{ hole.split_at_first_point() },
|
||||||
|
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
|
||||||
|
};
|
||||||
|
// Only Inner/Outer/Inner orders narrow wall holes apart.
|
||||||
|
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||||
|
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
|
||||||
|
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
|
||||||
|
for (PerimeterGeneratorLoop &hole : holes[0]) {
|
||||||
|
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
|
||||||
|
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//BBS: refer to superslicer
|
//BBS: refer to superslicer
|
||||||
@@ -1835,18 +1852,16 @@ void PerimeterGenerator::process_classic()
|
|||||||
|
|
||||||
// if brim will be printed, reverse the order of perimeters so that
|
// if brim will be printed, reverse the order of perimeters so that
|
||||||
// we continue inwards after having finished the brim
|
// we continue inwards after having finished the brim
|
||||||
// TODO: add test for perimeter order
|
|
||||||
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
|
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
|
||||||
if (is_outer_wall_first ||
|
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
|
||||||
//BBS: always print outer wall first when there indeed has brim.
|
// then the contour, then the gap fill between them, as Arachne does.
|
||||||
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
|
const PerimeterGeneratorLoops &island_loops = contours.front();
|
||||||
(this->layer_id == 0 &&
|
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
|
||||||
(this->object_config->brim_type == BrimType::btOuterOnly ||
|
island_loops.front().children.front().is_narrow_wall_hole;
|
||||||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
|
if (is_outer_wall_first || this->outer_walls_first_for_brim())
|
||||||
this->object_config->brim_width.value > 0))
|
|
||||||
entities.reverse();
|
entities.reverse();
|
||||||
// Orca: sandwich mode. Apply after 1st layer.
|
// Orca: sandwich mode. Apply after 1st layer.
|
||||||
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){
|
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
|
||||||
entities.reverse(); // reverse all entities - order them from external to internal
|
entities.reverse(); // reverse all entities - order them from external to internal
|
||||||
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
|
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
|
||||||
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
||||||
@@ -2412,11 +2427,25 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
|
|||||||
|
|
||||||
// Function to reorder perimeters starting from a given reference index
|
// Function to reorder perimeters starting from a given reference index
|
||||||
auto reorderFromReference = [&](int referenceIndex) {
|
auto reorderFromReference = [&](int referenceIndex) {
|
||||||
std::unordered_set<int> firstLevelIndices;
|
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
|
||||||
firstLevelIndices.insert(referenceIndex);
|
// by the outer walls on both sides, so those join this island and are printed before it too, holes first.
|
||||||
|
std::vector<int> outerIndices{referenceIndex};
|
||||||
// Find first level touching perimeters
|
std::vector<int> firstLevelTouchingIndices;
|
||||||
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
|
for (size_t next = 0; next < outerIndices.size(); ++next)
|
||||||
|
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
|
||||||
|
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
|
||||||
|
continue;
|
||||||
|
firstLevelTouchingIndices.push_back(idx);
|
||||||
|
if (!entities[idx].extrusion->is_odd)
|
||||||
|
continue;
|
||||||
|
for (size_t i = 0; i < entities.size(); ++i)
|
||||||
|
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
|
||||||
|
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
|
||||||
|
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
|
||||||
|
outerIndices.push_back(int(i));
|
||||||
|
}
|
||||||
|
std::stable_partition(outerIndices.begin(), outerIndices.end(),
|
||||||
|
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
|
||||||
// Bring the largest first level perimeter to the front
|
// Bring the largest first level perimeter to the front
|
||||||
// The longest first neighbour is most likely the dominant proximate perimeter
|
// The longest first neighbour is most likely the dominant proximate perimeter
|
||||||
// hence printing it immediately after the external perimeter should speed things up
|
// hence printing it immediately after the external perimeter should speed things up
|
||||||
@@ -2427,8 +2456,10 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
|
|||||||
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
|
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
|
||||||
}
|
}
|
||||||
// Insert first level perimeters into reordered list
|
// Insert first level perimeters into reordered list
|
||||||
reordered.push_back(entities[referenceIndex]);
|
for (int idx : outerIndices) {
|
||||||
includedIndices.insert(referenceIndex);
|
reordered.push_back(entities[idx]);
|
||||||
|
includedIndices.insert(idx);
|
||||||
|
}
|
||||||
|
|
||||||
for (int idx : firstLevelTouchingIndices) {
|
for (int idx : firstLevelTouchingIndices) {
|
||||||
if (includedIndices.count(idx) == 0) {
|
if (includedIndices.count(idx) == 0) {
|
||||||
@@ -2757,7 +2788,7 @@ void PerimeterGenerator::process_arachne()
|
|||||||
|
|
||||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
||||||
is_outer_wall_first =
|
is_outer_wall_first =
|
||||||
this->config->wall_sequence == WallSequence::OuterInner;
|
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
|
||||||
}
|
}
|
||||||
if (is_outer_wall_first) {
|
if (is_outer_wall_first) {
|
||||||
start_perimeter = 0;
|
start_perimeter = 0;
|
||||||
@@ -3042,6 +3073,16 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
|
|||||||
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
|
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PerimeterGenerator::outer_walls_first_for_brim() const
|
||||||
|
{
|
||||||
|
return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width)
|
||||||
|
{
|
||||||
|
return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
|
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
|
||||||
{
|
{
|
||||||
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
|
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ public:
|
|||||||
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
|
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
|
||||||
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
|
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
|
||||||
ExPolygons printable_slices(const ExPolygons &slices) const;
|
ExPolygons printable_slices(const ExPolygons &slices) const;
|
||||||
|
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
|
||||||
|
bool outer_walls_first_for_brim() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<Polygons> generate_lower_polygons_series(float width);
|
std::vector<Polygons> generate_lower_polygons_series(float width);
|
||||||
@@ -183,6 +185,10 @@ private:
|
|||||||
Polygons m_lower_slices_polygons;
|
Polygons m_lower_slices_polygons;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
|
||||||
|
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
|
||||||
|
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2293,6 +2293,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
|
|||||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
|
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
extend_default_config_length(config, false, {});
|
||||||
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||||
//preset->config.apply(std::move(config));
|
//preset->config.apply(std::move(config));
|
||||||
Preset::normalize(preset->config);
|
Preset::normalize(preset->config);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "I18N.hpp"
|
#include "I18N.hpp"
|
||||||
#include "Layer.hpp"
|
#include "Layer.hpp"
|
||||||
#include "MutablePolygon.hpp"
|
#include "MutablePolygon.hpp"
|
||||||
|
#include "PerimeterGenerator.hpp"
|
||||||
#include "PrintBase.hpp"
|
#include "PrintBase.hpp"
|
||||||
#include "PrintConfig.hpp"
|
#include "PrintConfig.hpp"
|
||||||
#include "SLA/IndexedMesh.hpp"
|
#include "SLA/IndexedMesh.hpp"
|
||||||
@@ -858,11 +859,10 @@ void PrintObject::contour_z()
|
|||||||
BOOST_LOG_TRIVIAL(debug) << "Contouring in parallel - start";
|
BOOST_LOG_TRIVIAL(debug) << "Contouring in parallel - start";
|
||||||
|
|
||||||
TriangleMesh mesh = this->m_model_object->raw_mesh();
|
TriangleMesh mesh = this->m_model_object->raw_mesh();
|
||||||
if (m_model_object->instances.size() != 1) {
|
// A PrintObject groups the instances that share their whole transformation but the XY shift: an
|
||||||
throw RuntimeError("ContourZ: unexpected number of instances");
|
// instance rotated, scaled or mirrored differently is a PrintObject of its own. So the first
|
||||||
}
|
// instance stands for all of them.
|
||||||
|
const ModelInstance *inst = m_instances.front().model_instance;
|
||||||
ModelInstance *inst = m_model_object->instances.front();
|
|
||||||
Point center_offset = this->center_offset();
|
Point center_offset = this->center_offset();
|
||||||
Geometry::Transformation trans = inst->get_transformation();
|
Geometry::Transformation trans = inst->get_transformation();
|
||||||
|
|
||||||
@@ -1465,15 +1465,13 @@ bool PrintObject::invalidate_state_by_config_options(
|
|||||||
|| opt_key == "top_surface_speed") {
|
|| opt_key == "top_surface_speed") {
|
||||||
// Brim is printed below supports, support invalidates brim and skirt.
|
// Brim is printed below supports, support invalidates brim and skirt.
|
||||||
steps.emplace_back(posSupportMaterial);
|
steps.emplace_back(posSupportMaterial);
|
||||||
if (opt_key == "brim_type") {
|
if (opt_key == "brim_type" || opt_key == "brim_width") {
|
||||||
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
|
||||||
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
auto outer_walls_first = [](const ConfigOptionResolver &config) {
|
||||||
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
|
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
|
||||||
//to make the wall order of first layer to be outer-first
|
config.option<ConfigOptionFloat>("brim_width")->value);
|
||||||
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
|
};
|
||||||
// takes part in the same first-layer wall order rule.
|
if (outer_walls_first(old_config) != outer_walls_first(new_config))
|
||||||
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
|
|
||||||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
|
|
||||||
steps.emplace_back(posPerimeters);
|
steps.emplace_back(posPerimeters);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -707,8 +707,10 @@ void tree_supports_generate_paths(
|
|||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
if (eec) {
|
if (eec) {
|
||||||
std::reverse(eec->entities.begin(), eec->entities.end());
|
if (!eec->empty()) {
|
||||||
dst.emplace_back(eec.release());
|
std::reverse(eec->entities.begin(), eec->entities.end());
|
||||||
|
dst.emplace_back(eec.release());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1825,12 +1827,7 @@ void generate_support_toolpaths(
|
|||||||
no_sort = true;
|
no_sort = true;
|
||||||
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
|
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
|
||||||
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
|
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
|
||||||
// Orca: A special case for the hollow Organic supports
|
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
|
||||||
// Orca: If the tree supports are too tall, use a double wall to make it stronger
|
|
||||||
SupportParameters support_params2 = support_params;
|
|
||||||
if (support_layer.print_z > 100.0)
|
|
||||||
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
|
|
||||||
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
|
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
if (! done)
|
if (! done)
|
||||||
|
|||||||
@@ -2149,7 +2149,9 @@ void TreeSupport::draw_circles()
|
|||||||
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
|
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
|
||||||
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
|
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
|
||||||
const float tree_brim_width = config.tree_support_brim_width.value;
|
const float tree_brim_width = config.tree_support_brim_width.value;
|
||||||
|
// Reinforce branches moving more than half a support-line width between layers.
|
||||||
|
// Square the threshold for comparison with the squared XY movement below.
|
||||||
|
const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2)));
|
||||||
// Belt floor: the first object layer is not on a flat bed — it rests on the
|
// Belt floor: the first object layer is not on a flat bed — it rests on the
|
||||||
// tilted, moving belt. So the first-object-layer adhesion features (the tree
|
// tilted, moving belt. So the first-object-layer adhesion features (the tree
|
||||||
// support brim, the hybrid first-layer base expansion) must be suppressed:
|
// support brim, the hybrid first-layer base expansion) must be suppressed:
|
||||||
@@ -2201,7 +2203,7 @@ void TreeSupport::draw_circles()
|
|||||||
size_t first_base_roof_area = 0;
|
size_t first_base_roof_area = 0;
|
||||||
bool floor_interface_as_base = false;
|
bool floor_interface_as_base = false;
|
||||||
bool has_circle_node = false;
|
bool has_circle_node = false;
|
||||||
bool need_extra_wall = false;
|
ExPolygons extra_wall_areas;
|
||||||
ExPolygons collision_sharp_tails;
|
ExPolygons collision_sharp_tails;
|
||||||
ExPolygons collision_base;
|
ExPolygons collision_base;
|
||||||
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
|
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
|
||||||
@@ -2279,7 +2281,6 @@ void TreeSupport::draw_circles()
|
|||||||
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
|
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
|
||||||
|
|
||||||
if (!area.empty()) has_circle_node = true;
|
if (!area.empty()) has_circle_node = true;
|
||||||
if (node.need_extra_wall) need_extra_wall = true;
|
|
||||||
|
|
||||||
// merge overhang to get a smoother interface surface
|
// merge overhang to get a smoother interface surface
|
||||||
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
|
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
|
||||||
@@ -2294,6 +2295,10 @@ void TreeSupport::draw_circles()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
|
||||||
|
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
|
||||||
|
append(extra_wall_areas, area);
|
||||||
|
|
||||||
if (obj_layer_nr>0 && node.distance_to_top < 0)
|
if (obj_layer_nr>0 && node.distance_to_top < 0)
|
||||||
append(roof_gap_areas, area);
|
append(roof_gap_areas, area);
|
||||||
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
|
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
|
||||||
@@ -2530,7 +2535,8 @@ void TreeSupport::draw_circles()
|
|||||||
//if (area(expoly) < SQ(scale_(1))) continue;
|
//if (area(expoly) < SQ(scale_(1))) continue;
|
||||||
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
|
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
|
||||||
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
|
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
|
||||||
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
|
area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
|
||||||
|
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
|
||||||
}
|
}
|
||||||
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
|
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
|
||||||
auto &expoly = ts_layer->roof_areas[roof_idx];
|
auto &expoly = ts_layer->roof_areas[roof_idx];
|
||||||
@@ -3433,11 +3439,6 @@ void TreeSupport::smooth_nodes()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float max_move = scale_(m_object_config->support_line_width / 2);
|
|
||||||
// if the branch is very tall, the tip also needs extra wall
|
|
||||||
float thresh_tall_branch = 100;
|
|
||||||
float thresh_dist_to_top = 30;
|
|
||||||
|
|
||||||
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
|
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
|
||||||
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
|
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
|
||||||
if (curr_layer_nodes.empty()) continue;
|
if (curr_layer_nodes.empty()) continue;
|
||||||
@@ -3447,20 +3448,17 @@ void TreeSupport::smooth_nodes()
|
|||||||
std::vector<double> radii;
|
std::vector<double> radii;
|
||||||
std::vector<SupportNode *> branch;
|
std::vector<SupportNode *> branch;
|
||||||
SupportNode * p_node = node;
|
SupportNode * p_node = node;
|
||||||
float total_height = 0;
|
|
||||||
// add a fixed head if it's not a polygon node, see STUDIO-4403
|
// add a fixed head if it's not a polygon node, see STUDIO-4403
|
||||||
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
|
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
|
||||||
if (node->child && node->child->type!=ePolygon) {
|
if (node->child && node->child->type!=ePolygon) {
|
||||||
pts.push_back(p_node->child->position);
|
pts.push_back(p_node->child->position);
|
||||||
radii.push_back(p_node->child->radius);
|
radii.push_back(p_node->child->radius);
|
||||||
branch.push_back(p_node->child);
|
branch.push_back(p_node->child);
|
||||||
total_height += p_node->child->height;
|
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
pts.push_back(p_node->position);
|
pts.push_back(p_node->position);
|
||||||
radii.push_back(p_node->radius);
|
radii.push_back(p_node->radius);
|
||||||
branch.push_back(p_node);
|
branch.push_back(p_node);
|
||||||
total_height += p_node->height;
|
|
||||||
p_node = p_node->parent;
|
p_node = p_node->parent;
|
||||||
} while (p_node && !p_node->is_processed);
|
} while (p_node && !p_node->is_processed);
|
||||||
if (pts.size() < 3) continue;
|
if (pts.size() < 3) continue;
|
||||||
@@ -3479,9 +3477,6 @@ void TreeSupport::smooth_nodes()
|
|||||||
branch[i]->radius = radii1[i];
|
branch[i]->radius = radii1[i];
|
||||||
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
|
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
|
||||||
branch[i]->is_processed = true;
|
branch[i]->is_processed = true;
|
||||||
if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) ||
|
|
||||||
(total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top))
|
|
||||||
branch[i]->need_extra_wall = true;
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
|
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3489,13 +3484,6 @@ void TreeSupport::smooth_nodes()
|
|||||||
std::swap(pts, pts1);
|
std::swap(pts, pts1);
|
||||||
std::swap(radii, radii1);
|
std::swap(radii, radii1);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// interpolate need_extra_wall in the end
|
|
||||||
for (size_t i = 1; i < branch.size() - 1; i++) {
|
|
||||||
if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall)
|
|
||||||
branch[i]->need_extra_wall = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ struct SupportNode
|
|||||||
TreeNodeType type = eCircle;
|
TreeNodeType type = eCircle;
|
||||||
bool is_corner = false;
|
bool is_corner = false;
|
||||||
bool is_processed = false;
|
bool is_processed = false;
|
||||||
bool need_extra_wall = false;
|
|
||||||
bool is_sharp_tail = false;
|
bool is_sharp_tail = false;
|
||||||
bool valid = true;
|
bool valid = true;
|
||||||
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
|
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
|
|||||||
return decode_height_texture(layer).has_color();
|
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;
|
DecodedHeightTexture result;
|
||||||
if (layer.empty())
|
if (layer.empty())
|
||||||
@@ -351,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
if (keep_decoded) {
|
||||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||||
auto &entries = g_decoded_texture_cache.entries;
|
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||||
for (auto it = entries.begin(); it != entries.end();)
|
auto &entries = g_decoded_texture_cache.entries;
|
||||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
for (auto it = entries.begin(); it != entries.end();)
|
||||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
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,
|
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||||
|
|||||||
@@ -599,8 +599,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
|
// 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
|
// 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
|
// 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 if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
|
||||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
// 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
|
// 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.
|
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||||
|
|||||||
@@ -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);
|
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
|
} // namespace Slic3r
|
||||||
|
|
||||||
#if WIN32
|
#if WIN32
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||||
#include "boost/nowide/cstdio.hpp"
|
#include "boost/nowide/cstdio.hpp"
|
||||||
|
#include "boost/nowide/convert.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "I18N.hpp"
|
#include "I18N.hpp"
|
||||||
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
|||||||
|
|
||||||
FILE *f = nullptr;
|
FILE *f = nullptr;
|
||||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
#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)
|
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||||
f = fopen64(fname, mode);
|
f = fopen64(fname, mode);
|
||||||
#else
|
#else
|
||||||
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
|
|||||||
}
|
}
|
||||||
return ret;
|
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)
|
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_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
||||||
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
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)
|
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
|
||||||
{
|
{
|
||||||
if (stat.m_is_utf8)
|
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;
|
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||||
try {
|
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) {
|
if (stat.m_is_directory) {
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
if (!boost::filesystem::exists(io_dest))
|
||||||
if (!boost::filesystem::exists(dest_path))
|
boost::filesystem::create_directories(io_dest);
|
||||||
boost::filesystem::create_directories(dest_path);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (stat.m_uncomp_size == 0) {
|
if (stat.m_uncomp_size == 0) {
|
||||||
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Replace a symlink at the destination rather than writing through it.
|
// 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(io_dest)))
|
||||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
boost::filesystem::remove(io_dest);
|
||||||
boost::filesystem::remove(dest_path);
|
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
|
||||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||||
close_zip_reader(&archive);
|
close_zip_reader(&archive);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef MINIZ_EXTENSION_HPP
|
#ifndef MINIZ_EXTENSION_HPP
|
||||||
#define MINIZ_EXTENSION_HPP
|
#define MINIZ_EXTENSION_HPP
|
||||||
|
|
||||||
|
#include <boost/filesystem/path.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <miniz.h>
|
#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);
|
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.
|
// 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);
|
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 {
|
class MZ_Archive {
|
||||||
public:
|
public:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user