Compare commits

..
Author SHA1 Message Date
HanifKoh 2dd66b8f20 Merge branch 'main' into hanif/wizard-loading-signal 2026-10-15 13:44:00 +08:00
Hanif Koh d5165c3e75 Tell the Setup Wizard's Loading Page Only Once It Has Loaded
The wizard's loading page waits for HandleStudio to be called with
userguide_profile_load_finish and jumps to its target page on it; if the
call never arrives, a fallback timer in the page jumps after 180 s.

GuideFrame started the profile load from the first wxEVT_WEBVIEW_NAVIGATED
event and ran HandleStudio in the page as soon as the data was ready. That
event is the navigation's commit, before the page's scripts have run, and
with the wizard's derived cache the data is ready about 100 ms later.
Since the web view is created with an empty URL so the translated strings
can be injected before the first load, the first navigated event is the
commit of about:blank, so the data is ready before the real page has even
begun loading and the call reaches a page without HandleStudio. WebKit
drops the error and nothing retries, so on macOS the wizard often sits on
"Loading..." for three minutes before the printer list appears.

The loading page now reports in from its OnInit, the way the other wizard
pages request what they need, and the host sends the message once both
the data is ready and the page has reported; every instance of the page
reports again, so one that replaced an earlier one gets its own message.
2026-10-14 19:08:07 +08:00
146 changed files with 7062 additions and 48253 deletions
@@ -569,18 +569,12 @@ on the **first** call. So:
#endif
```
**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()`
**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()`
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
**wx's own detectors**, for reference:
@@ -61,8 +61,7 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
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 the no-EGL default path where Orca forces
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, 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
@@ -568,69 +567,33 @@ 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)
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.
branches on `GDK_BACKEND`:
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
`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.
`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.
- **`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, which keeps hardware acceleration for Device / Setup Wizard / login pages.
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
(see `references/platforms.md`), not these environment variables.
**Pitfall.**
- **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
- **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
waits forever for a C++→JS call.
**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.
**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.
```cpp
// Wrong: unconditional, kills HW acceleration everywhere
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
// 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)
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
if (display && *display && wayland && *wayland)
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
```
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
+1 -15
View File
@@ -22,7 +22,7 @@ function usage() {
echo " -g: run the requested build steps inside a Docker/Podman Ubuntu 24.04 container similar to the GitHub Actions Linux runner"
echo " -h: prints this help text"
echo " -i: build the Orca Slicer AppImage (optional)"
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
echo " -p: boost ccache hit rate by disabling precompiled headers (default: ON)"
echo " -r: skip RAM and disk checks (low RAM compiling)"
echo " -s: build the Orca Slicer (optional)"
echo " -t: build tests (optional), requires -s flag"
@@ -549,20 +549,6 @@ 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
+17 -2
View File
@@ -1,7 +1,22 @@
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
if (JPEG_VERSION STREQUAL "6")
message("Using Jpeg Lib 62")
set(jpeg_flag "")
elseif (JPEG_VERSION STREQUAL "7")
message("Using Jpeg Lib 70")
set(jpeg_flag "-DWITH_JPEG7=ON")
else ()
message("Using Jpeg Lib 80")
set(jpeg_flag "-DWITH_JPEG8=ON")
endif ()
endif()
orcaslicer_add_cmake_project(JPEG
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
DEPENDS ${ZLIB_PKG}
CMAKE_ARGS
-DENABLE_SHARED=OFF
-DENABLE_STATIC=ON
${jpeg_flag}
)
-131
View File
@@ -1,131 +0,0 @@
# 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.
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
-1
View File
@@ -325,7 +325,6 @@ 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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 366 B

@@ -592,10 +592,6 @@
"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"
@@ -1100,10 +1096,6 @@
"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"
@@ -2268,10 +2260,6 @@
"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"
@@ -3824,10 +3812,6 @@
"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"
@@ -1,18 +0,0 @@
{
"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": []
}
@@ -1,80 +0,0 @@
{
"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"
]
}
@@ -1,18 +0,0 @@
{
"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": []
}
@@ -1,35 +0,0 @@
{
"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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "RatRig",
"version": "02.04.00.07",
"version": "02.04.00.06",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] 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]}",
"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]}",
"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] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single]",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] 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",
@@ -1,155 +0,0 @@
/* 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};
});
@@ -1,314 +0,0 @@
// 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);
});
@@ -1,464 +0,0 @@
"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");
})();
@@ -1,103 +0,0 @@
"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"]);
});
@@ -1,154 +0,0 @@
<!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>
@@ -1,459 +0,0 @@
"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;
})();
@@ -1,534 +0,0 @@
: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()">
-9
View File
@@ -55,13 +55,6 @@
/* 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"] {
@@ -100,8 +93,6 @@
/* 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
+2 -6
View File
@@ -156,16 +156,12 @@ function SetCloudMode( bOn )
m_CloudMode=bOn;
$("body").toggleClass("cloud-mode", bOn);
if( bOn )
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
}
}
function GotoCloud()
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
SetCloudMode(true);
SendSimpleCommand("homepage_open_cloud");
}
@@ -336,10 +336,10 @@ modules:
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
dest: external-packages/PNG
# libjpeg-turbo 3.2.0
# libjpeg-turbo 3.0.1
- type: file
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
dest: external-packages/JPEG
# Freetype 2.12.1
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
# 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://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
+15 -21
View File
@@ -163,7 +163,6 @@ using json = nlohmann::json;
#include <X11/Xlib.h>
#endif
#include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif
#ifdef SLIC3R_GUI
@@ -1157,10 +1156,8 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup
// 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.
// Wizard / login / store. The default path still applies it on
// XWayland sessions as a conservative fallback for older WebKit.
// ------------------------------------------------------------------
{
const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1168,14 +1165,6 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list).
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.";
@@ -1215,18 +1204,23 @@ int CLI::run(int argc, char **argv)
if (wayland_env && *wayland_env) {
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
::setenv("GDK_BACKEND", "x11", true);
// GTK will use X11 in a Wayland session here, which is the
// one default-path case that really runs through XWayland.
// WebKit2GTK accelerated compositing could fail silently
// there on releases before 2.42, so force it off. Disabling
// it also turns off threaded scrolling, hence the gate.
if (WebView::NeedsCompositingWorkaround())
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
#endif
// WebKit2GTK compositing can fail under XWayland on older
// WebKit releases. Disable it only when both DISPLAY and
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
// use). On pure X11 or native Wayland, compositing is left
// enabled so WebKit retains HW acceleration.
{
const char* display_env_wk = ::getenv("DISPLAY");
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
// XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink
// (no Xlib involved), so the call is skipped.
@@ -429,8 +429,7 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
export WEBKIT_DISABLE_DMABUF_RENDERER=1
fi
fi
fi
+16 -25
View File
@@ -432,36 +432,27 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string OozePrevention::pre_toolchange(GCode& gcodegen)
{
std::string gcode;
unsigned int extruder_id = gcodegen.writer().filament()->id();
// Check if this tool will ever be used again in this print.
bool is_last_use = false;
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
const auto& cfg = gcodegen.config();
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
for (unsigned int f : tool_ordering.all_extruders())
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
}
int temp = 0;
if (!is_last_use) {
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) {
if (gcodegen.config().standby_temperature_delta.value == 0)
return std::string();
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
} else {
temp = filament_idle_temp.get_at(extruder_id);
}
}
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
// There is no idle temperature defined in filament settings.
// Use the delta value from print config.
if (gcodegen.config().standby_temperature_delta.value != 0) {
// we assume that heating is always slower than cooling, so no need to block
gcode += gcodegen.writer().set_temperature
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
}
} else {
// Use the value from filament settings. That one is absolute, not delta.
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
}
return gcode;
}
-1
View File
@@ -1004,7 +1004,6 @@ protected:
size_t num_objects,
size_t num_islands);
friend class OozePrevention;
friend class Wipe;
friend class WipeTowerIntegration;
friend class PressureEqualizer;
+1 -14
View File
@@ -1276,13 +1276,6 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
sort_remove_duplicates(m_all_printing_extruders);
}
m_last_layer_per_extruder.clear();
for (const LayerTools& lt : m_layer_tools) {
for (unsigned int ext : lt.extruders) {
m_last_layer_per_extruder[ext] = lt.print_z;
}
}
if (prime_multi_material && ! m_all_printing_extruders.empty()) {
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
// Then set m_first_printing_extruder to the 1st extruder primed.
@@ -3671,11 +3664,5 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
return -1;
}
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
{
auto it = m_last_layer_per_extruder.find(extruder_id);
if (it == m_last_layer_per_extruder.end())
return true;
return print_z >= it->second - EPSILON;
}
} // namespace Slic3r
-3
View File
@@ -370,7 +370,6 @@ public:
std::vector<int> & initial_filaments);
bool has_non_support_filament(const PrintConfig &config);
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
private:
void initialize_layers(std::vector<coordf_t> &zs);
@@ -414,8 +413,6 @@ private:
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
// no-ops; downstream emission falls through to the per-object branch).
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
bool m_sorted = false;
+5 -4
View File
@@ -858,10 +858,11 @@ void PrintObject::contour_z()
BOOST_LOG_TRIVIAL(debug) << "Contouring in parallel - start";
TriangleMesh mesh = this->m_model_object->raw_mesh();
// A PrintObject groups the instances that share their whole transformation but the XY shift: an
// instance rotated, scaled or mirrored differently is a PrintObject of its own. So the first
// instance stands for all of them.
const ModelInstance *inst = m_instances.front().model_instance;
if (m_model_object->instances.size() != 1) {
throw RuntimeError("ContourZ: unexpected number of instances");
}
ModelInstance *inst = m_model_object->instances.front();
Point center_offset = this->center_offset();
Geometry::Transformation trans = inst->get_transformation();
-8
View File
@@ -129,8 +129,6 @@ set(SLIC3R_GUI_SOURCES
GUI/Shortcuts.hpp
GUI/SpeedDialDialog.cpp
GUI/SpeedDialDialog.hpp
GUI/ColorPickerDialog.cpp
GUI/ColorPickerDialog.hpp
GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
@@ -222,7 +220,6 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoUtils.hpp
GUI/GLCanvas3D.cpp
GUI/GLCanvas3D.hpp
GUI/SequentialPrintClearance.hpp
GUI/GLModel.cpp
GUI/GLModel.hpp
GUI/GLSelectionRectangle.cpp
@@ -690,8 +687,6 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/host/PluginResourceTextures.cpp
plugin/host/PluginResourceTextures.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
@@ -715,9 +710,6 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+112 -286
View File
@@ -1,9 +1,4 @@
#include "AMSMaterialsSetting.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <variant>
#include <cstdint>
#include "ExtrusionCalibration.hpp"
#include "MsgDialog.hpp"
#include "GUI_App.hpp"
@@ -41,9 +36,11 @@
#include <cmath>
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include "slic3r/GUI/Widgets/StaticBox.hpp"
#include <wx/colordlg.h>
#include <wx/dcclient.h>
#include <wx/dc.h>
#include <wx/colour.h>
#include <wx/colourdata.h>
#include <wx/dcgraph.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
@@ -85,96 +82,6 @@ static std::string float_to_string_with_precision(float value, int precision = 3
return stream.str();
}
static std::string colour_to_ams_string(const wxColour& color)
{
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
return col_buf;
}
static wxColour mix_colour(const wxColour& left, const wxColour& right, double ratio)
{
ratio = std::max(0.0, std::min(1.0, ratio));
auto mix_channel = [ratio](unsigned char l, unsigned char r) {
return static_cast<unsigned char>(std::round(l + (r - l) * ratio));
};
return wxColour(mix_channel(left.Red(), right.Red()),
mix_channel(left.Green(), right.Green()),
mix_channel(left.Blue(), right.Blue()),
mix_channel(left.Alpha(), right.Alpha()));
}
static std::vector<ColorPickerPopup::ColorItem> collect_ams_color_items(DevFilaSystem* fila_system)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (!fila_system) return items;
for (const auto& ams_pair : fila_system->GetAmsList()) {
DevAms* ams = ams_pair.second;
if (!ams) continue;
for (const auto& tray_pair : ams->GetTrays()) {
DevAmsTray* tray = tray_pair.second;
if (!tray || !tray->is_tray_info_ready()) continue;
ColorPickerPopup::ColorItem item;
for (const std::string& col : tray->cols) {
item.colors.emplace_back(DevAmsTray::decode_color(col));
}
if (item.colors.empty() && !tray->color.empty()) {
item.colors.emplace_back(DevAmsTray::decode_color(tray->color));
}
if (item.colors.empty()) continue;
item.ctype = item.colors.size() > 1 ? tray->ctype : 2;
items.emplace_back(std::move(item));
}
}
return items;
}
static std::vector<ColorPickerPopup::ColorItem> collect_preset_color_items(const std::string& filament_id)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (filament_id.empty()) return items;
auto* clr_query = GUI::wxGetApp().get_filament_color_code_query();
if (!clr_query) return items;
auto* agent = wxGetApp().getAgent();
const std::string catalog_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id;
FilamentColorCodes* color_codes = clr_query->GetFilaInfoMap(wxString::FromUTF8(catalog_id));
if (!color_codes || !color_codes->GetFilamentColor2CodeMap()) return items;
std::set<std::string> seen;
for (const auto& color_pair : *color_codes->GetFilamentColor2CodeMap()) {
const FilamentColor& fila_color = color_pair.first;
FilamentColorCode* color_code = color_pair.second;
if (!color_code || fila_color.GetColors().empty()) continue;
ColorPickerPopup::ColorItem item;
item.ctype = fila_color.ColorCount() < 2 ? 2 :
(fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR ? 0 : 1);
item.name = color_code->GetFilaColorName();
std::string key = std::to_string(item.ctype);
for (const wxColour& color : fila_color.GetColors()) {
item.colors.emplace_back(color);
char rgba[9];
std::snprintf(rgba, sizeof(rgba), "%02X%02X%02X%02X",
static_cast<unsigned>(color.Red()), static_cast<unsigned>(color.Green()),
static_cast<unsigned>(color.Blue()), static_cast<unsigned>(color.Alpha()));
key += "|" + std::string(rgba);
}
if (item.colors.empty() || !seen.insert(key).second) continue;
items.emplace_back(std::move(item));
}
return items;
}
AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id)
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_color_picker_popup(ColorPickerPopup(this))
@@ -830,19 +737,8 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
wxColour color = m_clr_picker->m_colour;
std::string tray_color = colour_to_ams_string(color);
std::vector<std::string> tray_colors;
int tray_ctype = 2;
// Only firmware advertising fun2[23] receives the manual multi-color fields.
if (obj->is_support_filament_manual_multi_color) {
for (const wxColour& selected_color : m_clr_picker->m_cols) {
tray_colors.emplace_back(colour_to_ams_string(selected_color));
}
if (tray_colors.empty()) {
tray_colors.emplace_back(tray_color);
}
tray_ctype = tray_colors.size() > 1 ? m_clr_picker->ctype : 2;
}
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) {
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
@@ -861,33 +757,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
<< ", tray_info_idx (filament_id) = " << ams_filament_id
<< ", setting_id = " << ams_setting_id
<< ", tray_type = " << m_filament_type
<< ", tray_color = " << tray_color
<< ", tray_color = " << col_buf
<< ", nozzle_temp_min = " << nozzle_temp_min_int
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
// set filament
if (m_is_third) {
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, tray_color, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int,
tray_colors, tray_ctype);
// Refresh the edited tray immediately while its MQTT acknowledgement is pending.
DevAmsTray* tray = nullptr;
if (is_virtual_tray()) {
for (auto& virtual_tray : obj->vt_slot) {
if (virtual_tray.id == std::to_string(ams_id)) {
tray = &virtual_tray;
break;
}
}
} else {
tray = obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
}
if (tray) {
tray->color = tray_color;
tray->cols = tray_colors;
tray->ctype = tray_ctype;
tray->set_hold_count();
}
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int);
}
//reset param
@@ -1044,15 +920,8 @@ void AMSMaterialsSetting::set_ctype(int ctype)
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
{
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
if (colors.empty()) {
unsigned int color_num = event.GetInt();
colors.emplace_back(color_num >> 24 & 0xFF, color_num >> 16 & 0xFF, color_num >> 8 & 0xFF, color_num & 0xFF);
}
set_ctype(m_color_picker_popup.get_selected_ctype());
set_color(colors.front());
set_colors(colors);
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
}
void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
@@ -1067,12 +936,14 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
}
}
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id));
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour, m_clr_picker->m_cols, m_clr_picker->ctype);
std::vector<wxColour> ams_colors;
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
m_color_picker_popup.set_ams_colours(ams_colors);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
m_color_picker_popup.Popup();
}
@@ -1861,46 +1732,7 @@ void ColorPicker::doRender(wxDC& dc)
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
if (m_selected) radius -= FromDIP(1);
if (m_show_full && m_cols.size() > 1) {
if (ctype == 0) {
const double center_x = size.x / 2.0;
const double center_y = size.y / 2.0;
const double draw_radius = radius;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int x = 0; x < size.x; ++x) {
const double dx = x + 0.5 - center_x;
if (std::abs(dx) > draw_radius) continue;
const double half_height = std::sqrt(std::max(0.0, draw_radius * draw_radius - dx * dx));
const int top = std::max(0, static_cast<int>(std::ceil(center_y - half_height)));
const int bottom = std::min(size.y - 1, static_cast<int>(std::floor(center_y + half_height)));
const double pos = size.x > 1 ? static_cast<double>(x) / (size.x - 1) : 0.0;
const double scaled = pos * (m_cols.size() - 1);
const size_t idx = std::min(static_cast<size_t>(scaled), m_cols.size() - 2);
const double ratio = scaled - idx;
if (top <= bottom) {
dc.SetBrush(wxBrush(mix_colour(m_cols[idx], m_cols[idx + 1], ratio)));
dc.DrawRectangle(x, top, 1, bottom - top + 1);
}
}
}
else {
float ev_angle = 360.0 / m_cols.size();
const float overlap = 2.0f;
wxPoint center(size.x / 2, size.y / 2);
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_cols.front()));
dc.DrawCircle(center.x, center.y, radius);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
float startAngle = 270.0f + i * ev_angle;
float endAngle = startAngle + ev_angle;
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle - overlap, endAngle + overlap);
}
}
}
else if (alpha == 0) {
if (alpha == 0) {
wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize();
int center_x = (size.x - bmp_size.x) / 2;
int center_y = (size.y - bmp_size.y) / 2;
@@ -1942,6 +1774,52 @@ void ColorPicker::doRender(wxDC& dc)
dc.SetPen(wxPen(wxColour("#6B6B6B")));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(size.x / 2, size.y / 2, radius);
if (m_cols.size() > 1) {
if (ctype == 0) {
int left = FromDIP(0);
float total_width = size.x;
int gwidth = std::round(total_width / (m_cols.size() - 1));
for (int i = 0; i < m_cols.size() - 1; i++) {
if ((left + gwidth) > (size.x)) {
gwidth = size.x - left;
}
auto rect = wxRect(left, 0, gwidth, size.y);
dc.GradientFillLinear(rect, m_cols[i], m_cols[i + 1], wxEAST);
left += gwidth;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
else {
float ev_angle = 360.0 / m_cols.size();
float startAngle = 270.0;
float endAngle = 270.0;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
endAngle += ev_angle;
endAngle = endAngle > 360.0 ? endAngle - 360.0 : endAngle;
wxPoint center(size.x / 2, size.y / 2);
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle, endAngle);
startAngle += ev_angle;
startAngle = startAngle > 360.0 ? startAngle - 360.0 : startAngle;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
}
}
if (m_is_empty) {
@@ -2010,21 +1888,19 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
//other
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
wxFlexGridSizer* fg_sizer;
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
fg_sizer->SetFlexibleDirection(wxBOTH);
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
for (wxColour col : m_def_colors) {
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col);
cp->set_colors({col});
cp->ctype = 2;
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp);
m_default_color_pickers.push_back(cp);
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour);
@@ -2087,11 +1963,16 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
sizer_custom->Add(m_ts_stbitmap_custom, 0, wxEXPAND, 0);
m_custom_cp->Layout();
m_clrData = new wxColourData();
m_clrData->SetChooseFull(true);
m_clrData->SetChooseAlpha(false);
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_other_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
@@ -2102,7 +1983,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_def_color_box->Fit();
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
SetSizerAndFit(m_sizer_main);
SetSizer(m_sizer_main);
Layout();
Fit();
@@ -2110,45 +1991,55 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
wxGetApp().UpdateDarkUIWin(this);
}
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
{
const bool was_shown = IsShown();
std::optional<wxColour> picker_color;
{
const ColorRGBA initial(m_def_col.Red(), m_def_col.Green(), m_def_col.Blue(), static_cast<unsigned char>(255));
// Snapshot the custom control's anchor before releasing the transient popup grab.
ColorPickerDialog dialog(GetParent(), initial, {}, false, m_custom_cp);
Dismiss();
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
const auto& result = std::get<ColorRGBA>(*dialog.selection());
picker_color = wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
static_cast<unsigned char>(std::lround(result.b() * 255.f)));
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
for (int i = 0; i < colors.size(); i++) {
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
}
} // Destroy the modal before restoring the first-level popup and its focus/grab.
if (!picker_color) {
if (was_shown) Popup();
return;
auto clr_dialog = new wxColourDialog(nullptr, m_clrData);
wxColour picker_color;
if (clr_dialog->ShowModal() == wxID_OK) {
m_clrData = &(clr_dialog->GetColourData());
if (colors.size() != CUSTOM_COLOR_COUNT) {
colors.resize(CUSTOM_COLOR_COUNT);
}
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
}
wxGetApp().app_config->save_custom_color_to_config(colors);
picker_color = wxColour(
m_clrData->GetColour().Red(),
m_clrData->GetColour().Green(),
m_clrData->GetColour().Blue(),
255
);
if (picker_color.Alpha() == 0) {
m_ts_stbitmap_custom->Show();
}
else {
m_ts_stbitmap_custom->Hide();
m_custom_cp->SetBackgroundColor(picker_color);
}
set_def_colour(*picker_color);
set_def_colour(picker_color);
wxCommandEvent evt(EVT_SELECTED_COLOR);
const std::uint32_t packed = (std::uint32_t(picker_color->Red()) << 24) |
(std::uint32_t(picker_color->Green()) << 16) |
(std::uint32_t(picker_color->Blue()) << 8) | 255u;
evt.SetInt(static_cast<int>(packed));
unsigned long g_col = ((picker_color.Red() & 0xff) << 24) + ((picker_color.Green() & 0xff) << 16) + ((picker_color.Blue() & 0xff) << 8) + (picker_color.Alpha() & 0xff);
evt.SetInt(g_col);
wxPostEvent(GetParent(), evt);
}
}
void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
{
m_ams_fg_sizer->Clear(false);
if (m_ams_color_pickers.size() > 0) {
for (ColorPicker* col_pick:m_ams_color_pickers) {
std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy();
m_color_pickers.erase(iter);
}
@@ -2158,22 +2049,17 @@ void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
}
m_ams_color_items = ams;
for (const ColorItem& item : m_ams_color_items) {
if (item.colors.empty()) continue;
m_ams_colors = ams;
for (wxColour col : m_ams_colors) {
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_color(col);
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp);
m_ams_color_pickers.push_back(cp);
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
set_def_colour(cp->m_colour);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
@@ -2186,68 +2072,9 @@ void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
Fit();
}
void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors)
{
m_other_fg_sizer->Clear(false);
if (!m_preset_color_pickers.empty()) {
for (ColorPicker* col_pick : m_preset_color_pickers) {
auto iter = std::find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy();
m_color_pickers.erase(iter);
}
}
m_preset_color_pickers.clear();
}
for (const ColorItem& item : preset_colors) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
if (!item.name.empty()) {
cp->SetToolTip(item.name);
}
m_color_pickers.push_back(cp);
m_preset_color_pickers.push_back(cp);
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
evt.SetInt(g_col);
wxPostEvent(GetParent(), evt);
});
}
const bool show_default_colours = m_preset_color_pickers.empty();
for (ColorPicker* cp : m_default_color_pickers) {
cp->Show(show_default_colours);
}
for (ColorPicker* cp : (show_default_colours ? m_default_color_pickers : m_preset_color_pickers)) {
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
}
m_other_fg_sizer->Layout();
Layout();
Fit();
}
void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols, int ctype)
void ColorPickerPopup::set_def_colour(wxColour col)
{
m_def_col = col;
if (cols.empty()) {
cols.push_back(col);
}
m_def_cols = cols;
m_def_ctype = m_def_cols.size() > 1 ? ctype : 2;
for (ColorPicker* cp : m_color_pickers) {
if (cp->m_selected) {
@@ -2256,8 +2083,7 @@ void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols,
}
for (ColorPicker* cp : m_color_pickers) {
if (!cp->IsShown()) continue;
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
if (cp->m_colour == m_def_col) {
cp->set_selected(true);
break;
}
+5 -19
View File
@@ -82,22 +82,15 @@ public:
class ColorPickerPopup : public PopupWindow
{
public:
struct ColorItem
{
std::vector<wxColour> colors;
int ctype = 2;
wxString name;
};
ScalableBitmap m_ts_bitmap_custom;
wxStaticBitmap* m_ts_stbitmap_custom;
StaticBox* m_custom_cp;
wxColourData* m_clrData;
StaticBox* m_def_color_box;
wxFlexGridSizer* m_ams_fg_sizer;
wxFlexGridSizer* m_other_fg_sizer;
wxColour m_def_col;
std::vector<wxColour> m_def_colors;
std::vector<ColorItem> m_ams_color_items;
std::vector<wxColour> m_ams_colors;
std::vector<ColorPicker*> m_color_pickers;
std::vector<ColorPicker*> m_ams_color_pickers;
@@ -105,20 +98,13 @@ public:
ColorPickerPopup(wxWindow* parent);
~ColorPickerPopup() {};
void on_custom_clr_picker(wxMouseEvent& event);
void set_ams_colours(const std::vector<ColorItem>& ams);
void set_preset_colours(const std::vector<ColorItem>& preset_colors);
void set_def_colour(wxColour col, std::vector<wxColour> cols = {}, int ctype = 2);
const std::vector<wxColour>& get_selected_colours() const { return m_def_cols; }
int get_selected_ctype() const { return m_def_ctype; }
void set_ams_colours(std::vector<wxColour> ams);
void set_def_colour(wxColour col);
void paintEvent(wxPaintEvent& evt);
virtual void OnDismiss() wxOVERRIDE;
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
private:
std::vector<ColorPicker*> m_default_color_pickers;
std::vector<ColorPicker*> m_preset_color_pickers;
std::vector<wxColour> m_def_cols;
int m_def_ctype = 2;
public:
};
+1 -3
View File
@@ -747,9 +747,7 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
// Orca: prevent resetting under gcode viewer mode
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
// Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply()
// (loading presets, config or input files, e.g. from the CLI) therefore dereferences a null `mainframe` and crashes.
const auto plater = GUI::wxGetApp().plater();
const auto plater = GUI::wxGetApp().mainframe->m_plater;
if (plater && plater->only_gcode_mode()) {
invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
}
File diff suppressed because it is too large Load Diff
-92
View File
@@ -1,92 +0,0 @@
#pragma once
#include "libslic3r/Color.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <array>
#include <cstddef>
#include <variant>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <wx/gdicmn.h>
class wxWindow;
namespace Slic3r::GUI {
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
// and never offers more, so the picker, favorites and project colors all share one limit.
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
using ColorGradient = std::vector<ColorRGBA>;
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
struct ColorPickerOptions {
bool allow_gradient = false;
bool allow_alpha = false;
};
// Project storage preserves direction; FilamentColor's set is only for the
// official palette and cannot represent ordered custom gradients.
struct FilamentColorPickerValue {
std::vector<std::string> colors;
bool gradient = false;
std::string primary;
};
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
class ColorPickerDialog : public WebViewHostDialog
{
public:
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
~ColorPickerDialog() override;
const std::optional<ColorSelection>& selection() const { return m_selection; }
private:
void add_user_scripts() override;
void on_script_message(const nlohmann::json& payload) override;
void handle_web_command(const nlohmann::json& payload);
void send_initial_state();
void finish(int return_code);
void position_panel();
void move_to_anchor();
void resize_to_content(int height);
void focus_webview();
void repaint_webview();
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_sys_color_changed() override;
ColorSelection m_initial;
ColorPickerOptions m_options;
std::optional<ColorSelection> m_selection;
std::vector<ColorSelection> m_favorites;
std::string m_page_id;
bool m_preserve_multi_color = false;
bool m_init_sent = false;
bool m_page_ready = false;
bool m_closing = false;
bool m_favorites_writable = false;
bool m_sidebar_open = false;
std::string m_palette;
bool m_initial_positioned = false;
wxRect m_anchor_rect;
wxRect m_work_area;
int m_content_height = 360;
int m_content_width = 330;
bool m_positioning = false;
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
};
} // namespace Slic3r::GUI
+1 -7
View File
@@ -1798,8 +1798,7 @@ int MachineObject::command_ams_calibrate(int ams_id)
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type,
int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors, int tray_ctype)
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
{
int tag_tray_id = 0;
int tag_ams_id = ams_id;
@@ -1828,10 +1827,6 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
j["print"]["nozzle_temp_min"] = nozzle_temp_min;
j["print"]["nozzle_temp_max"] = nozzle_temp_max;
j["print"]["tray_type"] = tray_type;
if (!tray_colors.empty()) {
j["print"]["cols"] = tray_colors;
j["print"]["ctype"] = tray_ctype;
}
return this->publish_json(j);
}
@@ -5483,7 +5478,6 @@ void MachineObject::parse_new_info(json print)
is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1);
is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
is_support_filament_manual_multi_color = (get_flag_bits_no_border(fun2, 23) == 1);
if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) {
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
+1 -2
View File
@@ -674,7 +674,6 @@ public:
bool is_support_remote_dry = false;
bool is_support_check_track_switch_match_slice_printer{false};
bool is_support_pa_mode{false};
bool is_support_filament_manual_multi_color{false};
std::optional<bool> m_firmware_support_print_tpu_left;
bool installed_upgrade_kit{false};
@@ -805,7 +804,7 @@ public:
int command_ams_switch_filament(bool switch_filament);
int command_ams_air_print_detect(bool air_print_detect);
int command_ams_calibrate(int ams_id);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors = {}, int tray_ctype = 2);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max);
int command_ams_select_tray(std::string tray_id);
int command_ams_refresh_rfid(int ams_id, int slot_id);
int command_ams_control(std::string action);
+1 -1
View File
@@ -96,7 +96,7 @@ void FilamentColorCodeQuery::LoadFromLocal()
const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>();
for (const auto& color_str : fila_color_strs) {
if (color_str.size() > 3) /* Skip the value like "#0"*/{
fila_color.AddColor(wxColour(color_str));
fila_color.m_colors.emplace(wxColour(color_str));
}
}
}
-11
View File
@@ -87,17 +87,6 @@ struct FilamentColor
public:
size_t ColorCount() const noexcept { return m_colors.size(); }
// Preserve catalog endpoint order for display while retaining set-based lookup identity.
const std::vector<wxColour>& GetColors() const noexcept { return m_color_list; }
void AddColor(const wxColour& color)
{
if (m_colors.insert(color).second) m_color_list.push_back(color);
}
private:
std::vector<wxColour> m_color_list;
public:
void EndSet(int ctype)
{
+165 -74
View File
@@ -2,7 +2,6 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Field.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "wxExtensions.hpp"
#include "Plater.hpp"
@@ -46,11 +45,7 @@
#include <wx/arrstr.h>
#include <wx/anybutton.h>
#include <wx/colour.h>
#include <wx/button.h>
#include <wx/brush.h>
#include <wx/pen.h>
#include <optional>
#include <variant>
#include <wx/clrpicker.h>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/dc.h>
@@ -2623,41 +2618,92 @@ void PluginConfigField::msw_rescale()
void ColourPicker::BUILD()
{
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
// field the same height as other parameter buttons before drawing its swatch.
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
window = button;
button->SetBitmapMargins(0, 0);
// Validate the color
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk()) {
clr = wxTransparentColour;
}
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
if (parent_is_custom_ctrl && m_opt.height < 0)
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
button->SetFont(wxGetApp().normal_font());
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
set_value(boost::any(clr_str));
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
apply_user_color(wxTransparentColour);
event.Skip();
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
convert_to_picker_widget(temp);
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
// // recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
#ifdef __WXMSW__
temp,
#endif
this](wxCommandEvent e) {
#ifdef __WXMSW__
draw_bmp_btn(temp, temp->GetColour());
#endif
on_change_field();
}), temp->GetId());
// ORCA reset value to default on right click. previously no way to switch back on windows
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
#ifdef __WXMSW__
temp->SetColour(wxTransparentColour);
draw_bmp_btn(temp, wxTransparentColour);
#else
set_undef_value(temp);
#endif
on_change_field();
e.Skip();
});
// Native layouts may assign the final field size after construction.
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
draw_bmp_btn();
event.Skip();
});
button->SetToolTip(get_tooltip_text(clr_str));
temp->SetToolTip(get_tooltip_text(clr_str));
}
void ColourPicker::draw_bmp_btn()
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
{
if (!window) return;
auto* btn = static_cast<wxButton*>(window);
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
field->SetColour(wxTransparentColour);
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
wxImage image(btn->GetBitmap().GetSize());
image.InitAlpha();
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return;
#ifdef __WXMSW__
wxGCDC dc2(dc);
#else
wxDC &dc2(dc);
#endif
dc2.SetPen(wxPen("#F1754E", 1));
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
dc.SelectObject(wxNullBitmap);
btn->SetBitmapLabel(bmp);
}
// ORCA match style with button on windows
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
{
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
btn->SetBackgroundColour(*wxWHITE);
wxGetApp().UpdateDarkUI(btn);
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
wxSize btn_sz = btn->GetSize();
wxImage image(btn_sz);
@@ -2666,11 +2712,11 @@ void ColourPicker::draw_bmp_btn()
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return bmp;
wxGCDC dc2(dc);
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
int padding = btn->FromDIP(5);
dc2.SetPen(*wxTRANSPARENT_PEN);
@@ -2689,72 +2735,117 @@ void ColourPicker::draw_bmp_btn()
return bmp;
};
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
}
void ColourPicker::set_value(const std::string& value, bool change_event)
void ColourPicker::set_value(const boost::any& value, bool change_event)
{
set_value(boost::any(wxString::FromUTF8(value)), change_event);
}
m_disable_change_event = !change_event;
const wxString clr_str(boost::any_cast<wxString>(value));
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
{
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
const wxString clr_str = boost::any_cast<wxString>(value);
const wxColour parsed(clr_str);
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
draw_bmp_btn();
#ifdef __WXMSW__
const wxColour parsed_clr(clr_str);
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
field->SetColour(clr);
draw_bmp_btn(field, clr);
#else
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk())
set_undef_value(field);
else
field->SetColour(clr);
#endif
m_disable_change_event = false;
}
boost::any& ColourPicker::get_value()
{
m_value = m_colour == wxTransparentColour ? std::string() :
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
save_colors_to_config();
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
if (colour == wxTransparentColour)
m_value = std::string("");
else {
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
}
return m_value;
}
void ColourPicker::apply_user_color(const std::optional<wxColour>& color)
{
if (!color || !color->IsOk() || *color == m_colour) return;
const std::string previous = boost::any_cast<std::string>(get_value());
m_colour = *color;
draw_bmp_btn();
if (previous != boost::any_cast<std::string>(get_value())) on_change_field();
}
void ColourPicker::msw_rescale()
{
Field::msw_rescale();
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
if (m_opt.height >= 0)
size.SetHeight(m_opt.height * m_em_unit);
else if (parent_is_custom_ctrl && opt_height > 0)
size.SetHeight(lround(opt_height * m_em_unit));
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
if (parent_is_custom_ctrl) window->SetSize(size);
else window->SetMinSize(size);
draw_bmp_btn();
if (parent_is_custom_ctrl)
field->SetSize(size);
else
field->SetMinSize(size);
#ifdef __WXMSW__
draw_bmp_btn(field, field->GetColour());
#else
if (field->GetColour() == wxTransparentColour)
set_undef_value(field);
#endif
}
void ColourPicker::sys_color_changed()
{
if (window) wxGetApp().UpdateDarkUI(window, true);
draw_bmp_btn();
#ifdef _WIN32
if (wxWindow* win = this->getWindow())
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
draw_bmp_btn(picker, picker->GetColour());
}
#endif
}
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
{
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
ColorPickerDialog dialog(m_parent, color, {}, false, window);
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
const auto& result = std::get<ColorRGBA>(*dialog.selection());
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
void ColourPicker::on_button_click(wxCommandEvent &event) {
#if !defined(__linux__) && !defined(__LINUX__)
if (m_clrData) {
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
for (int i = 0; i < colors.size(); i++) {
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
}
}
m_picker_widget->OnButtonClick(event);
#endif
}
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
{
#if !defined(__linux__) && !defined(__LINUX__)
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
if (m_picker_widget) {
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
m_clrData = m_picker_widget->GetColourData();
}
#endif
}
void ColourPicker::save_colors_to_config() {
#if !defined(__linux__) && !defined(__LINUX__)
if (m_clrData) {
std::vector<std::string> colors;
if (colors.size() != CUSTOM_COLOR_COUNT) {
colors.resize(CUSTOM_COLOR_COUNT);
}
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
}
wxGetApp().app_config->save_custom_color_to_config(colors);
}
#endif
}
void PointCtrl::BUILD()
+24 -10
View File
@@ -22,10 +22,10 @@
#include <boost/any.hpp>
#include "I18N.hpp"
#include <wx/colour.h>
#include <wx/colourdata.h>
#include <wx/spinctrl.h>
#include <wx/bmpcbox.h>
#include <optional>
#include <wx/clrpicker.h>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp"
@@ -610,27 +610,41 @@ private:
};
class ColourPicker : public Field {
using Field::Field;
void set_undef_value(wxColourPickerCtrl* field);
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
public:
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~ColourPicker() {}
wxWindow* window{ nullptr };
void BUILD() override;
void set_value(const std::string& value, bool change_event = false);
void set_value(const std::string& value, bool change_event = false) {
m_disable_change_event = !change_event;
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
m_disable_change_event = false;
}
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void msw_rescale() override;
void sys_color_changed() override;
void enable() override { if (window) window->Enable(); }
void disable() override{ if (window) window->Disable(); }
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
wxWindow* getWindow() override { return window; }
private:
// An absent result is cancellation; transparent color is the undefined field.
void apply_user_color(const std::optional<wxColour>& color);
void draw_bmp_btn();
void on_button_click(wxCommandEvent& event);
wxColour m_colour = wxTransparentColour;
void convert_to_picker_widget(wxColourPickerCtrl *widget);
void on_button_click(wxCommandEvent &WXUNUSED(ev));
void save_colors_to_config();
private:
#if !defined(__linux__) && !defined(__LINUX__)
wxColourData* m_clrData{nullptr};
wxColourPickerWidget* m_picker_widget{nullptr};
#endif
};
class PointCtrl : public Field {
+30 -8
View File
@@ -23,6 +23,7 @@
#include <string>
#include <tuple>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
@@ -277,6 +278,19 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
return bdc.bitmap;
}
// Sort colors by HSV values (primarily by hue, then saturation, then value)
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
if (colors.size() < 2) return;
std::sort(colors.begin(), colors.end(),
[](const wxColour& a, const wxColour& b) {
ColourHSV ha = wxColourToHSV(a);
ColourHSV hb = wxColourToHSV(b);
if (ha.h != hb.h) return ha.h < hb.h;
if (ha.s != hb.s) return ha.s < hb.s;
return ha.v < hb.v;
});
}
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
{
// Check if color is transparent
@@ -416,16 +430,24 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
{
if (colors.empty()) return wxNullBitmap;
if (force_gradient && colors.size() >= 2) {
return create_gradient_filament_bitmap(colors, size);
// Make a copy to sort without modifying original
std::vector<wxColour> sorted_colors = colors;
// Sort colors by HSV when there are 2 or more colors
if (sorted_colors.size() >= 2) {
sort_colors_by_hsv(sorted_colors);
}
switch (colors.size()) {
case 1: return create_single_filament_bitmap(colors[0], size);
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
case 3: return create_triple_filament_bitmap(colors, size);
case 4: return create_quadruple_filament_bitmap(colors, size);
default: return create_gradient_filament_bitmap(colors, size);
if (force_gradient && sorted_colors.size() >= 2) {
return create_gradient_filament_bitmap(sorted_colors, size);
}
switch (sorted_colors.size()) {
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
case 3: return create_triple_filament_bitmap(sorted_colors, size);
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
default: return create_gradient_filament_bitmap(sorted_colors, size);
}
}
+35 -47
View File
@@ -1,8 +1,4 @@
#include "FilamentPickerDialog.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <utility>
#include "GUI.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -83,8 +79,7 @@ void FilamentPickerDialog::StartFlashing()
m_flash_timer->Start(50);
}
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
std::optional<FilamentColorPickerValue> ordered_color)
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
: DPIDialog(parent ? parent : wxGetApp().mainframe,
wxID_ANY,
_L("Select Filament"),
@@ -97,7 +92,6 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
m_color_query = new FilamentColorCodeQuery();
m_is_data_loaded = LoadFilamentData(fila_id);
m_cur_filament_color = fila_color;
m_ordered_color = std::move(ordered_color);
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
m_cur_color_name = new wxString(color_name);
@@ -447,9 +441,6 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
// Bind click
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
m_selection_changed = true;
m_ordered_color.reset();
m_custom_selection.reset();
m_cur_filament_color = color_code->GetFilaColor();
UpdatePreview(*color_code);
UpdateButtonStates(btn);
@@ -520,27 +511,24 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
Layout();
}
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
{
std::vector<wxColour> wx_colors = {custom_color};
// Update preview bitmap
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
if (bmp.IsOk()) {
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
m_color_demo->SetBitmap(bmp);
m_color_demo->SetBackgroundColour(wx_colors.front());
m_color_demo->SetBackgroundColour(custom_color);
m_color_demo->Refresh();
} else {
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
}
// Update preview labels for custom color
wxString label;
for (const auto& color : wx_colors) {
if (!label.empty()) label += " / ";
label += color.GetAsString(wxC2S_HTML_SYNTAX);
}
m_label_preview_color->SetLabel(label);
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
m_label_preview_idx->SetLabel("");
Layout();
}
@@ -569,6 +557,16 @@ void FilamentPickerDialog::CreateMoreInfoButton()
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
}
wxColourData FilamentPickerDialog::GetSingleColorData()
{
wxColourData data;
data.SetChooseFull(true);
if (m_cur_filament_color.ColorCount() > 0) {
data.SetColour(*m_cur_filament_color.m_colors.begin());
}
return data;
}
void FilamentPickerDialog::BindEvents()
{
// Bind mouse events for window dragging
@@ -590,37 +588,27 @@ void FilamentPickerDialog::BindEvents()
// Pause click detection while color picker is open
StopClickDetection();
FilamentColorPickerValue current;
if (m_ordered_color) {
current = *m_ordered_color;
} else {
for (const auto& color : m_cur_filament_color.m_colors)
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
}
const auto initial = filament_color_picker_initial(current);
std::optional<ColorSelection> selected;
{
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
if (dialog.ShowModal() == wxID_OK)
selected = dialog.selection();
}
StartClickDetection();
if (!filament_color_picker_changed(initial, selected))
return;
wxColourData original_data = GetSingleColorData();
wxColourData result = show_sys_picker_dialog(this, original_data);
m_custom_selection = selected;
m_ordered_color = filament_color_picker_result(*selected);
m_selection_changed = true;
std::vector<wxColour> colors;
// Resume click detection after color picker closes
StartClickDetection();
// Check if user actually selected a different color
if (result.GetColour() != original_data.GetColour()) {
wxColour selected_color = result.GetColour();
// Update m_current_filament_color with the selected color
m_cur_filament_color.m_colors.clear();
for (const auto& color : m_ordered_color->colors) {
colors.emplace_back(wxString::FromUTF8(color));
m_cur_filament_color.m_colors.insert(colors.back());
}
m_cur_filament_color.m_color_type = m_ordered_color->gradient ? FilamentColor::ColorType::GRADIENT_CLR : FilamentColor::ColorType::SINGLE_CLR;
UpdateCustomColorPreview(colors, m_ordered_color->gradient);
m_cur_filament_color.m_colors.insert(selected_color);
m_cur_filament_color.m_color_type = FilamentColor::ColorType::SINGLE_CLR;
// Update preview
UpdateCustomColorPreview(selected_color);
// Clear currently selected button since custom color selected
UpdateButtonStates(nullptr);
}
});
}
+4 -10
View File
@@ -5,10 +5,9 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "FilamentBitmapUtils.hpp"
#include "ColorPickerDialog.hpp"
#include <optional>
#include "Widgets/Button.hpp"
#include "EncodedFilament.hpp"
#include <wx/colourdata.h>
#include <wx/dialog.h>
#include <wx/string.h>
#include <wx/event.h>
@@ -26,13 +25,10 @@ namespace Slic3r { namespace GUI {
class FilamentPickerDialog : public DPIDialog
{
public:
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
virtual ~FilamentPickerDialog();
// Public interface methods
bool HasSelectionChanged() const { return m_selection_changed; }
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
bool IsDataLoaded() const { return m_is_data_loaded; }
wxColour GetSelectedColour() const;
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
@@ -72,7 +68,7 @@ private:
// UI update methods
void UpdatePreview(const FilamentColorCode& filament);
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
void UpdateCustomColorPreview(const wxColour& custom_color);
void UpdateButtonStates(wxBitmapButton* selected_btn);
// Shaped window methods
@@ -81,6 +77,7 @@ private:
// Data loading
bool LoadFilamentData(const wxString& fila_id);
wxColourData GetSingleColorData();
// Flash effect
void StartFlashing();
@@ -101,9 +98,6 @@ private:
FilamentColorCodes* m_cur_color_codes{nullptr};
wxBitmapButton* m_cur_selected_btn{nullptr};
FilamentColor m_cur_filament_color;
std::optional<FilamentColorPickerValue> m_ordered_color;
std::optional<ColorSelection> m_custom_selection;
bool m_selection_changed = false;
// Shaped window members
wxBitmap m_shape_bmp;
+91 -15
View File
@@ -65,7 +65,6 @@
#include <string_view>
#include "slic3r/GUI/Selection.hpp"
#include "GLCanvas3D.hpp"
#include "SequentialPrintClearance.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
#endif
@@ -6398,16 +6397,17 @@ void GLCanvas3D::update_sequential_clearance()
//BBS: add the height logic
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
Polygons polygons;
std::vector<std::pair<Polygon, float>> height_polygons;
polygons.reserve(instances_count);
std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
convex_and_bounding_boxes.reserve(instances_count);
std::map<ObjectID, int> print_order;
for (const PrintObject* print_object : fff_print()->objects())
for (const PrintInstance& instance : print_object->instances())
print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order);
height_polygons.reserve(instances_count);
std::vector<struct height_info> convex_and_bounding_boxes;
struct height_info
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
};
for (size_t i = 0; i < instance_transforms.size(); ++i) {
const size_t first_instance = convex_and_bounding_boxes.size();
const auto& instances = instance_transforms[i];
double rotation_z0 = instances.front()->get_rotation().z();
int index = 0;
@@ -6427,20 +6427,96 @@ void GLCanvas3D::update_sequential_clearance()
Polygon convex_hull(std::move(inst_pts));
BoundingBox bouding_box = convex_hull.bounding_box();
BoundingBox plate_bb = plate->get_bounding_box_crd();
const ObjectID instance_id = m_model->objects[i]->instances[index]->id();
double instance_height = m_model->objects[i]->get_instance_max_z(index++);
//skip the object for not current plate
if (!plate_bb.overlap(bouding_box))
continue;
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull, instance_id});
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull});
polygons.emplace_back(std::move(convex_hull));
}
sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order);
}
const auto& config = fff_print()->config();
const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height,
config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod);
//sort the print instance
std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(),
[](auto &l, auto &r) {
auto ly1 = l.bounding_box.min.y();
auto ly2 = l.bounding_box.max.y();
auto ry1 = r.bounding_box.min.y();
auto ry2 = r.bounding_box.max.y();
auto inter_min = std::max(ly1, ry1);
auto inter_max = std::min(ly2, ry2);
auto lx = l.bounding_box.min.x();
auto rx = r.bounding_box.min.x();
if (inter_max - inter_min > 0)
return (lx < rx) || ((lx == rx)&&(ly1 < ry1));
else
return (ly1 < ry1);
});
/*bool has_interlaced_objects = false;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
has_interlaced_objects = true;
break;
}
}
if (has_interlaced_objects)
break;
}*/
int bounding_box_count = convex_and_bounding_boxes.size();
double printable_height = fff_print()->config().printable_height;
double hc1 = fff_print()->config().extruder_clearance_height_to_lid;
double hc2 = fff_print()->config().extruder_clearance_height_to_rod;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
double height = (k == (bounding_box_count - 1))?printable_height:hc1;
/*if (has_interlaced_objects) {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc2)) {
height_polygons.emplace_back(std::make_pair(convex, hc2));
}
}
else {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc1)) {
height_polygons.emplace_back(std::make_pair(convex, hc1));
}
}*/
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
height = hc2;
break;
}
}
if (height < convex_and_bounding_boxes[k].instance_height)
height_polygons.emplace_back(std::make_pair(convex, height));
}
// sends instances 2d hulls to be rendered
set_sequential_print_clearance_visible(true);
+15 -108
View File
@@ -252,11 +252,9 @@
#include "slic3r/GUI/ParamsDialog.hpp"
#include "slic3r/GUI/ParamsPanel.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/host/PluginPages.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include <wx/defs.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#include <cwchar>
@@ -335,23 +333,6 @@ static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent*
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
}
// Full {web} page URL for a same-site OrcaCloud path, or empty when it cannot be built. An already
// open cloud pane is navigated straight to it, without re-running the session handoff.
static wxString cloud_page_url(NetworkAgent* agent, const std::string& path)
{
if (path.empty())
return {};
auto orca = get_orca_cloud_agent(agent);
if (!orca)
return {};
std::string base = orca->get_cloud_base_url();
while (!base.empty() && base.back() == '/')
base.pop_back();
if (base.empty())
return {};
return from_u8(base + path);
}
void start_ping_test()
{
return;
@@ -3082,37 +3063,6 @@ void GUI_App::init_plugin_gui_wiring()
refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key);
});
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_register(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_register(capability);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_deregister(capability);
Slic3r::GUI::bump_texture_library_generation();
});
// Packs already loaded before this wiring, or in tests where no callback fires.
PluginResourceTextures::instance().initial_sweep();
Slic3r::GUI::bump_texture_library_generation();
}
bool GUI_App::on_init_inner()
@@ -5484,7 +5434,12 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({});
}
else if (command_str.compare("homepage_open_cloud") == 0) {
this->open_orca_cloud();
// Deferred: set_cloud_mode() may create a second native webview, which must
// not run inside this webview's script-message callback.
CallAfter([this]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(true, cloud_webview_handoff());
});
}
else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() {
@@ -5633,22 +5588,6 @@ std::string GUI_App::handle_web_request(std::string cmd)
return "";
}
void GUI_App::open_orca_cloud(const std::string& next)
{
// Remembered so a login while the pane is open re-seeds the same page rather than the default.
m_cloud_open_path = next;
// Deferred: set_cloud_mode() may create the second native webview, so this must not run inside
// the caller's frame (a webview script-message callback or an ImGui frame).
CallAfter([this]() {
if (mainframe != nullptr)
mainframe->select_tab(TAB_ID_HOME);
// The Home page is built lazily, so the tab switch may not have created it yet: ensure() builds
// it if needed, matching what showing the tab does, rather than dropping the open request.
if (WebViewPanel* home = WebViewPanel::ensure())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path), cloud_page_url(m_agent, m_cloud_open_path));
});
}
void GUI_App::handle_script_message(std::string msg, const std::string& provider)
{
try {
@@ -5926,7 +5865,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/
}
wxString GUI_App::cloud_webview_handoff(const std::string& next)
wxString GUI_App::cloud_webview_handoff()
{
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5934,7 +5873,7 @@ wxString GUI_App::cloud_webview_handoff(const std::string& next)
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return {};
return from_u8(orca->build_web_session_start_url(next));
return from_u8(orca->build_web_session_start_url());
}
void GUI_App::request_cloud_web_session(const std::string& state)
@@ -6077,7 +6016,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
home->set_cloud_mode(true, cloud_webview_handoff());
}
@@ -6924,7 +6863,7 @@ void GUI_App::push_notification(const MachineObject* obj, wxString msg, wxStrin
}
}
bool GUI_App::reload_settings()
void GUI_App::reload_settings()
{
if (preset_bundle && m_agent) {
// Load user's personal presets
@@ -6932,7 +6871,7 @@ bool GUI_App::reload_settings()
int result = m_agent->get_user_presets(&user_presets);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync";
return false;
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
@@ -7007,30 +6946,13 @@ bool GUI_App::reload_settings()
refresh_synced_ui();
else
CallAfter(refresh_synced_ui);
return true;
}
return false;
}
void GUI_App::check_legacy_user_filament_presets(bool by_user)
{
// One check at a time: the startup check can come due while the manual one is open, and on macOS the menu bar
// stays usable while a modal dialog is up.
if (m_legacy_filament_check_running)
return;
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return;
// The local copies may be older than the cloud ones another machine changed: migrating them would push changes
// made against stale copies and conflict with the cloud. The startup check waits for the presets to be pulled.
if (!by_user && m_user_preset_pull_pending) {
m_legacy_filament_check_due = true;
return;
}
// This check also answers a startup check that is still waiting.
m_legacy_filament_check_due = false;
m_legacy_filament_check_running = true;
ScopeGuard running_guard([this] { m_legacy_filament_check_running = false; });
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) {
if (by_user)
@@ -7075,9 +6997,6 @@ void GUI_App::check_legacy_user_filament_presets(bool by_user)
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return;
}
// Ask the sync loop to push on its next pass instead of its next 60 s round, which shortens the time the migrated
// presets stay unpushed.
m_sync_user_presets_now.store(true, std::memory_order_release);
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
@@ -7844,7 +7763,6 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
// BBS
m_user_sync_token.reset(new int(0));
m_user_preset_pull_pending = true;
if (with_progress_dlg) {
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores
// repeat triggers while it is on screen (prevents stacking modal dialogs).
@@ -7918,16 +7836,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
finishFn(ret == 0);
// Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded.
if (ret == 0 && m_agent && !t.expired() && reload_settings())
CallAfter([this, t] {
// A stopped or restarted sync: the next sync, if one starts, pulls the presets again.
if (t.expired() || is_closing())
return;
m_user_preset_pull_pending = false;
if (m_legacy_filament_check_due)
check_legacy_user_filament_presets(false);
});
if (ret == 0 && m_agent && !t.expired())
reload_settings();
// For orca specific syncing
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
@@ -7942,11 +7852,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds.
while (!t.expired()) {
++tick_tock;
// Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked
// for it. The request is consumed on every tick, so one made before a scheduled pass is served by it
// instead of running a second pass straight after.
const bool push_now = m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel);
if (tick_tock % 120 == 0 || push_now) {
// Sync once immediately, then every 60s, or right away when a force-push asked for it.
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
tick_tock = 0;
if (m_agent) {
if (!m_agent->is_user_login()) {
+3 -13
View File
@@ -355,9 +355,6 @@ private:
std::atomic<bool> m_restart_sync_pending {false};
std::atomic<bool> m_sync_user_preset_dlg_active {false}; // a manual "Sync Presets" progress dialog is on screen (see restart_sync_user_preset)
std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick
bool m_user_preset_pull_pending {false}; // set when a sync thread starts, cleared once its pull is applied; a failed pull or a stopped sync leaves it set
bool m_legacy_filament_check_due {false}; // the startup check waits for that pull
bool m_legacy_filament_check_running {false}; // a check is in progress, its dialog may be open
std::atomic<bool> m_migration_retry_pending {false};
bool m_is_dark_mode{ false };
bool m_adding_script_handler { false };
@@ -368,8 +365,6 @@ private:
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
// newest request counts").
std::string m_cloud_handoff_state;
// The OrcaCloud path the open requests target, remembered so a login re-seed keeps the same page.
std::string m_cloud_open_path;
uint64_t m_cloud_handoff_gen { 0 };
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
@@ -471,9 +466,6 @@ public:
#if wxUSE_WEBVIEW_EDGE
void init_webview_runtime();
#endif
// Switches to the Home tab and opens the OrcaCloud pane beside the homepage on `next`, a
// same-site OrcaCloud path (empty = the page's default). Work is deferred internally.
void open_orca_cloud(const std::string& next = std::string());
static unsigned get_colour_approx_luma(const wxColour& colour);
static bool dark_mode();
const wxColour get_label_default_clr_system();
@@ -580,7 +572,7 @@ public:
// called from the pane when the page asks for a session; it opens the desktop login dialog if
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// in the same webview.
wxString cloud_webview_handoff(const std::string& next = std::string());
wxString cloud_webview_handoff();
void request_cloud_web_session(const std::string& state);
void mint_cloud_web_session(const std::string& state, uint64_t gen);
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
@@ -597,8 +589,7 @@ public:
void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; while the user presets sync with the cloud, it waits until they have been pulled. by_user also
// reports when there is nothing to migrate.
// reminded; by_user also reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
@@ -609,8 +600,7 @@ public:
std::string format_IP(const std::string& ip);
void show_dialog(wxString msg);
void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL);
// Returns false when the cloud presets could not be fetched, so nothing was reloaded.
bool reload_settings();
void reload_settings();
void remove_user_presets();
bool maybe_migrate_user_presets_on_login();
@@ -40,7 +40,6 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "GLGizmoUtils.hpp"
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <functional>
@@ -73,7 +72,6 @@
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
@@ -99,48 +97,6 @@ namespace {
// bigger jump than the whole useful region.
constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic);
// The scrollbar of the panel's scrolling areas (the panel body and the texture library). ImGui's stock
// scrollbar is a wide, square-cornered slab in a tinted track - against the flat panel it reads as a raw
// widget bolted onto the edge - so it is slimmed to a rounded thumb over an invisible track. The child
// background goes transparent with it, so the area reads as part of the panel.
void push_slim_scrollbar_style(float width, bool dark)
{
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, width);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * width);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
}
void pop_slim_scrollbar_style()
{
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
}
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
std::string ellipsize(std::string s, float max_w, float font_size = 0.f)
{
const auto text_width = [&](const std::string &text) {
return font_size > 0.f
? ImGui::GetFont()->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.f, text.c_str()).x
: ImGui::CalcTextSize(text.c_str()).x;
};
if (text_width(s) <= max_w)
return s;
while (!s.empty() && text_width(s + "...") > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast<unsigned char>(s.back());
s.pop_back();
} while (!s.empty() && (c & 0xC0) == 0x80);
}
return s + "...";
}
// Uploads decoded 8-bit grayscale height data as an RGBA GPU texture for display in the panel.
// Shared by the per-layer thumbnail and the picker's library entries, which differ only in where
// their pixels came from.
@@ -2965,7 +2921,6 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
// The same texture library the layer card opens; the panel renders it this frame.
m_picker_slot = m_active_layer_slot;
m_picker_open_request = true;
m_picker_reveal_selected = true;
} else if (cmd == int(Command::Unwrap)) {
// The solve runs only on this - painting, the seam angle and seam marking never trigger it.
m_uv_unwrap_pending = true;
@@ -4147,7 +4102,7 @@ void GLGizmoTextureDisplacement::set_layer_texture(TextureDisplacementLayer &lay
// the PNG the first time the layer is previewed or baked.
layer.image_data = tex->image_data;
// The user's own textures are listed most recently used first.
if (entry.source == TextureSource::User)
if (entry.is_user)
touch_user_texture(entry.path);
rebuild_preview();
@@ -4174,40 +4129,6 @@ void GLGizmoTextureDisplacement::import_custom_texture(TextureDisplacementLayer
set_layer_texture(layer, *entry);
}
void GLGizmoTextureDisplacement::ease_wheel_scroll(float &target, float &applied)
{
ImGuiIO &io = ImGui::GetIO();
const float scroll_now = ImGui::GetScrollY();
const float scroll_max = ImGui::GetScrollMaxY();
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
// content shrinking under a clamped offset, a SetScroll*() call - has to re-seed the target, or the
// easing below would immediately drag the view back to where it last animated to.
if (applied < 0.f || std::abs(scroll_now - applied) > 0.5f)
target = scroll_now;
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
target = std::floor(std::clamp(target, 0.f, scroll_max));
const float delta = target - scroll_now;
if (std::abs(delta) >= 1.f) {
// Exponential ease, formulated against the frame time so the glide takes the same wall time
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
// away, so land on the target outright once the remainder is that small.
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
const float next = std::floor(scroll_now + step);
ImGui::SetScrollY(next);
applied = next;
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
} else {
applied = scroll_now;
}
}
void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max)
{
ModelVolume *mv = texture_volume();
@@ -4228,51 +4149,13 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
const float spacing = std::round(m_imgui->scaled(0.4f));
const float grid_w = 4.f * tile + 3.f * spacing;
const float gap = m_imgui->scaled(0.5f);
const float scrollbar_w = m_imgui->scaled(0.5f);
const float ring_w = std::max(2.f, std::round(m_imgui->scaled(0.1f))); // the current texture's ring
const float frame_h = ImGui::GetFrameHeight();
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
const ImGuiStyle &style = ImGui::GetStyle();
// A fixed size rather than a fit-to-content one. The width is the four-column grid, a gutter and the
// slim scrollbar beside it, plus the window padding, so the columns always fit and no horizontal
// scrollbar is needed; it derives from scaled values, so it tracks the DPI like the rest.
const float fixed_w = grid_w + spacing + scrollbar_w + 2.f * style.WindowPadding.x;
// BeginPopup() ORs in ImGuiWindowFlags_AlwaysAutoResize (deps_src/imgui/imgui.cpp:8371) and no flag
// turns it back off. A SetNextWindowSize() overrides it for each axis it sets (imgui.cpp:5983-5996),
// so an explicit size is the only way to fix the popup's size; the grid child below then scrolls.
//
// Top-aligned with the panel, which keeps it clear of the gizmo toolbar above a docked panel, and as
// tall as the panel or 70% of the window, whichever is taller, while keeping a gap inside the window.
// The height follows the window and the panel, never the content, so it does not jump as packs load or
// sections collapse.
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const float vp_top = viewport->Pos.y + gap;
const float vp_bottom = viewport->Pos.y + viewport->Size.y - gap;
const float height = std::max(std::min(std::max(0.70f * viewport->Size.y, panel_max.y - panel_min.y), vp_bottom - vp_top),
2.f * frame_h);
const float popup_y = std::max(vp_top, std::min(panel_min.y, vp_bottom - height));
// Beside the panel rather than over it, so the layer being retextured stays in view: to its left when
// there is room (the docked panel sits against the right edge of the canvas), otherwise to its right.
const float width = viewport->Size.x > 0.f ? std::min(fixed_w, viewport->Size.x) : fixed_w;
const bool left = panel_min.x - gap - width >= 0.f;
const float popup_x = left ? panel_min.x - gap - width : panel_max.x + gap;
// Cond_Always, not Appearing: a viewport resize while the popup is open must move and resize it too.
ImGui::SetNextWindowPos(ImVec2(popup_x, popup_y), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
// Drawn as the panel's own sheet: its opaque background and rounding, no border. A popup is filled with
// PopupBg, which the panel's toolbar style leaves at ImGui's translucent near-black - a dark window
// beside a light panel, with whatever it covers showing through. Only Begin reads these, so they are
// popped at once and the tooltips shown from inside keep their own look.
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, style.WindowRounding);
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.f);
// The window itself never scrolls; the grid child below does.
const bool open = ImGui::BeginPopup(popup_id, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!open)
const float popup_w = grid_w + 2.f * ImGui::GetStyle().WindowPadding.x;
const bool left = panel_min.x - gap - popup_w >= 0.f;
ImGui::SetNextWindowPos(ImVec2(left ? panel_min.x - gap : panel_max.x + gap, panel_min.y + m_imgui->scaled(4.f)),
ImGuiCond_Appearing, ImVec2(left ? 1.f : 0.f, 0.f));
if (!ImGui::BeginPopup(popup_id))
return;
if (layer == nullptr) {
ImGui::CloseCurrentPopup();
@@ -4288,49 +4171,16 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
// The header stays put while the grid scrolls under it: the title says what the popup is for, and the
// close button is there for anyone who does not know that a click outside closes it.
bool close_clicked = false;
{
ImDrawList *hdl = ImGui::GetWindowDrawList();
const ImVec2 row = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + 0.5f * (frame_h - ImGui::GetTextLineHeight())));
m_imgui->bold_text(_u8L("Choose a texture"));
const ImVec2 btn(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x - frame_h, row.y);
ImGui::SetCursorScreenPos(btn);
close_clicked = ImGui::InvisibleButton("##close", ImVec2(frame_h, frame_h));
const bool hot = ImGui::IsItemHovered();
const ImVec2 c(btn.x + 0.5f * frame_h, btn.y + 0.5f * frame_h);
const float k = std::round(0.18f * frame_h);
const float line_w = std::max(1.f, std::round(m_imgui->scaled(0.08f)));
const ImU32 x_col = ImGui::GetColorU32(ImGuiCol_Text, hot ? 1.f : 0.6f);
if (hot)
hdl->AddCircleFilled(c, 0.5f * frame_h, ImGui::GetColorU32(ImGuiCol_Text, 0.08f));
hdl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), x_col, line_w);
hdl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), x_col, line_w);
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + frame_h + style.ItemSpacing.y));
ImGui::Separator();
}
m_imgui->push_bold_font();
m_imgui->text(_L("Choose a texture"));
m_imgui->pop_bold_font();
const std::vector<TextureLibraryEntry> &library = texture_library();
// A reinstalled pack can drop new bytes at a path already cached on this gizmo, and get_library_texture()
// caches a failure as well as a success, so a moved generation makes every cached entry suspect.
if (const uint64_t generation = texture_library_generation(); generation != m_library_generation) {
m_library_textures.clear();
m_library_generation = generation;
}
// The grid scrolls as the panel body does: the same slim scrollbar, and the wheel eased by
// ease_wheel_scroll() instead of ImGui's five-line jumps (hence NoScrollWithMouse).
push_slim_scrollbar_style(scrollbar_w, wxGetApp().dark_mode());
ImGui::BeginChild("##texture_grid", ImVec2(0.f, 0.f), false, ImGuiWindowFlags_NoScrollWithMouse);
ease_wheel_scroll(m_picker_scroll_target, m_picker_scroll_applied);
ImDrawList *dl = ImGui::GetWindowDrawList(); // the child's, so tiles clip to the grid
ImDrawList *dl = ImGui::GetWindowDrawList();
const ImU32 selected_col = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA);
std::optional<TextureLibraryEntry> chosen;
std::optional<TextureLibraryEntry> to_remove;
bool import_clicked = false;
bool download_clicked = false;
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the
@@ -4346,15 +4196,16 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
ImGui::Dummy(ImVec2(grid_w, float(rows) * tile + float(rows - 1) * spacing));
}
};
// One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
// name, the user's own most recently used first - the order texture_library() produced); a user tile
// carries a remove button on hover. Returns how many it drew.
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
// One group's tiles: shipped textures alphabetically, or the user's own most recently used first (the
// order texture_library() keeps), those with a remove button on hover. Returns how many it drew.
const auto grid = [&](const ImVec2 &origin, bool user) {
int n = 0;
for (const TextureLibraryEntry *entry : entries) {
for (const TextureLibraryEntry &entry : library) {
if (entry.is_user != user)
continue;
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
ImGui::PushID(entry->path.c_str());
const LibraryTexture *tex = get_library_texture(entry->path);
ImGui::PushID(entry.path.c_str());
const LibraryTexture *tex = get_library_texture(entry.path);
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
bool clicked = false;
if (thumb != nullptr) {
@@ -4372,27 +4223,15 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
} else {
clicked = ImGui::Button("?", ImVec2(tile, tile));
}
if (removable)
if (user)
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
const bool current = entry->path == layer->path;
const bool hovered = ImGui::IsItemHovered();
// The layer's current texture carries an accent ring, and the tile under the pointer a thinner,
// fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
if (current || hovered) {
const float w = current ? ring_w : std::round(0.5f * ring_w);
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w);
}
// Centred, as far as the scroll range allows, so the textures around it are in view too.
if (current && m_picker_reveal_selected)
ImGui::SetScrollFromPosY(0.5f * (a.y + b.y) - ImGui::GetWindowPos().y, 0.5f);
if (hovered)
panel_tooltip(entry->name);
if (entry.path == layer->path)
dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f);
if (ImGui::IsItemHovered())
panel_tooltip(entry.name);
// Clipped by the grid's clip rect, so a tile scrolled out of the grid or under its scrollbar
// does not offer its remove button. IsMouseHoveringRect() clips by default.
if (removable && ImGui::IsMouseHoveringRect(a, b)) {
if (user && ImGui::IsMouseHoveringRect(a, b)) {
const float r = std::round(tile * 0.17f);
const ImVec2 c(b.x - r - 2.f, a.y + r + 2.f);
ImGui::SetCursorScreenPos(ImVec2(c.x - r, c.y - r));
@@ -4405,162 +4244,27 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
if (hot)
panel_tooltip(_u8L("Remove from My textures"));
if (remove_clicked)
to_remove = *entry;
to_remove = entry;
}
ImGui::PopID();
if (clicked)
chosen = *entry;
chosen = entry;
}
return n;
};
// Every section collapses; the state is remembered for the app run. Pack keys use pack_name, a
// capability name unique per type across loaded packages and stable across reinstalls. This function-local
// static survives popup closes, never touches app_config, and resets on restart.
static std::map<std::string, bool> s_section_collapsed; // true = collapsed; absent = expanded
const auto split_pack_title = [](const std::string &title) -> std::pair<std::string, std::string> {
size_t split = std::string::npos;
for (size_t i = 0; i + 3 < title.size(); ++i)
if (title[i] == ' ' && (title[i + 1] == 'b' || title[i + 1] == 'B') &&
(title[i + 2] == 'y' || title[i + 2] == 'Y') && title[i + 3] == ' ')
split = i;
if (split == std::string::npos)
return { title, {} };
std::string name = title.substr(0, split);
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back())))
name.pop_back();
std::string attribution = title.substr(split);
size_t first = 0;
while (first < attribution.size() && std::isspace(static_cast<unsigned char>(attribution[first])))
++first;
attribution.erase(0, first);
if (name.empty() || attribution.empty())
return { title, {} };
return { name, attribution };
};
// A section heading, one row as wide as the grid: a click anywhere on it collapses or expands the
// section. Child headings indent their contents while keeping their counts right-aligned. Attribution
// lines use a smaller disabled style; the grid follows closer than the next heading does.
// Returns true when the section is open, so the caller knows whether to draw its contents.
const auto section_header = [&](const std::string &key, const std::string &title, size_t count, float indent,
const std::string &attribution, const std::string &full_title) {
bool &collapsed = s_section_collapsed[key]; // default-inserts false -> expanded
const ImVec2 row = ImGui::GetCursorScreenPos();
const bool two_lines = !attribution.empty();
const float line_h = ImGui::GetTextLineHeight();
const float small_font_size = ImGui::GetFontSize() * 0.8f;
const float small_line_h = line_h * 0.8f;
const float row_h = two_lines ? std::max(frame_h, line_h + small_line_h + spacing) : frame_h;
const ImVec2 row_max(row.x + grid_w, row.y + row_h);
if (ImGui::InvisibleButton(key.c_str(), ImVec2(grid_w, row_h)))
collapsed = !collapsed;
const bool hovered = ImGui::IsItemHovered();
if (hovered)
dl->AddRectFilled(row, row_max, ImGui::GetColorU32(ImGuiCol_Text, 0.05f), style.FrameRounding);
const float inset = std::round(0.25f * frame_h);
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
const float cx = row.x + inset + indent + 0.5f * arrow;
const float cy = row.y + 0.5f * row_h;
const float hs = 0.3f * arrow;
const ImU32 accent = ImGui::GetColorU32(orca);
if (collapsed)
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx + 0.5f * hs, cy), ImVec2(cx - 0.5f * hs, cy + hs), accent);
else
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs), ImVec2(cx, cy + 0.5f * hs), accent);
const std::string n = std::to_string(count);
const float n_w = ImGui::CalcTextSize(n.c_str()).x;
const float tx = row.x + inset + indent + arrow + spacing;
const float max_text_w = std::max(0.f, row_max.x - inset - n_w - spacing - tx);
const std::string shown = ellipsize(title, max_text_w);
const std::string shown_attribution = two_lines ? ellipsize(attribution, max_text_w, small_font_size) : std::string{};
const float title_y = two_lines ? row.y + 0.5f * (row_h - line_h - small_line_h)
: row.y + 0.5f * (frame_h - line_h);
const float count_y = row.y + 0.5f * (row_h - line_h);
dl->AddText(ImVec2(tx, title_y), ImGui::GetColorU32(ImGuiCol_Text), shown.c_str());
if (two_lines)
dl->AddText(ImGui::GetFont(), small_font_size, ImVec2(tx, title_y + line_h),
ImGui::GetColorU32(ImGuiCol_TextDisabled), shown_attribution.c_str());
dl->AddText(ImVec2(row_max.x - inset - n_w, count_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), n.c_str());
if (hovered && (shown != title || shown_attribution != attribution))
panel_tooltip(full_title); // a long plugin-supplied name, cut by the fixed width
ImGui::SetCursorScreenPos(ImVec2(row.x, row_max.y + spacing));
return !collapsed;
};
// Built-in is first, plugin packs are grouped under Texture libraries, and My
// textures stays last so its import tile and help line describe that group. Pack names identify both
// their child section and state.
struct Section
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
{
std::string key;
std::string title;
std::vector<const TextureLibraryEntry *> entries;
};
std::vector<Section> sections;
std::map<std::string, size_t> pack_section; // pack_name -> index in `sections`
sections.push_back({ "builtin:", _u8L("Built-in"), {} });
for (const TextureLibraryEntry &entry : library) {
if (entry.source == TextureSource::Shipped) {
sections.front().entries.push_back(&entry); // the section exists even when the library is empty
continue;
}
if (entry.source == TextureSource::User)
continue; // added below, so My textures stays last
if (entry.source != TextureSource::Plugin)
continue;
const auto [it, inserted] = pack_section.emplace(entry.pack_name, sections.size());
if (inserted)
sections.push_back({ "pack:" + entry.pack_name, entry.pack_name, {} });
sections[it->second].entries.push_back(&entry);
}
sections.push_back({ "user:", _u8L("My textures"), {} });
for (const TextureLibraryEntry &entry : library)
if (entry.source == TextureSource::User)
sections.back().entries.push_back(&entry);
const float child_indent = std::round(m_imgui->scaled(0.8f));
const Section &built_in = sections.front();
if (section_header(built_in.key, built_in.title, built_in.entries.size(), 0.f, {}, built_in.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, built_in.entries, false);
const int count = grid(origin, false);
end_grid(origin, count);
if (count == 0)
ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
}
size_t subscribed_count = 0;
for (size_t i = 1; i + 1 < sections.size(); ++i)
subscribed_count += sections[i].entries.size();
const std::string subscribed_title = _u8L("Texture libraries");
if (section_header("subscribed:", subscribed_title, subscribed_count, 0.f, {}, subscribed_title)) {
for (size_t i = 1; i + 1 < sections.size(); ++i) {
const Section &pack = sections[i];
const auto [name, attribution] = split_pack_title(pack.title);
if (!section_header(pack.key, name, pack.entries.size(), child_indent, attribution, pack.title))
continue;
ImGui::TextDisabled("%s", _u8L("My textures").c_str());
{
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, pack.entries, false);
end_grid(origin, count);
}
ImGui::Dummy(ImVec2(0.f, spacing));
const std::string download_label = _u8L("Explore more texture libraries");
if (ImGui::Button(download_label.c_str(), ImVec2(grid_w, frame_h)))
download_clicked = true;
if (ImGui::IsItemHovered())
panel_tooltip(_u8L("Opens OrcaCloud in the Home page to browse for plugins."));
}
const Section &user = sections.back();
if (section_header(user.key, user.title, user.entries.size(), 0.f, {}, user.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, user.entries, true);
const int count = grid(origin, true);
ImGui::SetCursorScreenPos(tile_pos(origin, count));
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
import_clicked = true;
@@ -4569,25 +4273,17 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
"bakes from and saved to your personal texture folder, kept separate from the "
"textures shipped with OrcaSlicer."));
end_grid(origin, count + 1);
// Belongs to this section: it describes the import tile and the remove-on-hover tiles, both of
// which live here, so it is hidden with them rather than left describing controls off-screen.
}
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::PopTextWrapPos();
}
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
m_picker_reveal_selected = false;
ImGui::EndChild();
pop_slim_scrollbar_style();
if (chosen || import_clicked || close_clicked || download_clicked)
if (chosen || import_clicked)
ImGui::CloseCurrentPopup();
m_tooltip_anchor_min = prev_anchor_min;
m_tooltip_anchor_max = prev_anchor_max;
ImGui::EndPopup();
// The OrcaCloud plugin hub filtered to resource textures; the pane's session handoff redirects here.
if (download_clicked)
wxGetApp().open_orca_cloud("/app/plugins/plugin-hub?types=resource-textures");
// Only once the loop over the library is done: importing, picking (which reorders the recently used)
// and removing all change the library the loop was iterating.
@@ -6890,6 +6586,19 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->text(text);
m_imgui->pop_bold_font();
};
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
const auto ellipsize = [](std::string s, float max_w) {
if (ImGui::CalcTextSize(s.c_str()).x <= max_w)
return s;
while (!s.empty() && ImGui::CalcTextSize((s + "...").c_str()).x > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast<unsigned char>(s.back());
s.pop_back();
} while (!s.empty() && (c & 0xC0) == 0x80);
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) {
@@ -7082,11 +6791,52 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
style.WindowPadding.y - style.ItemSpacing.y);
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
push_slim_scrollbar_style(m_imgui->scaled(0.5f), dark);
// NoScrollWithMouse: ease_wheel_scroll() eases the scroll instead of teleporting five text lines per
// notch, which on blocks this tall lost the reader's place.
// ImGui's stock scrollbar is a wide, square-cornered slab in a tinted track - against this flat panel
// it reads as a raw widget bolted onto the edge. Slim it to a rounded thumb over an invisible track.
const float scrollbar_w = m_imgui->scaled(0.5f);
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
// NoScrollWithMouse: the wheel is handled below so the scroll can be eased instead of teleporting
// five text lines per notch, which on blocks this tall lost the reader's place.
ImGui::BeginChild("##td_body", ImVec2(panel_w, body_h), false, ImGuiWindowFlags_NoScrollWithMouse);
ease_wheel_scroll(m_panel_scroll_target, m_panel_scroll_applied);
{
ImGuiIO &io = ImGui::GetIO();
const float scroll_now = ImGui::GetScrollY();
const float scroll_max = ImGui::GetScrollMaxY();
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
// content shrinking under a clamped offset - has to re-seed the target, or the easing below would
// immediately drag the view back to where it last animated to.
if (m_panel_scroll_applied < 0.f || std::abs(scroll_now - m_panel_scroll_applied) > 0.5f)
m_panel_scroll_target = scroll_now;
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
m_panel_scroll_target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
m_panel_scroll_target = std::floor(std::clamp(m_panel_scroll_target, 0.f, scroll_max));
const float delta = m_panel_scroll_target - scroll_now;
if (std::abs(delta) >= 1.f) {
// Exponential ease, formulated against the frame time so the glide takes the same wall time
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
// away, so land on the target outright once the remainder is that small.
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
const float next = std::floor(scroll_now + step);
ImGui::SetScrollY(next);
m_panel_scroll_applied = next;
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
} else {
m_panel_scroll_applied = scroll_now;
}
}
// Everything that changes the layer list is deferred until the list is no longer being drawn:
// removing or reordering shifts mv->texture_displacement_layers under the pointers the loop holds,
@@ -7333,7 +7083,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const auto open_picker = [&](int slot) {
m_picker_slot = slot;
m_picker_open_request = true;
m_picker_reveal_selected = true;
};
// The six-dot drag icon, drawn into `list` over [x, x + w] x [y, y + h].
const auto draw_drag_icon = [&](ImDrawList *list, float x, float y, float w, float h, ImU32 tint) {
@@ -8328,7 +8077,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// height of everything above, whether or not it is scrolled.
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
ImGui::EndChild();
pop_slim_scrollbar_style();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
// The layer-list changes deferred from the loop above.
if (slot_to_remove >= 0)

Some files were not shown because too many files have changed in this diff Show More