mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 20:01:31 +00:00
Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4149401c53 | ||
|
|
4ee1b9c6e9 | ||
|
|
bc8d0a7858 | ||
|
|
e0651ec31d | ||
|
|
0caefcf4aa | ||
|
|
86905a8a20 | ||
|
|
d4fabbf7c8 | ||
|
|
51efb24752 | ||
|
|
cf9ec18df5 | ||
|
|
99de46b9d0 | ||
|
|
258f927a0f | ||
|
|
b8d40ed61f | ||
|
|
6aa613ccf0 | ||
|
|
54e6df0235 | ||
|
|
7599868797 | ||
|
|
81ec931990 | ||
|
|
8fd97b5120 | ||
|
|
aacc36b9d2 | ||
|
|
79ed246d26 | ||
|
|
92b5301bf6 | ||
|
|
26f5fd6251 | ||
|
|
20c157b0f4 | ||
|
|
5d73410281 | ||
|
|
b75af485ce | ||
|
|
6a7763f08d | ||
|
|
70e5b330be | ||
|
|
5e94368863 | ||
|
|
0df9b782bb | ||
|
|
d5403ccc94 | ||
|
|
32d690ecee | ||
|
|
303d7a1a07 | ||
|
|
c0e6db227d | ||
|
|
a078155890 | ||
|
|
3b6e27d725 | ||
|
|
6254ab643a | ||
|
|
9ab0178dd0 | ||
|
|
4cf16febf6 | ||
|
|
70e52a105f | ||
|
|
b525cb79e1 | ||
|
|
86ab598899 |
@@ -569,12 +569,18 @@ on the **first** call. So:
|
||||
#endif
|
||||
```
|
||||
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
|
||||
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
|
||||
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
|
||||
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
|
||||
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
|
||||
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
|
||||
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
|
||||
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
|
||||
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
|
||||
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
|
||||
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
|
||||
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
|
||||
Wayland app inherits both). It calls `XInitThreads()`
|
||||
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
|
||||
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
|
||||
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
|
||||
|
||||
**wx's own detectors**, for reference:
|
||||
|
||||
@@ -61,7 +61,8 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
|
||||
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
|
||||
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
|
||||
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces
|
||||
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
|
||||
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
|
||||
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
|
||||
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
|
||||
@@ -567,33 +568,69 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
|
||||
## WebKitGTK on Linux sessions
|
||||
|
||||
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
|
||||
branches on `GDK_BACKEND`:
|
||||
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
|
||||
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
|
||||
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
|
||||
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
|
||||
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
|
||||
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
|
||||
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
|
||||
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
|
||||
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
|
||||
`DISPLAY` is set.
|
||||
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
|
||||
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
|
||||
default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not
|
||||
identify it, because a native Wayland app inherits both. `XInitThreads()` runs only when `DISPLAY` is
|
||||
set.
|
||||
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
|
||||
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
|
||||
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
|
||||
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
|
||||
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
|
||||
|
||||
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
|
||||
(see `references/platforms.md`), not these environment variables.
|
||||
|
||||
**Pitfall.**
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
|
||||
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
|
||||
initialises; never unconditionally. Keep JS-side fallbacks so a page never
|
||||
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
||||
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
|
||||
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
|
||||
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
|
||||
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
|
||||
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
|
||||
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
|
||||
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
|
||||
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
|
||||
process, before the first WebView.
|
||||
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
|
||||
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
|
||||
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
|
||||
```cpp
|
||||
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
|
||||
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
|
||||
// Right: keep compositing, skip only the hardware DMABUF path
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
|
||||
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
|
||||
}
|
||||
```
|
||||
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
|
||||
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
|
||||
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path, in the branch where wx
|
||||
lacks EGL and Orca forces `GDK_BACKEND=x11`, non-replacing (`setenv(..., false)`, so a value the user
|
||||
already exported wins), before GTK
|
||||
initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
|
||||
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
|
||||
waits forever for a C++→JS call.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
|
||||
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
|
||||
acceleration; without the JS timeout the user is stuck when the signal is lost.
|
||||
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
|
||||
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
|
||||
so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
|
||||
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
|
||||
```cpp
|
||||
// Wrong: unconditional, kills HW acceleration everywhere
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
|
||||
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
|
||||
if (display && *display && wayland && *wayland)
|
||||
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
|
||||
// and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the
|
||||
// case that runs through XWayland
|
||||
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
|
||||
```
|
||||
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
@@ -31,6 +32,7 @@ on:
|
||||
paths:
|
||||
- 'deps/**'
|
||||
- 'deps_src/**'
|
||||
- 'cmake/**'
|
||||
- 'src/**'
|
||||
- '**/CMakeLists.txt'
|
||||
- 'version.inc'
|
||||
|
||||
+2
-1
@@ -1345,7 +1345,8 @@ set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
|
||||
set (CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/images\\\\OrcaSlicer.ico")
|
||||
set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_MUI_UNIICON "${CPACK_PACKAGE_ICON}")
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "$INSTDIR\\\\orca-slicer.exe")
|
||||
# Relative to the install dir: CPack's NSIS template prefixes $INSTDIR itself.
|
||||
set (CPACK_NSIS_INSTALLED_ICON_NAME "orca-slicer.exe")
|
||||
set (CPACK_PACKAGE_CHECKSUM SHA256)
|
||||
set (CPACK_PACKAGE_INSTALL_REGISTRY_KEY "OrcaSlicer")
|
||||
set (CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
|
||||
|
||||
@@ -86,7 +86,8 @@ The [wiki](https://www.orcaslicer.com/wiki) aims to provide a detailed explanati
|
||||
## Stable Release
|
||||
|
||||
📥 **[Download the Latest Stable Release](https://github.com/OrcaSlicer/OrcaSlicer/releases/latest)**
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Visit our GitHub Releases page for the latest stable version of OrcaSlicer, recommended for most users.
|
||||
Windows release binaries are code signed; see our [Code signing policy](#code-signing-policy).
|
||||
|
||||
## Nightly Builds
|
||||
|
||||
@@ -244,6 +245,16 @@ OrcaSlicer began in that same spirit, drawing from BambuStudio, PrusaSlicer, and
|
||||
|
||||
The OrcaSlicer logo was designed by community member [Justin Levine](https://github.com/jal-co).
|
||||
|
||||
# Code Signing Policy
|
||||
|
||||
Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
|
||||
|
||||
Windows release binaries are built by GitHub Actions from this repository and signed through the SignPath Foundation program. Every signing request is manually approved.
|
||||
|
||||
**Team roles:** [SoftFever](https://github.com/SoftFever) is the committer, reviewer and approver for all signed releases.
|
||||
|
||||
**Privacy policy:** See the [OrcaSlicer privacy policy](https://www.orcaslicer.com/privacy-policy/).
|
||||
|
||||
# License
|
||||
|
||||
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
|
||||
|
||||
+15
-1
@@ -22,7 +22,7 @@ function usage() {
|
||||
echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
|
||||
echo " -h: prints this help text"
|
||||
echo " -i: build the Orca Slicer AppImage (optional)"
|
||||
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
|
||||
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
|
||||
echo " -r: skip RAM and disk checks (low RAM compiling)"
|
||||
echo " -s: build the Orca Slicer (optional)"
|
||||
echo " -t: build tests (optional), requires -s flag"
|
||||
@@ -549,6 +549,20 @@ fi
|
||||
if [ -n "${CMAKE_CCACHE}" ] ; then
|
||||
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
|
||||
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
|
||||
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
|
||||
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
|
||||
# a checkout has just written. It is added to the sloppiness already configured, and
|
||||
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
|
||||
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
|
||||
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
|
||||
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
|
||||
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
|
||||
fi
|
||||
# Hash the includes the compiler reports instead of preprocessing every miss.
|
||||
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
|
||||
export CCACHE_DEPEND=1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
|
||||
fi
|
||||
|
||||
@@ -42,6 +42,7 @@ call :add_arg build_deps bool d deps "Download and build the dependencies, neede
|
||||
call :add_arg build_slicer bool s slicer "Build OrcaSlicer"
|
||||
call :add_arg build_tests bool "" tests "Build the unit tests"
|
||||
call :add_arg run_tests bool "" run-tests "Build the unit tests and run them"
|
||||
call :add_arg make_installer bool "" installer "Build OrcaSlicer and package it into an NSIS installer"
|
||||
call :add_arg pack_deps bool p pack "Bundle the built dependencies into a zip file"
|
||||
call :add_arg install_deps bool u install-deps "Install or update CMake, Perl and Git with WinGet"
|
||||
call :add_arg install_vs string "" install-vs "Also install Visual Studio: buildtools or ide"
|
||||
@@ -185,6 +186,9 @@ if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" (
|
||||
if "%run_tests%" == "ON" set "build_slicer=ON"
|
||||
)
|
||||
|
||||
REM The installer packages the slicer build, so it always needs one.
|
||||
if "%make_installer%" == "ON" set "build_slicer=ON"
|
||||
|
||||
REM Options like --config or -j only shape a build. Without one of the actions
|
||||
REM there is nothing for them to shape, so say so rather than resolving a whole
|
||||
REM build and reporting it took no time. The block above may have added one.
|
||||
@@ -222,6 +226,30 @@ if "%build_slicer%" == "ON" if "%install_slicer%" == "ON" if not "%slicer_target
|
||||
echo --install needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
REM cpack runs the same install, so it has the same need.
|
||||
if "%make_installer%" == "ON" if not "%slicer_target%" == "" if /I not "%slicer_target%" == "OrcaSlicer" (
|
||||
echo --installer needs the executable, but --slicer-target names "%slicer_target%".
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM cpack finds makensis on PATH or through the registry key the NSIS installer
|
||||
REM writes, so look in the same places, and before the build rather than after
|
||||
REM it. The value is read past its REG_SZ marker because the name column is
|
||||
REM localised and may hold spaces.
|
||||
if not "%make_installer%" == "ON" goto :nsis_ready
|
||||
if "%dry_run%" == "ON" goto :nsis_ready
|
||||
where makensis >nul 2>nul && goto :nsis_ready
|
||||
for %%v in (32 64) do (
|
||||
for /f "delims=" %%l in ('reg query "HKLM\SOFTWARE\NSIS" /ve /reg:%%v 2^>nul ^| findstr REG_SZ') do (
|
||||
set "nsis_line=%%l"
|
||||
set "nsis_line=!nsis_line:*REG_SZ=!"
|
||||
for /f "tokens=*" %%d in ("!nsis_line!") do if exist "%%d\makensis.exe" goto :nsis_ready
|
||||
)
|
||||
)
|
||||
echo NSIS was not found, and --installer needs it to build the installer.
|
||||
echo Install it with "winget install NSIS.NSIS", then run this again.
|
||||
exit /b 1
|
||||
:nsis_ready
|
||||
if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" (
|
||||
echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026.
|
||||
exit /b 1
|
||||
@@ -734,6 +762,14 @@ if "%build_slicer%" == "ON" (
|
||||
call :print_and_run cmake --build "%build_dir%" --target install --config %build_type%
|
||||
%error_check%
|
||||
)
|
||||
|
||||
REM cpack installs into its own staging tree, so this does not need -i.
|
||||
REM Unlike CI it skips the preset cache, whose packaging step prunes the
|
||||
REM checkout's preset JSONs.
|
||||
if "%make_installer%" == "ON" (
|
||||
call :print_and_run cpack -G NSIS -C %build_type% --config "%build_dir%\CPackConfig.cmake" -B "%build_dir%"
|
||||
%error_check%
|
||||
)
|
||||
)
|
||||
|
||||
REM Elapsed wall clock. The 1%%a-100 trick strips a leading zero, which set /A
|
||||
@@ -820,6 +856,16 @@ REM worked out the same way in either run.
|
||||
if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target%
|
||||
if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution!
|
||||
if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle!
|
||||
if "%make_installer%" == "ON" (
|
||||
REM The file name carries the version, which only CMake knows, so name
|
||||
REM the newest match, or the pattern when there is none yet.
|
||||
set "installer="
|
||||
for /f "delims=" %%f in ('dir /b /o-d "!build_full!\OrcaSlicer_Windows_Installer_*.exe" 2^>nul') do (
|
||||
if not defined installer set "installer=!build_full!\%%f"
|
||||
)
|
||||
if not defined installer set "installer=!build_full!\OrcaSlicer_Windows_Installer_*.exe"
|
||||
echo Installer !installer!
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Next
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Find the wxWidgets module based on the information provided by the Perl Alien::wxWidgets module.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(AlienWx Perl)
|
||||
libfind_package(AlienWx PerlLibs)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_FIND_COMPONENTS=${AlienWx_FIND_COMPONENTS}")
|
||||
endif()
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(AlienWx_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/AlienWx_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Text::ParseWords;
|
||||
|
||||
BEGIN {
|
||||
# CMake sets the environment variables CC and CXX to the detected C compiler.
|
||||
# There is an issue with the Perl ExtUtils::CBuilder, which does not handle whitespaces
|
||||
# in the paths correctly on Windows, so we rather drop the CMake auto-detected paths.
|
||||
delete \$ENV{CC};
|
||||
delete \$ENV{CXX};
|
||||
}
|
||||
|
||||
use Alien::wxWidgets;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# List of wxWidgets components to be used.
|
||||
my @components = split /;/, '${AlienWx_FIND_COMPONENTS}';
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$version = Alien::wxWidgets->version;
|
||||
my \$config = Alien::wxWidgets->config;
|
||||
my \$compiler = Alien::wxWidgets->compiler;
|
||||
my \$linker = Alien::wxWidgets->linker;
|
||||
my \$include_path = ' ' . Alien::wxWidgets->include_path;
|
||||
my \$defines = ' ' . Alien::wxWidgets->defines;
|
||||
my \$cflags = Alien::wxWidgets->c_flags;
|
||||
my \$linkflags = Alien::wxWidgets->link_flags;
|
||||
my \$libraries = ' ' . Alien::wxWidgets->libraries(@components);
|
||||
my \$gui_toolkit = Alien::wxWidgets->config->{toolkit};
|
||||
#my @libraries = Alien::wxWidgets->link_libraries(@components);
|
||||
#my @implib = Alien::wxWidgets->import_libraries(@components);
|
||||
#my @shrlib = Alien::wxWidgets->shared_libraries(@components);
|
||||
#my @keys = Alien::wxWidgets->library_keys; # 'gl', 'adv', ...
|
||||
#my \$library_path = Alien::wxWidgets->shared_library_path;
|
||||
#my \$key = Alien::wxWidgets->key;
|
||||
#my \$prefix = Alien::wxWidgets->prefix;
|
||||
|
||||
my \$filename = '${AlienWx_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(AlienWx_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('VERSION', \$version);
|
||||
\$include_path =~ s/ -I/ /g;
|
||||
cmake_set_var('INCLUDE_DIRS', \$include_path);
|
||||
\$libraries =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LIBRARIES', \$libraries);
|
||||
#cmake_set_var('LIBRARY_DIRS', );
|
||||
#\$defines =~ s/ -D/ /g;
|
||||
cmake_set_var('DEFINITIONS', \$defines);
|
||||
#cmake_set_var('DEFINITIONS_DEBUG', );
|
||||
cmake_set_var('CXX_FLAGS', \$cflags);
|
||||
cmake_set_var('GUI_TOOLKIT', \$gui_toolkit);
|
||||
close \$fh;
|
||||
")
|
||||
include(${AlienWx_TEMP_INCLUDE})
|
||||
file(REMOVE ${AlienWx_TEMP_INCLUDE})
|
||||
unset(AlienWx_TEMP_INCLUDE)
|
||||
|
||||
if (AlienWx_DEBUG)
|
||||
message(STATUS " AlienWx_VERSION = ${AlienWx_VERSION}")
|
||||
message(STATUS " AlienWx_INCLUDE_DIRS = ${AlienWx_INCLUDE_DIRS}")
|
||||
message(STATUS " AlienWx_LIBRARIES = ${AlienWx_LIBRARIES}")
|
||||
message(STATUS " AlienWx_LIBRARY_DIRS = ${AlienWx_LIBRARY_DIRS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS = ${AlienWx_DEFINITIONS}")
|
||||
message(STATUS " AlienWx_DEFINITIONS_DEBUG = ${AlienWx_DEFINITIONS_DEBUG}")
|
||||
message(STATUS " AlienWx_CXX_FLAGS = ${AlienWx_CXX_FLAGS}")
|
||||
message(STATUS " AlienWx_GUI_TOOLKIT = ${AlienWx_GUI_TOOLKIT}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(AlienWx
|
||||
REQUIRED_VARS AlienWx_INCLUDE_DIRS AlienWx_LIBRARIES
|
||||
# HANDLE_COMPONENTS
|
||||
VERSION_VAR AlienWx_VERSION)
|
||||
@@ -1,88 +0,0 @@
|
||||
# Find the dependencies for linking with the Perl runtime library.
|
||||
|
||||
# Check for the Perl & PerlLib modules
|
||||
include(LibFindMacros)
|
||||
libfind_package(PerlEmbed Perl)
|
||||
libfind_package(PerlEmbed PerlLibs)
|
||||
|
||||
# Execute an Alien::Wx module to find the relevant information regarding
|
||||
# the wxWidgets used by the Perl interpreter.
|
||||
# Perl specific stuff
|
||||
set(PerlEmbed_TEMP_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/PerlEmbed_TEMP_INCLUDE.txt)
|
||||
execute_process(
|
||||
COMMAND ${PERL_EXECUTABLE} -MExtUtils::Embed -e "
|
||||
# Import Perl modules.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Config;
|
||||
use Text::ParseWords;
|
||||
use ExtUtils::CppGuess;
|
||||
|
||||
# Test for a Visual Studio compiler
|
||||
my \$cpp_guess = ExtUtils::CppGuess->new;
|
||||
my \$mswin = \$^O eq 'MSWin32';
|
||||
my \$msvc = \$cpp_guess->is_msvc;
|
||||
|
||||
# Query the available data from Alien::wxWidgets.
|
||||
my \$ccflags;
|
||||
my \$ldflags;
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ccflags; ccflags; }
|
||||
{ local *STDOUT; open STDOUT, '>', \\\$ldflags; ldopts; }
|
||||
\$ccflags = ' ' . \$ccflags;
|
||||
\$ldflags = ' ' . \$ldflags;
|
||||
|
||||
my \$filename = '${PerlEmbed_TEMP_INCLUDE}';
|
||||
open(my $fh, '>', \$filename) or die \"Could not open file '\$filename' \$!\";
|
||||
|
||||
# Convert a space separated lists to CMake semicolon separated lists,
|
||||
# escape the backslashes,
|
||||
# export the resulting list to a temp file.
|
||||
sub cmake_set_var {
|
||||
my (\$varname, \$content) = @_;
|
||||
# Remove line separators.
|
||||
\$content =~ s/\\r|\\n//g;
|
||||
# Escape the path separators.
|
||||
\$content =~ s/\\\\/\\\\\\\\\\\\\\\\/g;
|
||||
my @words = shellwords(\$content);
|
||||
print \$fh \"set(PerlEmbed_\$varname \\\"\" . join(';', @words) . \"\\\")\\n\";
|
||||
}
|
||||
cmake_set_var('ARCHNAME', \$Config{archname});
|
||||
cmake_set_var('CCFLAGS', \$ccflags);
|
||||
\$ldflags =~ s/ -L/ -LIBPATH:/g if \$msvc;
|
||||
cmake_set_var('LD', \$Config{ld});
|
||||
cmake_set_var('LDFLAGS', \$ldflags);
|
||||
cmake_set_var('CCCDLFLAGS', \$Config{cccdlflags});
|
||||
cmake_set_var('LDDLFLAGS', \$Config{lddlflags});
|
||||
cmake_set_var('DLEXT', \$Config{dlext});
|
||||
close \$fh;
|
||||
")
|
||||
include(${PerlEmbed_TEMP_INCLUDE})
|
||||
file(REMOVE ${PerlEmbed_TEMP_INCLUDE})
|
||||
unset(PerlEmbed_TEMP_INCLUDE)
|
||||
|
||||
if (PerlEmbed_DEBUG)
|
||||
# First show the configuration extracted by FindPerl & FindPerlLibs:
|
||||
message(STATUS " PERL_INCLUDE_PATH = ${PERL_INCLUDE_PATH}")
|
||||
message(STATUS " PERL_LIBRARY = ${PERL_LIBRARY}")
|
||||
message(STATUS " PERL_EXECUTABLE = ${PERL_EXECUTABLE}")
|
||||
message(STATUS " PERL_SITESEARCH = ${PERL_SITESEARCH}")
|
||||
message(STATUS " PERL_SITELIB = ${PERL_SITELIB}")
|
||||
message(STATUS " PERL_VENDORARCH = ${PERL_VENDORARCH}")
|
||||
message(STATUS " PERL_VENDORLIB = ${PERL_VENDORLIB}")
|
||||
message(STATUS " PERL_ARCHLIB = ${PERL_ARCHLIB}")
|
||||
message(STATUS " PERL_PRIVLIB = ${PERL_PRIVLIB}")
|
||||
message(STATUS " PERL_EXTRA_C_FLAGS = ${PERL_EXTRA_C_FLAGS}")
|
||||
# Second show the configuration extracted by this module (FindPerlEmbed):
|
||||
message(STATUS " PerlEmbed_ARCHNAME = ${PerlEmbed_ARCHNAME}")
|
||||
message(STATUS " PerlEmbed_CCFLAGS = ${PerlEmbed_CCFLAGS}")
|
||||
message(STATUS " PerlEmbed_CCCDLFLAGS = ${PerlEmbed_CCCDLFLAGS}")
|
||||
message(STATUS " LD = ${PerlEmbed_LD}")
|
||||
message(STATUS " PerlEmbed_LDFLAGS = ${PerlEmbed_LDFLAGS}")
|
||||
message(STATUS " PerlEmbed_LDDLFLAGS = ${PerlEmbed_LDDLFLAGS}")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(PerlEmbed
|
||||
REQUIRED_VARS PerlEmbed_CCFLAGS PerlEmbed_LDFLAGS
|
||||
VERSION_VAR PERL_VERSION)
|
||||
@@ -1,265 +0,0 @@
|
||||
# Version 2.2
|
||||
# Public Domain, originally written by Lasse Kärkkäinen <tronic>
|
||||
# Maintained at https://github.com/Tronic/cmake-modules
|
||||
# Please send your improvements as pull requests on Github.
|
||||
|
||||
# Find another package and make it a dependency of the current package.
|
||||
# This also automatically forwards the "REQUIRED" argument.
|
||||
# Usage: libfind_package(<prefix> <another package> [extra args to find_package])
|
||||
macro (libfind_package PREFIX PKG)
|
||||
set(${PREFIX}_args ${PKG} ${ARGN})
|
||||
if (${PREFIX}_FIND_REQUIRED)
|
||||
set(${PREFIX}_args ${${PREFIX}_args} REQUIRED)
|
||||
endif()
|
||||
find_package(${${PREFIX}_args})
|
||||
set(${PREFIX}_DEPENDENCIES ${${PREFIX}_DEPENDENCIES};${PKG})
|
||||
unset(${PREFIX}_args)
|
||||
endmacro()
|
||||
|
||||
# A simple wrapper to make pkg-config searches a bit easier.
|
||||
# Works the same as CMake's internal pkg_check_modules but is always quiet.
|
||||
macro (libfind_pkg_check_modules)
|
||||
find_package(PkgConfig QUIET)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(${ARGN} QUIET)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Avoid useless copy&pasta by doing what most simple libraries do anyway:
|
||||
# pkg-config, find headers, find library.
|
||||
# Usage: libfind_pkg_detect(<prefix> <pkg-config args> FIND_PATH <name> [other args] FIND_LIBRARY <name> [other args])
|
||||
# E.g. libfind_pkg_detect(SDL2 sdl2 FIND_PATH SDL.h PATH_SUFFIXES SDL2 FIND_LIBRARY SDL2)
|
||||
function (libfind_pkg_detect PREFIX)
|
||||
# Parse arguments
|
||||
set(argname pkgargs)
|
||||
foreach (i ${ARGN})
|
||||
if ("${i}" STREQUAL "FIND_PATH")
|
||||
set(argname pathargs)
|
||||
elseif ("${i}" STREQUAL "FIND_LIBRARY")
|
||||
set(argname libraryargs)
|
||||
else()
|
||||
set(${argname} ${${argname}} ${i})
|
||||
endif()
|
||||
endforeach()
|
||||
if (NOT pkgargs)
|
||||
message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.")
|
||||
endif()
|
||||
# Find library
|
||||
libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs})
|
||||
if (pathargs)
|
||||
find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (libraryargs)
|
||||
find_library(${PREFIX}_LIBRARY NAMES ${libraryargs} HINTS ${${PREFIX}_PKGCONF_LIBRARY_DIRS})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Extracts a version #define from a version.h file, output stored to <PREFIX>_VERSION.
|
||||
# Usage: libfind_version_header(Foobar foobar/version.h FOOBAR_VERSION_STR)
|
||||
# Fourth argument "QUIET" may be used for silently testing different define names.
|
||||
# This function does nothing if the version variable is already defined.
|
||||
function (libfind_version_header PREFIX VERSION_H DEFINE_NAME)
|
||||
# Skip processing if we already have a version or if the include dir was not found
|
||||
if (${PREFIX}_VERSION OR NOT ${PREFIX}_INCLUDE_DIR)
|
||||
return()
|
||||
endif()
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
# Process optional arguments
|
||||
foreach(arg ${ARGN})
|
||||
if (arg STREQUAL "QUIET")
|
||||
set(quiet TRUE)
|
||||
else()
|
||||
message(AUTHOR_WARNING "Unknown argument ${arg} to libfind_version_header ignored.")
|
||||
endif()
|
||||
endforeach()
|
||||
# Read the header and parse for version number
|
||||
set(filename "${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
if (NOT EXISTS ${filename})
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
file(READ "${filename}" header)
|
||||
string(REGEX REPLACE ".*#[ \t]*define[ \t]*${DEFINE_NAME}[ \t]*\"([^\n]*)\".*" "\\1" match "${header}")
|
||||
# No regex match?
|
||||
if (match STREQUAL header)
|
||||
if (NOT quiet)
|
||||
message(AUTHOR_WARNING "Unable to find \#define ${DEFINE_NAME} \"<version>\" from ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
# Export the version string
|
||||
set(${PREFIX}_VERSION "${match}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Do the final processing once the paths have been detected.
|
||||
# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
|
||||
# all the variables, each of which contain one include directory.
|
||||
# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
|
||||
# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
|
||||
# Also handles errors in case library detection was required, etc.
|
||||
function (libfind_process PREFIX)
|
||||
# Skip processing if already processed during this configuration run
|
||||
if (${PREFIX}_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(found TRUE) # Start with the assumption that the package was found
|
||||
|
||||
# Did we find any files? Did we miss includes? These are for formatting better error messages.
|
||||
set(some_files FALSE)
|
||||
set(missing_headers FALSE)
|
||||
|
||||
# Shorthands for some variables that we need often
|
||||
set(quiet ${${PREFIX}_FIND_QUIETLY})
|
||||
set(required ${${PREFIX}_FIND_REQUIRED})
|
||||
set(exactver ${${PREFIX}_FIND_VERSION_EXACT})
|
||||
set(findver "${${PREFIX}_FIND_VERSION}")
|
||||
set(version "${${PREFIX}_VERSION}")
|
||||
|
||||
# Lists of config option names (all, includes, libs)
|
||||
unset(configopts)
|
||||
set(includeopts ${${PREFIX}_PROCESS_INCLUDES})
|
||||
set(libraryopts ${${PREFIX}_PROCESS_LIBS})
|
||||
|
||||
# Process deps to add to
|
||||
foreach (i ${PREFIX} ${${PREFIX}_DEPENDENCIES})
|
||||
if (DEFINED ${i}_INCLUDE_OPTS OR DEFINED ${i}_LIBRARY_OPTS)
|
||||
# The package seems to export option lists that we can use, woohoo!
|
||||
list(APPEND includeopts ${${i}_INCLUDE_OPTS})
|
||||
list(APPEND libraryopts ${${i}_LIBRARY_OPTS})
|
||||
else()
|
||||
# If plural forms don't exist or they equal singular forms
|
||||
if ((NOT DEFINED ${i}_INCLUDE_DIRS AND NOT DEFINED ${i}_LIBRARIES) OR
|
||||
({i}_INCLUDE_DIR STREQUAL ${i}_INCLUDE_DIRS AND ${i}_LIBRARY STREQUAL ${i}_LIBRARIES))
|
||||
# Singular forms can be used
|
||||
if (DEFINED ${i}_INCLUDE_DIR)
|
||||
list(APPEND includeopts ${i}_INCLUDE_DIR)
|
||||
endif()
|
||||
if (DEFINED ${i}_LIBRARY)
|
||||
list(APPEND libraryopts ${i}_LIBRARY)
|
||||
endif()
|
||||
else()
|
||||
# Oh no, we don't know the option names
|
||||
message(FATAL_ERROR "We couldn't determine config variable names for ${i} includes and libs. Aieeh!")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (includeopts)
|
||||
list(REMOVE_DUPLICATES includeopts)
|
||||
endif()
|
||||
|
||||
if (libraryopts)
|
||||
list(REMOVE_DUPLICATES libraryopts)
|
||||
endif()
|
||||
|
||||
string(REGEX REPLACE ".*[ ;]([^ ;]*(_INCLUDE_DIRS|_LIBRARIES))" "\\1" tmp "${includeopts} ${libraryopts}")
|
||||
if (NOT tmp STREQUAL "${includeopts} ${libraryopts}")
|
||||
message(AUTHOR_WARNING "Plural form ${tmp} found in config options of ${PREFIX}. This works as before but is now deprecated. Please only use singular forms INCLUDE_DIR and LIBRARY, and update your find scripts for LibFindMacros > 2.0 automatic dependency system (most often you can simply remove the PROCESS variables entirely).")
|
||||
endif()
|
||||
|
||||
# Include/library names separated by spaces (notice: not CMake lists)
|
||||
unset(includes)
|
||||
unset(libs)
|
||||
|
||||
# Process all includes and set found false if any are missing
|
||||
foreach (i ${includeopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND includes "${${i}}")
|
||||
else()
|
||||
set(found FALSE)
|
||||
set(missing_headers TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Process all libraries and set found false if any are missing
|
||||
foreach (i ${libraryopts})
|
||||
list(APPEND configopts ${i})
|
||||
if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND")
|
||||
list(APPEND libs "${${i}}")
|
||||
else()
|
||||
set (found FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Version checks
|
||||
if (found AND findver)
|
||||
if (NOT version)
|
||||
message(WARNING "The find module for ${PREFIX} does not provide version information, so we'll just assume that it is OK. Please fix the module or remove package version requirements to get rid of this warning.")
|
||||
elseif (version VERSION_LESS findver OR (exactver AND NOT version VERSION_EQUAL findver))
|
||||
set(found FALSE)
|
||||
set(version_unsuitable TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# If all-OK, hide all config options, export variables, print status and exit
|
||||
if (found)
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(${i})
|
||||
endforeach()
|
||||
if (NOT quiet)
|
||||
message(STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
|
||||
if (LIBFIND_DEBUG)
|
||||
message(STATUS " ${PREFIX}_DEPENDENCIES=${${PREFIX}_DEPENDENCIES}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_OPTS=${includeopts}")
|
||||
message(STATUS " ${PREFIX}_INCLUDE_DIRS=${includes}")
|
||||
message(STATUS " ${PREFIX}_LIBRARY_OPTS=${libraryopts}")
|
||||
message(STATUS " ${PREFIX}_LIBRARIES=${libs}")
|
||||
endif()
|
||||
set (${PREFIX}_INCLUDE_OPTS ${includeopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARY_OPTS ${libraryopts} PARENT_SCOPE)
|
||||
set (${PREFIX}_INCLUDE_DIRS ${includes} PARENT_SCOPE)
|
||||
set (${PREFIX}_LIBRARIES ${libs} PARENT_SCOPE)
|
||||
set (${PREFIX}_FOUND TRUE PARENT_SCOPE)
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Format messages for debug info and the type of error
|
||||
set(vars "Relevant CMake configuration variables:\n")
|
||||
foreach (i ${configopts})
|
||||
mark_as_advanced(CLEAR ${i})
|
||||
set(val ${${i}})
|
||||
if ("${val}" STREQUAL "${i}-NOTFOUND")
|
||||
set (val "<not found>")
|
||||
elseif (val AND NOT EXISTS ${val})
|
||||
set (val "${val} (does not exist)")
|
||||
else()
|
||||
set(some_files TRUE)
|
||||
endif()
|
||||
set(vars "${vars} ${i}=${val}\n")
|
||||
endforeach()
|
||||
set(vars "${vars}You may use CMake GUI, cmake -D or ccmake to modify the values. Delete CMakeCache.txt to discard all values and force full re-detection if necessary.\n")
|
||||
if (version_unsuitable)
|
||||
set(msg "${PREFIX} ${${PREFIX}_VERSION} was found but")
|
||||
if (exactver)
|
||||
set(msg "${msg} only version ${findver} is acceptable.")
|
||||
else()
|
||||
set(msg "${msg} version ${findver} is the minimum requirement.")
|
||||
endif()
|
||||
else()
|
||||
if (missing_headers)
|
||||
set(msg "We could not find development headers for ${PREFIX}. Do you have the necessary dev package installed?")
|
||||
elseif (some_files)
|
||||
set(msg "We only found some files of ${PREFIX}, not all of them. Perhaps your installation is incomplete or maybe we just didn't look in the right place?")
|
||||
if(findver)
|
||||
set(msg "${msg} This could also be caused by incompatible version (if it helps, at least ${PREFIX} ${findver} should work).")
|
||||
endif()
|
||||
else()
|
||||
set(msg "We were unable to find package ${PREFIX}.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Fatal error out if REQUIRED
|
||||
if (required)
|
||||
set(msg "REQUIRED PACKAGE NOT FOUND\n${msg} This package is REQUIRED and you need to install it or adjust CMake configuration in order to continue building ${CMAKE_PROJECT_NAME}.")
|
||||
message(FATAL_ERROR "${msg}\n${vars}")
|
||||
endif()
|
||||
# Otherwise just print a nasty warning
|
||||
if (NOT quiet)
|
||||
message(WARNING "WARNING: MISSING PACKAGE\n${msg} This package is NOT REQUIRED and you may ignore this warning but by doing so you may miss some functionality of ${CMAKE_PROJECT_NAME}. \n${vars}")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -1,89 +0,0 @@
|
||||
# Source: https://gist.github.com/sivachandran/3a0de157dccef822a230#file-bin2h-cmake
|
||||
# Added modifications to suit prusaslicer
|
||||
include(CMakeParseArguments)
|
||||
|
||||
# Function to wrap a given string into multiple lines at the given column position.
|
||||
# Parameters:
|
||||
# VARIABLE - The name of the CMake variable holding the string.
|
||||
# AT_COLUMN - The column position at which string will be wrapped.
|
||||
function(WRAP_STRING)
|
||||
set(oneValueArgs VARIABLE AT_COLUMN)
|
||||
cmake_parse_arguments(WRAP_STRING "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
string(LENGTH ${${WRAP_STRING_VARIABLE}} stringLength)
|
||||
math(EXPR offset "0")
|
||||
|
||||
while(stringLength GREATER 0)
|
||||
|
||||
if(stringLength GREATER ${WRAP_STRING_AT_COLUMN})
|
||||
math(EXPR length "${WRAP_STRING_AT_COLUMN}")
|
||||
else()
|
||||
math(EXPR length "${stringLength}")
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${${WRAP_STRING_VARIABLE}} ${offset} ${length} line)
|
||||
set(lines "${lines}\n${line}")
|
||||
|
||||
math(EXPR stringLength "${stringLength} - ${length}")
|
||||
math(EXPR offset "${offset} + ${length}")
|
||||
endwhile()
|
||||
|
||||
set(${WRAP_STRING_VARIABLE} "${lines}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Function to embed contents of a file as byte array in C/C++ header file(.h). The header file
|
||||
# will contain a byte array and integer variable holding the size of the array.
|
||||
# Parameters
|
||||
# SOURCE_FILE - The path of source file whose contents will be embedded in the header file.
|
||||
# VARIABLE_NAME - The name of the variable for the byte array. The string "_SIZE" will be append
|
||||
# to this name and will be used a variable name for size variable.
|
||||
# HEADER_FILE - The path of header file.
|
||||
# APPEND - If specified appends to the header file instead of overwriting it
|
||||
# NULL_TERMINATE - If specified a null byte(zero) will be append to the byte array. This will be
|
||||
# useful if the source file is a text file and we want to use the file contents
|
||||
# as string. But the size variable holds size of the byte array without this
|
||||
# null byte.
|
||||
# Usage:
|
||||
# bin2h(SOURCE_FILE "Logo.png" HEADER_FILE "Logo.h" VARIABLE_NAME "LOGO_PNG")
|
||||
function(BIN2H)
|
||||
set(options APPEND NULL_TERMINATE ADD_WARNING_TEXT)
|
||||
set(oneValueArgs SOURCE_FILE VARIABLE_NAME HEADER_FILE)
|
||||
cmake_parse_arguments(BIN2H "${options}" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
# reads source file contents as hex string
|
||||
file(READ ${BIN2H_SOURCE_FILE} hexString HEX)
|
||||
string(LENGTH ${hexString} hexStringLength)
|
||||
|
||||
# appends null byte if asked
|
||||
if(BIN2H_NULL_TERMINATE)
|
||||
set(hexString "${hexString}00")
|
||||
endif()
|
||||
|
||||
# wraps the hex string into multiple lines at column 32(i.e. 16 bytes per line)
|
||||
wrap_string(VARIABLE hexString AT_COLUMN 32)
|
||||
math(EXPR arraySize "${hexStringLength} / 2")
|
||||
|
||||
# adds '0x' prefix and comma suffix before and after every byte respectively
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " arrayValues ${hexString})
|
||||
# removes trailing comma
|
||||
string(REGEX REPLACE ", $" "" arrayValues ${arrayValues})
|
||||
|
||||
# converts the variable name into proper C identifier
|
||||
string(MAKE_C_IDENTIFIER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
# string(TOUPPER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME)
|
||||
|
||||
# declares byte array and the length variables
|
||||
set(arrayDefinition "const unsigned char ${BIN2H_VARIABLE_NAME}[] = { ${arrayValues} };")
|
||||
set(arraySizeDefinition "const size_t ${BIN2H_VARIABLE_NAME}_SIZE = ${arraySize};")
|
||||
set(warnTxt "")
|
||||
if (BIN2H_ADD_WARNING_TEXT)
|
||||
set(warnTxt "/* WARN: This file is auto-generated from ${BIN2H_SOURCE_FILE} */\n")
|
||||
endif ()
|
||||
|
||||
set(declarations "${warnTxt}${arrayDefinition}\n\n${arraySizeDefinition}\n\n")
|
||||
if(BIN2H_APPEND)
|
||||
file(APPEND ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
else()
|
||||
file(WRITE ${BIN2H_HEADER_FILE} "${declarations}")
|
||||
endif()
|
||||
endfunction()
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
From b32e63f75046d186d93ee7d627c978f2f7c892f3 Mon Sep 17 00:00:00 2001
|
||||
From: Steve Dower <steve.dower@python.org>
|
||||
Date: Fri, 10 Jul 2026 14:04:00 +0100
|
||||
Subject: [PATCH] [3.13] gh-153438: Update NuGet download URL (GH-153482)
|
||||
(GH-153515)
|
||||
|
||||
gh-153438: Update NuGet download URL (GH-153482)
|
||||
(cherry picked from commit 106eb532ea3b243423e62a702719e9d3c0e40c16)
|
||||
(cherry picked from commit 62eb50d65d538dda5164f9c4a097a896454a0d1c)
|
||||
|
||||
Co-authored-by: Steve Dower <steve.dower@python.org>
|
||||
Co-authored-by: Harjoth Khara <harjoth.khara@gmail.com>
|
||||
---
|
||||
Doc/using/windows.rst | 6 +++---
|
||||
.../Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst | 2 ++
|
||||
PCbuild/find_python.bat | 2 +-
|
||||
Tools/msi/get_externals.bat | 2 +-
|
||||
4 files changed, 7 insertions(+), 5 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
|
||||
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
|
||||
index 2a507675044666..4800789ca2d6cc 100644
|
||||
--- a/Doc/using/windows.rst
|
||||
+++ b/Doc/using/windows.rst
|
||||
@@ -414,9 +414,9 @@ on using nuget. What follows is a summary that is sufficient for Python
|
||||
developers.
|
||||
|
||||
The ``nuget.exe`` command line tool may be downloaded directly from
|
||||
-``https://aka.ms/nugetclidl``, for example, using curl or PowerShell. With the
|
||||
-tool, the latest version of Python for 64-bit or 32-bit machines is installed
|
||||
-using::
|
||||
+``https://dist.nuget.org/win-x86-commandline/latest/nuget.exe``, for example,
|
||||
+using curl or PowerShell. With the tool, the latest version of Python for
|
||||
+64-bit or 32-bit machines is installed using::
|
||||
|
||||
nuget.exe install python -ExcludeVersion -OutputDirectory .
|
||||
nuget.exe install pythonx86 -ExcludeVersion -OutputDirectory .
|
||||
diff --git a/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
new file mode 100644
|
||||
index 00000000000000..edab8e6ba7b259
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Build/2026-07-09-22-45-00.gh-issue-153438.Qr7N2p.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+Update Windows build and installer tooling and documentation to use the
|
||||
+current download URL for ``nuget.exe``.
|
||||
diff --git a/PCbuild/find_python.bat b/PCbuild/find_python.bat
|
||||
index 0af367a3efafad..9612a2860ef9c3 100644
|
||||
--- a/PCbuild/find_python.bat
|
||||
+++ b/PCbuild/find_python.bat
|
||||
@@ -52,7 +52,7 @@
|
||||
@set _Py_HOST_PYTHON=%HOST_PYTHON%
|
||||
@if "%_Py_HOST_PYTHON%"=="" set _Py_HOST_PYTHON=py
|
||||
@if "%_Py_NUGET%"=="" (set _Py_NUGET=%_Py_EXTERNALS_DIR%\nuget.exe)
|
||||
-@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+@if "%_Py_NUGET_URL%"=="" (set _Py_NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
@if NOT exist "%_Py_NUGET%" (
|
||||
@if not "%_Py_Quiet%"=="1" @echo Downloading nuget...
|
||||
@rem NB: Must use single quotes around NUGET here, NOT double!
|
||||
diff --git a/Tools/msi/get_externals.bat b/Tools/msi/get_externals.bat
|
||||
index f6602ce9588ff4..c7c7e9f470bc25 100644
|
||||
--- a/Tools/msi/get_externals.bat
|
||||
+++ b/Tools/msi/get_externals.bat
|
||||
@@ -6,7 +6,7 @@ set HERE=%~dp0
|
||||
if "%PCBUILD%"=="" (set PCBUILD=%HERE%..\..\PCbuild\)
|
||||
if "%EXTERNALS_DIR%"=="" (set EXTERNALS_DIR=%HERE%..\..\externals\windows-installer)
|
||||
if "%NUGET%"=="" (set NUGET=%EXTERNALS_DIR%\..\nuget.exe)
|
||||
-if "%NUGET_URL%"=="" (set NUGET_URL=https://aka.ms/nugetclidl)
|
||||
+if "%NUGET_URL%"=="" (set NUGET_URL=https://dist.nuget.org/win-x86-commandline/latest/nuget.exe)
|
||||
|
||||
set DO_FETCH=true
|
||||
set DO_CLEAN=false
|
||||
Vendored
+80
-15
@@ -8,20 +8,45 @@ set(_python_url "https://www.python.org/ftp/python/${_python_version}/Python-${_
|
||||
set(_python_sha256 "c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684")
|
||||
|
||||
|
||||
set(_patch_cmd "")
|
||||
if(WIN32)
|
||||
|
||||
# Fix python build failure on Windows if python is not available, due to wrong nuget download URL
|
||||
# See https://github.com/python/cpython/issues/153438
|
||||
# Patch from https://github.com/python/cpython/pull/153608
|
||||
# This patch has not been merged to 3.12 yet so we need to apply it manually
|
||||
#
|
||||
# Without core.autocrlf=false the patched find_python.bat comes out LF and
|
||||
# cmd.exe cannot find its goto labels.
|
||||
set(_patch_cmd git init
|
||||
&& ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose
|
||||
--ignore-space-change --whitespace=fix
|
||||
${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch)
|
||||
# Build-time Python for PCbuild's scripts, pinned because 3.15 cannot run the 3.12 scripts.
|
||||
set(DEP_PYTHON_HOST "" CACHE FILEPATH
|
||||
"Python 3.10-3.13 that runs CPython's build scripts; empty downloads a pinned one from nuget.org")
|
||||
if(DEP_PYTHON_HOST)
|
||||
execute_process(
|
||||
COMMAND "${DEP_PYTHON_HOST}" -E -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
||||
OUTPUT_VARIABLE _python_host_minor
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE _python_host_result
|
||||
)
|
||||
if(NOT _python_host_result EQUAL 0 OR _python_host_minor VERSION_LESS 3.10 OR _python_host_minor VERSION_GREATER 3.13)
|
||||
message(FATAL_ERROR "DEP_PYTHON_HOST must be Python 3.10 to 3.13, got '${_python_host_minor}' from ${DEP_PYTHON_HOST}")
|
||||
endif()
|
||||
file(TO_NATIVE_PATH "${DEP_PYTHON_HOST}" _python_host)
|
||||
else()
|
||||
set(_python_host_version "3.13.16")
|
||||
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
|
||||
set(_python_host_package pythonarm64)
|
||||
set(_python_host_sha256 bfd77aa5b0edff4a7936a3220e8b79e7c78f864ce13e8fa2c835d9946e11694a)
|
||||
else()
|
||||
set(_python_host_package pythonx86)
|
||||
set(_python_host_sha256 c65cf47664b33286dfc5f2a17303668dd59e057869815ed9b270fb3866c1dd15)
|
||||
endif()
|
||||
set(_python_host_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_host-prefix/src/dep_python3_host")
|
||||
ExternalProject_Add(dep_python3_host
|
||||
URL "https://api.nuget.org/v3-flatcontainer/${_python_host_package}/${_python_host_version}/${_python_host_package}.${_python_host_version}.nupkg"
|
||||
URL_HASH SHA256=${_python_host_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
# Renames the .nupkg to .zip so ExternalProject extracts it.
|
||||
DOWNLOAD_NAME ${_python_host_package}.${_python_host_version}.zip
|
||||
SOURCE_DIR "${_python_host_dir}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
file(TO_NATIVE_PATH "${_python_host_dir}/tools/python.exe" _python_host)
|
||||
set(_python_depends dep_python3_host)
|
||||
endif()
|
||||
|
||||
if(MSVC_VERSION EQUAL 1800)
|
||||
set(_python_platform_toolset v120)
|
||||
@@ -63,6 +88,47 @@ if(WIN32)
|
||||
# release python3xx.lib; Py_DEBUG could not load release plugin modules.
|
||||
set(_python_pcbuild_config Release)
|
||||
|
||||
# CPython's own libraries, fetched here instead of by get_externals.bat, which ignores failed
|
||||
# downloads. Must match PCbuild/get_externals.bat for _python_version.
|
||||
set(_python_externals
|
||||
# repo folder commit sha256 of the archive
|
||||
source-deps bzip2-1.0.8 05301997b2f9590f49c672cf3dfd3d3dfa7ad521 44b15bede52b32f8673a2976621a847258b041243c003e30f4d9a05214785fe5
|
||||
source-deps sqlite-3.49.1.0 cb7a3997059739fe9c63b2819aeda7c6907510ef 586fe469463f5b27b749f9eef1938baa8e8a12030341e96160a371356c2d6759
|
||||
source-deps xz-5.2.5 c6bc0c612605622aaef101a33a751f9de2ecc193 d99882a8f9fd48be0e605e27e3c077e5eaab370f6f52ebcc8ff754480ffdf0bc
|
||||
source-deps zlib-1.3.1 4dc98e1909830e2bdc2a9cc2236e3c5d5037335b 39d867c083ebc37c77cf03a442f87c0f83db006eb64343bd4a19f1c84c26f50c
|
||||
bin-deps libffi-3.4.4 94cb9a1c7feb608adf2b9f8fe2dbd6925ffbf90d 6b0f53490e0dd958708bff32df689cf2163c214147fcb8edb76a403fa6a877ca
|
||||
bin-deps openssl-bin-3.0.16.2 1174fd43d858425c345b16d631716fb98ba492ea b741fd1411a36cb899fee2c39b70a4492f4bc1c6e2301d67df32e9754118a63b
|
||||
)
|
||||
set(_python_externals_dir "${CMAKE_CURRENT_BINARY_DIR}/dep_python3_externals")
|
||||
list(LENGTH _python_externals _python_externals_len)
|
||||
math(EXPR _python_externals_last "${_python_externals_len} - 1")
|
||||
foreach(_i RANGE 0 ${_python_externals_last} 4)
|
||||
math(EXPR _j "${_i} + 1")
|
||||
math(EXPR _k "${_i} + 2")
|
||||
math(EXPR _l "${_i} + 3")
|
||||
list(GET _python_externals ${_i} _ext_repo)
|
||||
list(GET _python_externals ${_j} _ext_name)
|
||||
list(GET _python_externals ${_k} _ext_commit)
|
||||
list(GET _python_externals ${_l} _ext_sha256)
|
||||
ExternalProject_Add(dep_python3_ext_${_ext_name}
|
||||
URL "https://github.com/python/cpython-${_ext_repo}/archive/${_ext_commit}.zip"
|
||||
URL_HASH SHA256=${_ext_sha256}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
DOWNLOAD_NAME cpython-${_ext_repo}-${_ext_name}.zip
|
||||
SOURCE_DIR "${_python_externals_dir}/${_ext_name}"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
)
|
||||
list(APPEND _python_depends dep_python3_ext_${_ext_name})
|
||||
endforeach()
|
||||
file(TO_NATIVE_PATH "${_python_externals_dir}" _python_externals_native)
|
||||
|
||||
# PCbuild runs PYTHON without -E, so PYTHONHOME and PYTHONPATH are cleared.
|
||||
set(_python_env_args --unset=PYTHONHOME --unset=PYTHONPATH "PYTHON=${_python_host}"
|
||||
"EXTERNALS_DIR=${_python_externals_native}"
|
||||
"GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
|
||||
# CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the
|
||||
# 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space
|
||||
# (fatal C1002 "out of heap space") building the LTCG-optimized pythoncore.
|
||||
@@ -71,7 +137,6 @@ if(WIN32)
|
||||
# then hosts arm64 tools on its own; the amd64 MSBuild still defaults to x86, so
|
||||
# PreferredToolArchitecture pins it to x64. Scoped to this build step, so
|
||||
# CPython's own sources stay untouched.
|
||||
set(_python_env_args "GIT_CEILING_DIRECTORIES=<SOURCE_DIR>/..")
|
||||
if(CMAKE_GENERATOR_INSTANCE) # empty for non-VS generators (e.g. Ninja)
|
||||
set(_python_msbuild "${CMAKE_GENERATOR_INSTANCE}/MSBuild/Current/Bin/${_python_msbuild_host}/MSBuild.exe")
|
||||
if(EXISTS "${_python_msbuild}")
|
||||
@@ -284,12 +349,12 @@ endif()
|
||||
ExternalProject_Add(dep_python3
|
||||
URL "${_python_url}"
|
||||
URL_HASH SHA256=${_python_sha256}
|
||||
PATCH_COMMAND ${_patch_cmd}
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/python3
|
||||
BUILD_IN_SOURCE ON
|
||||
CONFIGURE_COMMAND ${_conf_cmd}
|
||||
BUILD_COMMAND ${_build_cmd}
|
||||
INSTALL_COMMAND ${_install_cmd}
|
||||
DEPENDS ${_python_depends}
|
||||
)
|
||||
|
||||
# Python depends on OpenSSL and ZLIB
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Focus mode (hiding inactive plates) — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
"Focus mode" is an entry of the canvas menu in the bottom left corner of Prepare. Turned on (eye
|
||||
lit), the 3D view hides the inactive plates and shows the current plate alone, so a project with
|
||||
many plates can be worked on one plate at a time. Turned off, every plate is shown. It is a view
|
||||
setting: it changes nothing in the model, the slice or the project file, and it does not reach
|
||||
plate thumbnails. The choice is kept in the app config as `focus_mode`, off by default.
|
||||
|
||||
Preview needs no such option, as it already draws the current plate alone and has its own plate
|
||||
toolbar. The assembly view and the Design tab do not lay objects out on plates, so the entry is
|
||||
disabled there.
|
||||
|
||||
## What is shown
|
||||
|
||||
The current plate is drawn with its bed, label and icons; the other plates are not drawn at all. The
|
||||
world axes, which Prepare draws at the corner of the first plate, move to the corner of the current
|
||||
plate, where Preview draws them.
|
||||
|
||||
An object instance is shown when its footprint, the XY bounding box of the convex hulls of its
|
||||
volumes, overlaps the current plate, or when it is selected. The test is made per instance, so the
|
||||
parts of an object are shown or hidden together, and a wipe tower follows its plate the same way.
|
||||
It is geometric rather than the plate membership `PartPlateList` keeps, because an instance stays on
|
||||
the plate it was first placed on: an object of another plate that reaches over this one, or one too
|
||||
large for any plate, would otherwise be invisible while it takes room on the current plate. A
|
||||
selected instance stays visible so that what a selection or a gizmo acts on is never hidden, for
|
||||
instance an object picked in the object list on another plate.
|
||||
|
||||
## Hiding
|
||||
|
||||
A hidden object is neither drawn nor picked. `GLCanvas3D::_update_inactive_plates_visibility()`
|
||||
runs at the start of each frame of Prepare. It clears `GLVolume::is_active` on the volumes of the
|
||||
hidden instances and deactivates their raycasters, which every render pass, the hover and rectangle
|
||||
selection, the shadow and center of mass markers already respect. It also deactivates the raycasters
|
||||
of the other plates, whose icons would otherwise still take clicks where nothing is drawn. The state
|
||||
persists between frames, so the raycasts made while handling the mouse, such as the anchor of a pan,
|
||||
see the same scene as the frame.
|
||||
|
||||
Other features clear `is_active` too: gizmos hide the objects they do not edit and show them all
|
||||
again when they close. `GLVolume::on_hidden_plate` records which volumes this update hid, so it only
|
||||
shows again what it hid itself, and it waits for a gizmo that hides objects to close before doing
|
||||
so. A volume another feature made active again is hidden again on the next frame. While Focus mode
|
||||
is off and nothing is left hidden, the update returns at once.
|
||||
|
||||
## Switching plates
|
||||
|
||||
The other plates cannot be clicked while Focus mode hides them, so a compact counterpart of the Preview
|
||||
plate toolbar takes their place: a column of plate thumbnails in the top left corner, below the top
|
||||
toolbar, shown when the project has more than one plate and no gizmo is open. The thumbnails are the
|
||||
ones the Preview toolbar shows (`PartPlate::thumbnail_data`); the canvas rebuilds its textures when
|
||||
`Plater::plate_toolbar_image_version()` changes, a counter each consumer tracks on its own so that the
|
||||
Preview toolbar's dirty flag is left alone. That counter moves after almost every edit, so the
|
||||
thumbnail of the current plate, the only one edited while the others are hidden, is drawn again each
|
||||
time, and the other plates' only once an edit to them has invalidated theirs. The current plate has an
|
||||
accent border, a hovered one the gray border of the Preview toolbar, empty plates are dimmed and the
|
||||
plate number sits in the corner; the tooltip gives the plate name. A click selects the plate and
|
||||
clears the selection, as a click on a plate row of the object list does. The column scrolls when it
|
||||
does not fit above the 3D navigator or the canvas toolbar. Its scrollbar works as the Preview
|
||||
toolbar's, with which it shares its style: it shows only while the mouse is over the column or drags
|
||||
the bar, and the mouse wheel scrolls the column either way.
|
||||
|
||||
Whatever selects a plate while the others are hidden, the camera moves its target over the new plate
|
||||
and keeps its angle, distance and height (`PartPlateList::select_plate()`), so the plate comes into
|
||||
view where the previous one was. A zoom to all plates, as after loading a project or adding a plate,
|
||||
frames the current plate instead. Turning Focus mode on does the same, so the current plate fills the view.
|
||||
+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
@@ -215,6 +215,7 @@ src/libslic3r/GCode.cpp
|
||||
src/libslic3r/GCodeWriter.cpp
|
||||
src/libslic3r/GCode/ToolOrdering.cpp
|
||||
src/libslic3r/GCode/SeamPlacer.cpp
|
||||
src/libslic3r/GCode/GCodeStatistics.cpp
|
||||
src/libslic3r/ExtrusionEntity.cpp
|
||||
src/libslic3r/Flow.cpp
|
||||
src/libslic3r/Format/AMF.cpp
|
||||
@@ -324,6 +325,7 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
|
||||
src/slic3r/GUI/CloneDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeSupport.cpp
|
||||
src/slic3r/GUI/ColorPickerDialog.cpp
|
||||
src/slic3r/GUI/ConfigWizard.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevCalib.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",
|
||||
"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",
|
||||
"sub_path": "filament/eSUN/eSUN PETG @base.json"
|
||||
@@ -1096,6 +1100,10 @@
|
||||
"name": "SUNLU Wood PLA @base",
|
||||
"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",
|
||||
"sub_path": "filament/Valment/Valment PLA @base.json"
|
||||
@@ -2260,6 +2268,10 @@
|
||||
"name": "SUNLU PETG @System",
|
||||
"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",
|
||||
"sub_path": "filament/eSUN/eSUN PETG @System.json"
|
||||
@@ -3812,6 +3824,10 @@
|
||||
"name": "SUNLU Wood PLA @System",
|
||||
"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",
|
||||
"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",
|
||||
"version": "02.04.00.06",
|
||||
"version": "02.04.00.07",
|
||||
"force_update": "0",
|
||||
"description": "RatRig configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"z_hop": [
|
||||
"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]",
|
||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
|
||||
"change_filament_gcode": "M600",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"bed_exclude_area": [
|
||||
"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",
|
||||
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
|
||||
"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="./index.js"></script>
|
||||
<script src="./plugin-sort.js"></script>
|
||||
<script src="../js/fuzzy-search.js"></script>
|
||||
<script src="../../js/fuzzy-search.js"></script>
|
||||
<script src="./plugin-search.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #efefef;
|
||||
--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"] {
|
||||
@@ -93,6 +100,8 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #34343b;
|
||||
--speed-tile-border: #50505a;
|
||||
--checker-light: #68686f;
|
||||
--checker-dark: #414148;
|
||||
}
|
||||
|
||||
/* 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).
|
||||
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
|
||||
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 )
|
||||
|
||||
@@ -506,11 +506,6 @@ body
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.BtnShortcut
|
||||
{
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.ShortcutBrandIcon
|
||||
{
|
||||
width: 20px;
|
||||
@@ -519,25 +514,6 @@ body
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ShortcutTextRow
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ShortcutMark
|
||||
{
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: auto;
|
||||
color: currentColor;
|
||||
opacity: 0.72;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/*--------------------*/
|
||||
#RightBoard
|
||||
@@ -548,6 +524,20 @@ body
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
/*---- Cloud mode: OrcaCloud is a native pane to the right of the app panel, so this page
|
||||
is reduced to its left nav column (the class is toggled by SetCloudMode in js/home.js). ----*/
|
||||
body.cloud-mode #RightBoard
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.cloud-mode #LeftBoard
|
||||
{
|
||||
/*---- 262px of column plus its 1px separator is the width the app gives the nav. ----*/
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#ContentBoard
|
||||
{
|
||||
overflow-y:auto;
|
||||
|
||||
@@ -90,16 +90,9 @@
|
||||
<div class="BtnIcon "><img class="LeftIcon" src="img/i2.png" /></div>
|
||||
<div data-i18n="recent">Recent</div>
|
||||
</div>
|
||||
<div class="BtnItem BtnShortcut" onClick="OpenUrlInLocalBrowser('https://cloud.orcaslicer.com')">
|
||||
<div class="BtnItem BtnShortcut" onClick="GotoCloud()">
|
||||
<div class="BtnIcon "><img class="ShortcutBrandIcon" src="../image/logo.png" /></div>
|
||||
<div class="ShortcutTextRow">
|
||||
<div>OrcaCloud</div>
|
||||
<svg class="ShortcutMark" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 3.5H3.5C2.95 3.5 2.5 3.95 2.5 4.5V12.5C2.5 13.05 2.95 13.5 3.5 13.5H11.5C12.05 13.5 12.5 13.05 12.5 12.5V10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.5 3.5H13.5V8.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 3.5L7 10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>OrcaCloud</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -145,8 +145,40 @@ function HandleStudio( pVal )
|
||||
}
|
||||
}
|
||||
|
||||
/*---- OrcaCloud opens as a native pane beside this page, so the shell only switches its
|
||||
own cloud-mode state and asks the app to show or hide that pane (WebViewPanel::set_cloud_mode). ----*/
|
||||
var m_CloudMode=false;
|
||||
|
||||
function SetCloudMode( bOn )
|
||||
{
|
||||
if( m_CloudMode==bOn )
|
||||
return;
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
|
||||
if( bOn )
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
}
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
function GotoMenu( strMenu )
|
||||
{
|
||||
/*---- Any board in the right region means leaving the cloud pane. ----*/
|
||||
if( m_CloudMode )
|
||||
{
|
||||
SetCloudMode(false);
|
||||
SendSimpleCommand("homepage_close_cloud");
|
||||
}
|
||||
|
||||
let MenuList=$(".BtnItem");
|
||||
let nAll=MenuList.length;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
|
||||
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
|
||||
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
|
||||
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
|
||||
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
|
||||
|
||||
@@ -22,12 +22,12 @@ REQUIRED_DEV_PACKAGES=(
|
||||
libtool
|
||||
m4
|
||||
Mesa-libGL-devel
|
||||
ninja-build
|
||||
ninja
|
||||
openssl-devel
|
||||
perl-FindBin-Real
|
||||
texinfo
|
||||
wayland-protocols-devel
|
||||
webkit2gtk4-devel
|
||||
webkitgtk3-devel
|
||||
wget
|
||||
libcurl-devel
|
||||
pkgconf
|
||||
|
||||
+37
-16
@@ -144,6 +144,7 @@
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/GCode/ToolOrdering.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -162,6 +163,7 @@ using json = nlohmann::json;
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
@@ -207,6 +209,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
|
||||
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
|
||||
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
|
||||
{CLI_EXPORT_STATISTICS_ERROR, "Failed exporting the G-code statistics."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
|
||||
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
|
||||
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
|
||||
@@ -1154,8 +1157,10 @@ int CLI::run(int argc, char **argv)
|
||||
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
||||
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
||||
// unset preserves WebKit hardware acceleration on Device / Setup
|
||||
// Wizard / login / store. The default path still applies it on
|
||||
// XWayland sessions as a conservative fallback for older WebKit.
|
||||
// Wizard / login / store. The default path applies it only where it
|
||||
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really
|
||||
// runs through XWayland), and only when the linked WebKitGTK predates
|
||||
// the fix (< 2.42), as a conservative fallback for older WebKit.
|
||||
// ------------------------------------------------------------------
|
||||
{
|
||||
const char* gdk_backend = ::getenv("GDK_BACKEND");
|
||||
@@ -1163,6 +1168,14 @@ int CLI::run(int argc, char **argv)
|
||||
// honours the first backend in the comma-separated list).
|
||||
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
|
||||
|
||||
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
|
||||
// Force the shared-memory transport, which keeps compositing and threaded
|
||||
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
|
||||
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
|
||||
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
|
||||
}
|
||||
|
||||
if (x11_opt_in) {
|
||||
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
||||
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
||||
@@ -1202,23 +1215,18 @@ int CLI::run(int argc, char **argv)
|
||||
if (wayland_env && *wayland_env) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||
::setenv("GDK_BACKEND", "x11", true);
|
||||
|
||||
// GTK will use X11 in a Wayland session here, which is the
|
||||
// one default-path case that really runs through XWayland.
|
||||
// WebKit2GTK accelerated compositing could fail silently
|
||||
// there on releases before 2.42, so force it off. Disabling
|
||||
// it also turns off threaded scrolling, hence the gate.
|
||||
if (WebView::NeedsCompositingWorkaround())
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// WebKit2GTK compositing can fail under XWayland on older
|
||||
// WebKit releases. Disable it only when both DISPLAY and
|
||||
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
|
||||
// use). On pure X11 or native Wayland, compositing is left
|
||||
// enabled so WebKit retains HW acceleration.
|
||||
{
|
||||
const char* display_env_wk = ::getenv("DISPLAY");
|
||||
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
|
||||
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
|
||||
// XInitThreads is needed before GStreamer may use Xlib. On
|
||||
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
||||
// (no Xlib involved), so the call is skipped.
|
||||
@@ -1539,6 +1547,9 @@ int CLI::run(int argc, char **argv)
|
||||
if (allow_mix_temp_option)
|
||||
allow_mix_temp = allow_mix_temp_option->value;
|
||||
|
||||
const ConfigOptionBool* export_statistics_option = m_config.option<ConfigOptionBool>("export_statistics");
|
||||
const bool export_statistics = export_statistics_option != nullptr && export_statistics_option->value;
|
||||
|
||||
ConfigOptionBool* avoid_extrusion_cali_region_option = m_config.option<ConfigOptionBool>("avoid_extrusion_cali_region");
|
||||
if (avoid_extrusion_cali_region_option)
|
||||
avoid_extrusion_cali_region = avoid_extrusion_cali_region_option->value;
|
||||
@@ -7081,6 +7092,16 @@ int CLI::run(int argc, char **argv)
|
||||
record_exit_reson(outfile_dir, CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, index + 1, cli_errors[CLI_GCODE_PATH_IN_UNPRINTABLE_AREA], sliced_info);
|
||||
flush_and_exit(CLI_GCODE_PATH_IN_UNPRINTABLE_AREA);
|
||||
}
|
||||
// Without an output directory the G-code is a temporary file, kept only in an exported 3MF.
|
||||
if (export_statistics && !outfile_dir.empty()) {
|
||||
try {
|
||||
export_gcode_statistics(*gcode_result, gcode_statistics_path(outfile));
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate " << index + 1 << ": " << ex.what();
|
||||
record_exit_reson(outfile_dir, CLI_EXPORT_STATISTICS_ERROR, index + 1, cli_errors[CLI_EXPORT_STATISTICS_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_EXPORT_STATISTICS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//outfile_final = (dynamic_cast<Print*>(print))->print_statistics().finalize_output_path(outfile);
|
||||
@@ -7209,7 +7230,7 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<ThumbnailData *> thumbnails, no_light_thumbnails, top_thumbnails, pick_thumbnails;
|
||||
std::vector<PlateBBoxData*> plate_bboxes;
|
||||
PlateDataPtrs plate_data_list;
|
||||
partplate_list.store_to_3mf_structure(plate_data_list);
|
||||
partplate_list.store_to_3mf_structure(plate_data_list, true, -1, export_statistics);
|
||||
|
||||
if (sliced_plate == -1) {
|
||||
for (int i = 0; i < plate_data_list.size(); i++) {
|
||||
|
||||
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
|
||||
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
|
||||
export MESA_LOADER_DRIVER_OVERRIDE=zink
|
||||
export GALLIUM_DRIVER=zink
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
|
||||
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -399,6 +399,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_plate_gridlines").empty())
|
||||
set_bool("show_plate_gridlines", true);
|
||||
|
||||
if (get("focus_mode").empty())
|
||||
set_bool("focus_mode", false);
|
||||
|
||||
if (get("show_outline").empty())
|
||||
set_bool("show_outline", false);
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ set(lisbslic3r_sources
|
||||
GCode/FanMover.hpp
|
||||
GCode/GCodeProcessor.cpp
|
||||
GCode/GCodeProcessor.hpp
|
||||
GCode/GCodeStatistics.cpp
|
||||
GCode/GCodeStatistics.hpp
|
||||
GCode/ElegooGCodeProcessorHelper.cpp
|
||||
GCode.hpp
|
||||
GCode/PchipInterpolatorHelper.cpp
|
||||
|
||||
@@ -9053,6 +9053,16 @@ bool _BBS_3MF_Exporter::_add_gcode_file_to_archive(mz_zip_archive& archive, cons
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", store %1% to 3mf %2%\n") % src_gcode_file % gcode_in_3mf;
|
||||
}
|
||||
});
|
||||
for (const PlateData *plate_data : plate_data_list2) {
|
||||
if (plate_data->statistics.empty())
|
||||
continue;
|
||||
const std::string statistics_in_3mf = (boost::format(STATISTICS_FILE_FORMAT) % (plate_data->plate_index + 1)).str();
|
||||
if (!mz_zip_writer_add_mem(&archive, statistics_in_3mf.c_str(), plate_data->statistics.data(), plate_data->statistics.size(),
|
||||
MZ_DEFAULT_COMPRESSION)) {
|
||||
add_error("Unable to add statistics file to archive");
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ struct ThumbnailData;
|
||||
#define PLATE_THUMBNAIL_SMALL_HEIGHT 128
|
||||
|
||||
#define GCODE_FILE_FORMAT "Metadata/plate_%1%.gcode"
|
||||
#define STATISTICS_FILE_FORMAT "Metadata/plate_%1%.statistics.json"
|
||||
#define THUMBNAIL_FILE_FORMAT "Metadata/plate_%1%.png"
|
||||
#define NO_LIGHT_THUMBNAIL_FILE_FORMAT "Metadata/plate_no_light_%1%.png"
|
||||
#define TOP_FILE_FORMAT "Metadata/top_%1%.png"
|
||||
@@ -100,6 +101,8 @@ struct PlateData
|
||||
std::string nozzle_volume_types;
|
||||
std::string gcode_file;
|
||||
std::string gcode_file_md5;
|
||||
// The G-code's statistics as JSON, stored with the G-code when not empty.
|
||||
std::string statistics;
|
||||
std::string thumbnail_file;
|
||||
std::string no_light_thumbnail_file;
|
||||
ThumbnailData plate_thumbnail;
|
||||
@@ -174,6 +177,8 @@ enum class SaveStrategy
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
// With WithGcode, also the G-code's statistics.
|
||||
WithStatistics = 1 << 14,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
|
||||
+20
-11
@@ -2625,8 +2625,15 @@ static void set_mass_locator(GCodeProcessor &processor, const Print &print)
|
||||
}
|
||||
Object &o = objects.emplace_back(Object{ object, int(object_masses.size()), count, bodies_total, {}, std::move(bodies), {}, {} });
|
||||
object_masses.resize(object_masses.size() + object->instances().size());
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance)
|
||||
object_masses[o.first_instance + instance].assembly = assembly;
|
||||
const ModelInstancePtrs &model_instances = object->model_object()->instances;
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance) {
|
||||
GCodeProcessorResult::ObjectMass &mass = object_masses[o.first_instance + instance];
|
||||
mass.assembly = assembly;
|
||||
mass.name = object->model_object()->name;
|
||||
// Copies of another rotation or scale slice as objects of their own, so number them in the model object.
|
||||
mass.instance = int(std::find(model_instances.begin(), model_instances.end(), object->instances()[instance].model_instance) -
|
||||
model_instances.begin());
|
||||
}
|
||||
bodies_total += int(count * object->instances().size());
|
||||
for (const Layer *layer : layers) {
|
||||
o.print_zs.emplace_back(layer->print_z);
|
||||
@@ -3563,6 +3570,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
print.throw_if_canceled();
|
||||
|
||||
// Taken before the object loop, which reassigns initial_extruder_id when printing by object.
|
||||
const int compat_first_layer_bed_temp = m_config.bed_temperature_formula == BedTempFormula::btfHighestTemp ?
|
||||
get_highest_bed_temperature(true, print) :
|
||||
get_bed_temperature(initial_extruder_id, true, print.config().curr_bed_type);
|
||||
const int compat_first_layer_nozzle_temp =
|
||||
print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int) initial_extruder_id));
|
||||
|
||||
int extruder_id = get_extruder_id(initial_extruder_id);
|
||||
|
||||
m_cooling_buffer = make_unique<CoolingBuffer>(*this);
|
||||
@@ -4153,12 +4167,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple image
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n",
|
||||
first_layer_bed_temperature);
|
||||
file.write_format(
|
||||
"; first_layer_temperature = %d\n",
|
||||
print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
} else if (thumbnail_cb != nullptr) {
|
||||
// generate the thumbnails
|
||||
@@ -4743,10 +4753,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
|
||||
@@ -298,6 +298,9 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
BoundingBoxf3 box;
|
||||
// Of an object, whether it is an assembly.
|
||||
bool assembly{ false };
|
||||
// Of an object instance, its model object's name and its index among that object's instances.
|
||||
std::string name;
|
||||
int instance{ -1 };
|
||||
|
||||
Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); }
|
||||
void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "GCodeStatistics.hpp"
|
||||
|
||||
#include "GCodeProcessor.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <ios>
|
||||
#include <map>
|
||||
#include <math.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::ordered_json;
|
||||
|
||||
double rounded(double value, int decimals)
|
||||
{
|
||||
const double scale = std::pow(10., decimals);
|
||||
return std::round(value * scale) / scale;
|
||||
}
|
||||
|
||||
// As the object masses weigh it, at the default density for a filament without one.
|
||||
double weight_of(const GCodeProcessorResult &result, size_t filament, double volume)
|
||||
{
|
||||
const bool has_density = filament < result.filament_densities.size() && result.filament_densities[filament] > 0.f;
|
||||
// mm³ at g/cm³ weigh mg.
|
||||
return 0.001 * volume * (has_density ? result.filament_densities[filament] : DEFAULT_FILAMENT_DENSITY);
|
||||
}
|
||||
|
||||
struct Usage
|
||||
{
|
||||
double time{ 0. };
|
||||
double volume{ 0. };
|
||||
double weight{ 0. };
|
||||
|
||||
void add(const Usage &other)
|
||||
{
|
||||
time += other.time;
|
||||
volume += other.volume;
|
||||
weight += other.weight;
|
||||
}
|
||||
|
||||
json to_json() const { return json{ { "time", rounded(time, 3) }, { "volume", rounded(volume, 3) }, { "weight", rounded(weight, 4) } }; }
|
||||
};
|
||||
|
||||
json features_json(const std::map<ExtrusionRole, Usage> &features)
|
||||
{
|
||||
json out = json::object();
|
||||
for (const auto &[role, usage] : features)
|
||||
out[ExtrusionEntity::role_to_string(role)] = usage.to_json();
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result)
|
||||
{
|
||||
constexpr size_t normal = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
|
||||
struct Layer
|
||||
{
|
||||
float z{ 0.f };
|
||||
double travel_time{ 0. };
|
||||
Usage total;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
double travel_time = 0.;
|
||||
|
||||
// As the preview sums them: a layer takes the time of all its moves, a feature that of its extrusions.
|
||||
for (const GCodeProcessorResult::MoveVertex &move : result.moves) {
|
||||
if (layers.size() <= move.layer_id)
|
||||
layers.resize(move.layer_id + 1);
|
||||
Layer &layer = layers[move.layer_id];
|
||||
const double time = move.time[normal];
|
||||
layer.total.time += time;
|
||||
if (move.type == EMoveType::Travel) {
|
||||
layer.travel_time += time;
|
||||
travel_time += time;
|
||||
}
|
||||
// A vertex added inside a move to show its acceleration repeats the move's extrusion.
|
||||
if (move.type != EMoveType::Extrude || move.internal_only)
|
||||
continue;
|
||||
if (move.extrusion_role != erCustom)
|
||||
layer.z = move.position.z();
|
||||
Usage usage;
|
||||
usage.time = time;
|
||||
usage.volume = double(move.delta_extruder) * M_PI * sqr(0.5 * double(result.filament_diameters[move.extruder_id]));
|
||||
usage.weight = weight_of(result, move.extruder_id, usage.volume);
|
||||
layer.total.volume += usage.volume;
|
||||
layer.total.weight += usage.weight;
|
||||
layer.features[move.extrusion_role].add(usage);
|
||||
features[move.extrusion_role].add(usage);
|
||||
}
|
||||
|
||||
// As the preview's filament table, which counts the flush too.
|
||||
const PrintEstimatedStatistics &stats = result.print_statistics;
|
||||
double volume = 0., weight = 0., cost = 0.;
|
||||
json filaments_json = json::array();
|
||||
for (const auto &entry : stats.total_volumes_per_extruder) {
|
||||
const size_t filament = entry.first;
|
||||
const auto use = [&result, filament](const std::map<size_t, double> &volumes) {
|
||||
const auto it = volumes.find(filament);
|
||||
const double used = it == volumes.end() ? 0. : it->second;
|
||||
return json{ { "volume", rounded(used, 3) }, { "weight", rounded(weight_of(result, filament, used), 4) } };
|
||||
};
|
||||
const double filament_weight = weight_of(result, filament, entry.second);
|
||||
// Priced per kilogram.
|
||||
const double filament_cost = filament < result.filament_costs.size() ? 0.001 * filament_weight * result.filament_costs[filament] : 0.;
|
||||
volume += entry.second;
|
||||
weight += filament_weight;
|
||||
cost += filament_cost;
|
||||
filaments_json.push_back(json{ { "id", filament + 1 },
|
||||
{ "length", rounded(entry.second / (M_PI * sqr(0.5 * double(result.filament_diameters[filament]))), 2) },
|
||||
{ "volume", rounded(entry.second, 3) },
|
||||
{ "weight", rounded(filament_weight, 4) },
|
||||
{ "cost", rounded(filament_cost, 2) },
|
||||
{ "model", use(stats.model_volumes_per_extruder) },
|
||||
{ "support", use(stats.support_volumes_per_extruder) },
|
||||
{ "prime_tower", use(stats.wipe_tower_volumes_per_extruder) },
|
||||
{ "flush", use(stats.flush_per_filament) } });
|
||||
}
|
||||
|
||||
json summary = json{ { "time", rounded(stats.modes[normal].time, 3) },
|
||||
{ "prepare_time", rounded(stats.modes[normal].prepare_time, 3) },
|
||||
{ "travel_time", rounded(travel_time, 3) },
|
||||
{ "travel_distance", rounded(stats.total_travel_distance, 2) },
|
||||
{ "layer_count", layers.size() },
|
||||
{ "volume", rounded(volume, 3) },
|
||||
{ "weight", rounded(weight, 4) },
|
||||
{ "filament_cost", rounded(cost, 2) },
|
||||
{ "filament_changes", stats.total_filament_changes },
|
||||
{ "filaments", filaments_json },
|
||||
{ "features", features_json(features) } };
|
||||
|
||||
json layers_json = json::array();
|
||||
for (const Layer &layer : layers)
|
||||
layers_json.push_back(json{ { "z", rounded(layer.z, 3) },
|
||||
{ "time", rounded(layer.total.time, 3) },
|
||||
{ "travel_time", rounded(layer.travel_time, 3) },
|
||||
{ "volume", rounded(layer.total.volume, 3) },
|
||||
{ "weight", rounded(layer.total.weight, 4) },
|
||||
{ "features", features_json(layer.features) } });
|
||||
|
||||
// The supports of an instance are numbered as the instance.
|
||||
json objects_json = json::array();
|
||||
for (size_t i = 0; i < result.object_masses.size(); ++i) {
|
||||
const GCodeProcessorResult::ObjectMass &object = result.object_masses[i];
|
||||
const GCodeProcessorResult::ObjectMass::Sum part = object.total();
|
||||
const GCodeProcessorResult::ObjectMass::Sum support = i < result.support_masses.size() ? result.support_masses[i].total() :
|
||||
GCodeProcessorResult::ObjectMass::Sum{};
|
||||
// In mg, as volumes in mm³ at g/cm³.
|
||||
objects_json.push_back(json{ { "name", object.name },
|
||||
{ "instance", object.instance },
|
||||
{ "volume", rounded(part.volume, 3) },
|
||||
{ "weight", rounded(0.001 * part.mass, 4) },
|
||||
{ "support_volume", rounded(support.volume, 3) },
|
||||
{ "support_weight", rounded(0.001 * support.mass, 4) } });
|
||||
}
|
||||
|
||||
// The version goes up only when a key is removed, renamed or changes meaning.
|
||||
const json out = json{ { "version", 1 },
|
||||
{ "generator", std::string(SLIC3R_APP_NAME) + " " + SoftFever_VERSION },
|
||||
{ "units", json{ { "time", "s" }, { "length", "mm" }, { "volume", "mm3" }, { "weight", "g" } } },
|
||||
{ "summary", summary },
|
||||
{ "layers", layers_json },
|
||||
{ "objects", objects_json } };
|
||||
return out.dump(2);
|
||||
}
|
||||
|
||||
std::string gcode_statistics_path(const std::string &gcode_path)
|
||||
{
|
||||
return boost::filesystem::path(gcode_path).replace_extension(".statistics.json").string();
|
||||
}
|
||||
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path)
|
||||
{
|
||||
// Binary, so the file reads the same on every platform and inside a 3MF.
|
||||
boost::nowide::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file << gcode_statistics_json(result);
|
||||
file.close();
|
||||
if (file.fail())
|
||||
throw ExportError(format(_u8L("Failed to write the G-code statistics to %1%."), path));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct GCodeProcessorResult;
|
||||
|
||||
// The statistics of a plate's G-code as JSON, in seconds, millimeters and grams: totals, per layer and per object instance.
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result);
|
||||
// Where the statistics of the G-code at gcode_path are written.
|
||||
std::string gcode_statistics_path(const std::string &gcode_path);
|
||||
// Throws ExportError when the file cannot be written.
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
|
||||
return decode_colored_png(stream, out_img);
|
||||
}
|
||||
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
struct ReadBufStream stream{pngbuf};
|
||||
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
|
||||
stream.read(sig.data(), PNG_SIG_BYTES);
|
||||
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
PNGDescr dsc;
|
||||
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!dsc.png)
|
||||
return false;
|
||||
dsc.info = png_create_info_struct(dsc.png);
|
||||
if (!dsc.info)
|
||||
return false;
|
||||
|
||||
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
|
||||
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
|
||||
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
|
||||
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
|
||||
bit_depth == 8 && (grey || colour);
|
||||
}
|
||||
|
||||
|
||||
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
|
||||
// Based on https://www.lemoda.net/c/write-png/
|
||||
|
||||
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
|
||||
|
||||
bool is_png(const ReadBuf &pngbuf);
|
||||
|
||||
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
|
||||
|
||||
struct ReadBufStream: public IStream {
|
||||
const ReadBuf &rbuf_ref;
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -13040,6 +13040,13 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def->cli_params = "dir";
|
||||
def->set_default_value(new ConfigOptionString());
|
||||
|
||||
def = this->add("export_statistics", coBool);
|
||||
def->label = L("Export statistics");
|
||||
def->tooltip = L("Write the statistics of each sliced plate as JSON, as plate_N.statistics.json next to its G-code in the output "
|
||||
"directory and inside the 3MF of --export-3mf: print time, filament and feature totals, and the same per layer "
|
||||
"and per object.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("debug", coInt);
|
||||
def->label = L("Debug level");
|
||||
def->tooltip = L("Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:trace\n");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user