Compare commits

..
Author SHA1 Message Date
Hanif Koh f307785cad Drop Cached Bytecode Through the Interpreter in the Module Release Test 2026-10-16 19:18:02 +08:00
Hanif Koh 6bc835ea30 Keep Python Bytecode out of the Bundled Python Home
The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.
2026-10-16 16:23:51 +08:00
HanifKoh 29b6bbb8c9 Keep Slice and Print Visible When Plugin Page Tabs Overflow (#16541) 2026-10-16 15:39:02 +08:00
Lam Wei Lun 8b6c074c69 Fix publish dialog tab clipping and extruder switch distortion (#16543)
* Fix TabCtrl clipping titles and indicator dots

Button measures a label plus icon without the icon gap that render draws,
so icon tabs were ellipsized and the indicator dot was cut off. Widen the
horizontal padding so the measured width covers the gap.

* Re-layout publish dialog when extruder switch labels change

Setting the indicator bullet on a switch option changed its width without
laying out the page, leaving the switch distorted until the next layout.
2026-10-16 15:37:17 +08:00
Error404JoyNotFound 109d09e0b8 Fix multi-tool standby heaters by turning off unused extruders (#15849)
With ooze prevention on, an extruder that has no layers left is set to 0 °C when it is parked, instead of its standby temperature. On IMEX printers where several filaments share one heater, the heater is only turned off once every filament on it is done.
2026-10-15 16:35:53 -05:00
Matias Alejandro Yocca b00237807d Fix crash when dragging objects in sequential printing mode (#16029)
The clearance preview shown while dragging sorted instances with a comparator that is not a strict weak ordering, which aborts with libc++ debug hardening. The preview now uses object-list order, with each object's copies in their last validated print order, which is the order print validation uses. The height check is now in SequentialPrintClearance.hpp, with unit tests.
2026-10-15 16:13:28 -05:00
mosfet80 f6cf18fb96 Update libjpeg-turbo to 3.2.0 (#16098)
Also removes the Linux-only JPEG_VERSION block, so Linux builds libjpeg with the default 6.2 ABI like Windows and macOS.
2026-10-15 12:47:21 -05:00
SoftFever 4ee1b9c6e9 Add a ResourceTexture plugin capability for height-map texture packs (#16473)
# Description

Adds a fifth Python plugin capability type, `ResourceTexture`, which
contributes
height-map textures to the Texture Displacement picker. A plugin returns
a
directory of images; they appear as selectable textures in the picker,
grouped
under one collapsible section per pack. Disabling or uninstalling a pack
removes
them again, and projects that already use one of its textures keep
working,
because a layer holds its own image bytes.

**The plugin-facing contract** is one method:

```python
class WoodPack(orca.resource_texture.ResourceTexturePluginCapabilityBase):
    def get_name(self):
        return "Wood Pack"
    def get_texture_dir(self):
        return os.path.join(os.path.dirname(__file__), "textures")
```

The host scans one non-recursive level of that directory, accepting the
same
formats a hand-picked image already accepts (`.png/.jpg/.jpeg/.bmp`),
and decodes
them through the same `load_texture_image_data()` the picker uses for
the user's
own images, so a pack texture and a hand-picked one behave identically.

**Distribution is `.whl` only for v1.** `install_plugin` copies exactly
one
file, so a `.py` plugin cannot ship sibling images; a wheel is a single
file and
the loader already extracts it on demand.

**Files of interest**

- `src/slic3r/plugin/pluginTypes/resourceTexture/` — the capability, its
  trampoline and its pybind base, mirroring the Pages type.
- `src/slic3r/plugin/host/PluginResourceTextures.{hpp,cpp}` — host
registry,
  free of wx and GUI includes so it can sit beside `PluginManager`.
- `src/slic3r/GUI/TextureLibrary.{hpp,cpp}` — the third texture source,
the
  `pack_name` provenance on an entry, and a generation counter.
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp` — the picker.
- `src/slic3r/plugin/PluginFsUtils.{hpp,cpp}` —
`collect_resource_texture_files()`.

**Behaviour change worth a reviewer's attention:** the picker popup is
now a
fixed size that scrolls rather than sizing itself to its content, and it
gains
one collapsible section per pack. The sizing applies to every user, not
only
those with packs installed. With no packs installed the picker otherwise
behaves
exactly as before.

**Not in scope for v1:** folder or `.zip` install for non-programmers,
and
preset-referenceable pack textures (which would need a plugin-backed
`ConfigOptionDef::plugin_type`).

# Screenshots/Recordings/Graphs


https://github.com/user-attachments/assets/b96787cf-b475-4698-826d-5db8b9f386f1

## Tests

New suite `tests/slic3rutils/test_plugin_resource_texture.cpp`, 19
cases:

- Validator: a file inside the root is listed; a parent-directory escape
and an
absolute directory outside the root are refused; a symlink pointing
outside is
skipped; files in a subdirectory are not listed; non-regular entries are
skipped; the extension whitelist is case-insensitive and rejects
near-misses;
  the listing stops at 64 files; a missing directory yields an error.
- Listing and lifecycle: a pack's images appear and clear on disable and
unload;
an empty pack contributes nothing; two distinctly named packs each stay
one
contiguous run; a second pack declaring a loaded capability name is
refused;
  reinstalling with different bytes at the same path changes the decoded
  content.
- Hostile overrides: `get_texture_dir()` returning `data_dir()` or
`resources_dir()` is refused and the pack loads with no textures; a
raising
  override still loads the pack.
- Decoding: PNG and BMP from a pack decode through the same path as the
same
  files picked by hand.
- Persistence: a project keeps a pack texture after the pack is
uninstalled,
  bytes unchanged across a re-save.
- Wheel metadata: a minimal resource-texture wheel is accepted.

Known gaps, for transparency: the wheel path is covered only at the
metadata
gate, not through a full extraction-and-import lifecycle; there is no
test for a
registry callback after interpreter shutdown, as there is no seam for it
without
reaching into `src/`; and `.jpg` decode parity is covered by the listing
rather
than by a real fixture, since no JPEG encoder is available to synthesise
one.

Manually exercised in-app on Linux with a locally built pack wheel (five
colour
PNGs): install, enable, textures listed under the pack's own section,
then
disable and uninstall remove them. The new suite itself has not been run
locally — CI is the first execution.
2026-10-16 00:35:30 +08:00
SoftFever bc8d0a7858 Wait for the cloud preset sync before offering to migrate filament presets (#16523) 2026-10-16 00:27:33 +08:00
SoftFever e0651ec31d Let ccache cache precompiled-header compiles in Linux builds (#16519) 2026-10-16 00:09:08 +08:00
Lam Wei LunandSoftFever 0caefcf4aa Linux: only disable WebKit compositing for WebKitGTK older than 2.42 (#16385)
* Linux: only disable WebKit compositing for WebKitGTK older than 2.42

The default Wayland path set WEBKIT_DISABLE_COMPOSITING_MODE=1 whenever the
session had both DISPLAY and WAYLAND_DISPLAY, i.e. any Wayland session with
XWayland. That forces WebKitGTK accelerated compositing off and also disables
threaded scrolling, so the Device tab web view (Mainsail/Fluidd) lagged unless
the app was launched with GDK_BACKEND=x11, the only path that skipped the
variable. Disabling compositing also turns off threaded scrolling.

The XWayland compositing bug the fallback worked around is fixed in WebKitGTK
>= 2.42, so apply it only for older WebKit. WebView::NeedsCompositingWorkaround()
reads the runtime WebKitGTK version; the X11 opt-in path is unchanged.

Behavior is unchanged on WebKit < 2.42, on pure X11, and on native Wayland
without XWayland. The wxWidgets skill reference is updated to match.

* Linux: use WebKit's shared-memory transport for the NVIDIA DMABUF renderer

The Device tab web view is slow on NVIDIA proprietary drivers even with
accelerated compositing left on: since the DMABUF renderer became the
default in WebKitGTK 2.42, the driver cannot back it, and web views fall
back to a slow path. Gating the old WEBKIT_DISABLE_COMPOSITING_MODE
fallback on WebKit < 2.42 is the wrong axis, because the failure is
version-independent from 2.42 on.

Force the shared-memory transport with WEBKIT_DMABUF_RENDERER_FORCE_SHM=1.
That keeps compositing and threaded scrolling and skips only the hardware
path. Also set WEBKIT_FORCE_DMABUF_RENDERER=1, which clears the isNVIDIA()
early return that Debian/Ubuntu/openSUSE patch into
rendererBufferTransportMode() and which otherwise returns before the
FORCE_SHM check is reached.

Both are set before the X11-vs-Wayland split so the workaround also covers
XWayland, and only when WebKitGTK is new enough to honour FORCE_SHM
(>= 2.44, WebView::SupportsForceSharedMemory()).

The AppImage launcher exported the superseded
WEBKIT_DISABLE_DMABUF_RENDERER=1, which is read first, leaves the transport
set empty and so disables compositing outright; replace it with the same
pair. The wxWidgets skill reference is updated to match.

* Linux: drop the AppImage WebKit DMABUF exports; correct the skill notes

The launcher cannot gate on the runtime WebKitGTK version, so exporting
WEBKIT_DMABUF_RENDERER_FORCE_SHM/WEBKIT_FORCE_DMABUF_RENDERER there would
clear a distro isNVIDIA() guard on hosts older than 2.44, where FORCE_SHM
is ignored, re-exposing the broken hardware path. The binary already sets
both with the correct gate, so remove them and say who owns the workaround.

Also correct two claims in the skill reference. An empty transport set only
means "no accelerated compositing" from 2.44 on; 2.42/2.43 still fall back
to the Wayland/X11 renderers, removed in the same release that added
FORCE_SHM. And threaded scrolling is off by default in 2.44/2.45 (on from
2.46), so FORCE_SHM keeps compositing rather than scrolling. Document the
module-level detection cost and the WEBKIT_DMABUF_RENDERER_FORCE_SHM=0
opt-out.

* Flatpak: force WebKit's shared-memory transport instead of disabling DMABUF

The entrypoint exported WEBKIT_DISABLE_DMABUF_RENDERER=1 for NVIDIA. WebKit
reads that variable first and leaves the transport set empty, which turns
accelerated compositing off, so the Device tab still lagged for NVIDIA
Flatpak users, and the binary could not override it because its setenv is
non-replacing.

The runtime's WebKitGTK is vanilla, so there is no distro isNVIDIA() guard
to clear and WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 alone is the right switch:
compositing stays on and only the hardware DMABUF path is skipped.

* Flatpak: drop the WebKit NVIDIA export; the binary owns the workaround

/proc/driver/nvidia/version is readable inside the Flatpak sandbox, so the
binary's NVIDIA branch already sets WEBKIT_DMABUF_RENDERER_FORCE_SHM with
the correct runtime gate and the entrypoint's own export is redundant.

Verified with:
  flatpak run --command=cat com.orcaslicer.OrcaSlicer /proc/driver/nvidia/version
which prints the NVRM version (driver 615.71.09) from inside the sandbox.

* Linux: say what the XWayland compositing gate actually tests

DISPLAY and WAYLAND_DISPLAY are both set in a native Wayland session as
well, because GNOME and KDE run Xwayland as part of the session and export
DISPLAY for it whether or not this process is an X client. The pair means
XWayland is available in the session, not that GTK picked X11 for this
process, so "(an XWayland session)" reads as more than the condition
establishes.

Say what the pair means, and name the case the fallback actually covers:
the X11 backend inside a Wayland session, which is what the forced
GDK_BACKEND=x11 above produces in a build without EGL.

Comment only; behaviour is unchanged.

* Linux: apply the XWayland compositing fallback only where GTK picks X11

The default path set WEBKIT_DISABLE_COMPOSITING_MODE whenever DISPLAY and
WAYLAND_DISPLAY were both set, but a native Wayland app inherits both, so a
build with EGL disabled accelerated compositing — and with it threaded
scrolling — while running on Wayland, where the XWayland bug does not apply.

The only default-path case that really runs through XWayland is the forced
GDK_BACKEND=x11 used when wx lacks EGL, so set the variable there. The
backend cannot be re-checked later: that branch exports GDK_BACKEND itself,
so the force and a user's GDK_BACKEND=x11 opt-in, which deliberately keeps
compositing, would be indistinguishable.

Behaviour is unchanged for WebKitGTK >= 2.42, where the gate already skipped
the variable, and for builds without EGL. A build with EGL on WebKitGTK <
2.42 in a Wayland session no longer loses compositing and threaded scrolling.

The wxWidgets skill reference is updated to match.

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-15 22:35:56 +08:00
SoftFever 86905a8a20 Use a keyboard icon for the shortcut button and fix unreadable guide tooltips (#16515)
* Fix unreadable text in wiki and video guide tooltips

* Use a keyboard icon for the gizmo shortcut button

* Fix blank tooltips on the Move and Scale gizmo combo boxes
2026-10-15 20:28:13 +08:00
Noisyfox d4fabbf7c8 Fix AMS gradient rendering, color presets and printer writeback (#16252)
* Fix external spool gradient and multicolor rendering

* Display AMS gradients and multicolors in color popup

* Preserve AMS gradient selection through color popup

* Match popup selections by full filament color data

* Align AMS Lite gradient geometry with BambuStudio

* Align AMS color palette with BambuStudio presets

* Match Bambu circular color rendering without background masks

* Preserve AMS gradient colors when applying filament settings

* Match AMS preview gradient swatches to the solid color shape
2026-10-15 19:43:07 +08:00
SoftFever 51efb24752 Complete traditional Chinese translations (#16513) 2026-10-15 19:32:54 +08:00
SoftFever cf9ec18df5 Complete the Simplified Chinese translations (#16512) 2026-10-15 19:32:30 +08:00
Lam Wei Lun 99de46b9d0 Rename the texture picker's pack section and OrcaCloud shortcut
Use "Texture libraries" for the plugin pack group and "Explore more
texture libraries" for the button that opens the OrcaCloud plugin hub.
2026-10-15 19:19:28 +08:00
Tobias Glothandyw4z 258f927a0f Fix: Crash by null mainframe in BackgroundSlicingProcess for CLI (#15367)
* add nullptr check

* use plater from app instead mainframe

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-15 14:14:26 +03:00
Lam Wei Lun b8d40ed61f Load the OrcaCloud plugin page directly when the pane is already open
The texture picker's Download from OrcaCloud shortcut asks for a specific page. Re-running the
session handoff to reach it flashed the sign-in interstitial every time; instead an already-open
pane is navigated straight to the page URL, keeping its session. The handoff is still used on the
first open, where a session has to be established.

set_cloud_mode() gains an optional page_url for this. The homepage nav passes none, so its ordinary
re-open keeps the page the pane already holds (guide §8/§10).
2026-10-15 18:46:30 +08:00
Arthur Porto 6aa613ccf0 Fix Z contouring failing on objects with several instances (#16461)
* Let Z contouring slice objects that have several instances

The contouring step refused any object with more than one instance. The
instances of a PrintObject differ only by their XY shift, so the mesh is now
placed with the first instance of the PrintObject being contoured.

* Say in the Z contouring comment how instances map to print objects

* Check that Z contouring follows each instance as it is turned
2026-10-15 07:30:57 -03:00
Lam Wei Lun 54e6df0235 Group texture packs under one picker section and add a Download from OrcaCloud shortcut
The texture picker lists every plugin pack as an indented, collapsible child of one
"Resource Texture" section, which now always renders so the new button is reachable even
before any pack is installed. A pack whose name carries a " by ..." suffix shows that part
on a smaller, dimmed second line, keeping the name line short.

The button switches to the Home tab and opens the OrcaCloud pane on the plugin hub filtered
to resource textures. A native open now also drives the homepage shell (nav highlight and the
cloud-mode layout), and the requested page is remembered so a sign-in re-seed keeps it.

Also fixes the PluginsDialog fuzzy-search script path, which pointed at a directory the file
is not in.
2026-10-15 18:10:33 +08:00
SoftFever 7599868797 Update locale 2026-10-15 17:41:23 +08:00
Lam Wei Lun 81ec931990 Keep global shortcuts out of in-canvas ImGui text fields
Space opened the Speed Dial and Ctrl+P the Preferences while the text tool's
box or a sketch value field was being typed into. Those fields are ImGui drawn
on the GL canvas, so the canvas is the focused wx window and focus_keeps_space()
cannot see them; the CHAR_HOOK handler took the chord first and the field never
saw the key.

Skip the global shortcut dispatch while ImGui reports that one of its items owns
the keyboard and the focused window is not a wxControl.
2026-10-15 17:20:56 +08:00
Volodimer BabchynskyyandSoftFever 8fd97b5120 Pass EXTRUDER_OTHER_LAYER_TEMP in RatRig single-toolhead start G-code (#16444)
RatOS 2.1 START_PRINT aborts when the parameter is missing. The IDEX
profiles already pass it.

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-15 16:57:47 +08:00
Paul aacc36b9d2 Add official School House Spools PLA and PETG filament profiles (#15429)
# Description

Adds School House Spools PLA and PETG to the global Orca Filament
Library. Both are standard PLA and PETG, so Bambu PLA Basic / PETG Basic
served as the baseline for tuning values. The one intentional difference
is PETG nozzle temperature (252°C vs Bambu's inherited 255°C default),
based on our own testing.

# Screenshots/Recordings/Graphs

N/A

## Tests

Ran scripts/assign_vendor_setting_ids.py to generate setting_id values,
and scripts/orca_extra_profile_check.py --vendor="OrcaFilamentLibrary"
--check-filaments --check-materials to validate. No new errors or
warnings introduced (pre-existing unrelated errors in other vendors'
profiles were not touched).

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-15 16:36:21 +08:00
SoftFever 79ed246d26 Refresh the texture picker's look and scrolling 2026-10-15 15:34:05 +08:00
SoftFever 92b5301bf6 Fix trackpad scroll bounce in ImGui panels 2026-10-15 15:34:05 +08:00
Noisyfoxandyw4z 26f5fd6251 Add a themed WebView color picker with persistent favorites (#16197)
* Define validated color picker selection contract

* Integrate web color picker with gradient and alpha controls

Co-authored-by: yw4z <ywsyildiz@gmail.com>

* Apply shared Orca theme to web color picker

* Host color picker in a validated WebView dialog

* Persist color picker favorites in user preferences

* Integrate localized web color picker with filament actions

* Add color picker browser regressions and verify direct includes

* Fit color picker content in a borderless floating panel

* Anchor color picker panels to their triggering controls

* Use accent foreground for selected color picker switches

* Integrate web color picker into generic color fields

* Use web color picker for AMS custom colors

* Use shared web color picker from Linux sidebar menu

* Use web color picker when adding texture import material

* Preserve alpha in filament color picker actions

* Clean up color picker asset whitespace

* Use application translations for color picker validation

* Preserve existing formatting in color picker integration

* Match SpeedDial rounded window handling on macOS

* Align color picker host behavior with SpeedDial

* Consolidate color picker implementation into its dialog

* Remove native color picker fallback

* Limit color picker interfaces to production consumers

* Use shared WebView translation field name

* Reapply anchored color picker placement after GTK show

* update

* fix increment buttons not working on spin inputs

* Use thumb color from its own channel

* init gradients with shifted color for second color

* fix hsl sliders cannot move freely and lock downs other one

* unify save button and color preview & increase stored color count to 32

* switch to hsl based calculation and fix plot and slider drag events

* update

* add support for multi colored gradients

* Update test_filament_color_picker.cpp

* add filtering color palette and store sidebar and combo state

* open dialog with calculated size

* apply shadow to dialog on windows

* add tooltip for gradient add color button

* Restore draggable fixed-size color picker frame

* Fix color picker sidebar sizing in WebKit

* Fix invisible default filament color button on macOS

* Remove duplicated color defs for color picker

* Remove duplicate HTML color picker frame

* Match color picker tests to current theme roles

* update icons

* use focus color on background of filter box

* align gradient switch with hex input

* fix initial window sizing

* Preserve caller color order when drawing filament icons

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-15 15:29:49 +08:00
SoftFever 20c157b0f4 Merge branch 'main' into feature/resource-texture-pack-plugins 2026-10-15 13:57:38 +08:00
Lam Wei Lun 4cf16febf6 Fix plugin unit test 2026-10-14 18:23:32 +08:00
Lam Wei Lun 70e52a105f Fix clang tidy 2026-10-14 17:38:52 +08:00
Lam Wei Lun b525cb79e1 Merge main 2026-10-14 17:31:35 +08:00
Lam Wei Lun 86ab598899 Add a ResourceTexture plugin capability for height-map texture packs
A fifth Python plugin capability type, ResourceTexture, contributes
height-map textures to the Texture Displacement picker. A pack returns a
directory of images; the host validates it once at load and lists the
images under a section of their own.

- New capability type ResourceTexture, type string "resource-texture",
  exposed to plugin authors as
  orca.resource_texture.ResourceTexturePluginCapabilityBase with one
  required method, get_texture_dir().
- The directory is resolved once on the load worker, under the GIL, and
  cached as a plain string: the picker runs inside the ImGui frame, where
  a trampoline call would block on the GIL behind a load/unload worker, or
  throw past EndPopup.
- Validation resolves the directory and requires containment in the plugin
  root, re-checked per file so a symlink inside the pack cannot resolve
  outside it. Containment is the only security control here and it guards
  exfiltration: layer bytes are embedded in every saved .3mf, so a pack
  that could return ~/.ssh/id_rsa would leak it through any shared project.
- New host registry PluginResourceTextures, filled from both the plugin
  and the capability lifecycle callbacks plus a startup sweep. It holds
  strings only and never a capability reference, so nothing outlives
  Py_Finalize.
- The picker lists pack textures read-only, one collapsible section per
  pack, in a fixed-size scrolling popup whose height follows the window. A
  generation counter invalidates the cached listing, and each gizmo's
  cached thumbnails, when the set of packs changes.
- Tests in tests/slic3rutils/test_plugin_resource_texture.cpp cover the
  validator (parent-directory and absolute escapes, an escaping symlink,
  the extension whitelist, the 64-file cap), the listing and its
  load/disable/re-enable/unload lifecycle, hostile get_texture_dir()
  overrides, decode parity with hand-picked files, and a .3mf round trip
  with the pack uninstalled.

No persistence work is needed: a layer carries its own image bytes and 3MF
writes them into the archive, so uninstalling a pack leaves existing
projects intact.

Behaviour change worth noting for review: the picker popup is now a fixed
size that scrolls, rather than sizing itself to its content, and it gains
one collapsible section per pack. That sizing applies to every user, not
only those with packs installed.
2026-10-14 17:15:39 +08:00
149 changed files with 48268 additions and 7272 deletions
@@ -569,12 +569,18 @@ on the **first** call. So:
#endif
```
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
Wayland app inherits both). It calls `XInitThreads()`
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
**wx's own detectors**, for reference:
@@ -61,7 +61,8 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond the no-EGL default path where Orca forces
`GDK_BACKEND=x11`, for WebKitGTK older than 2.42, and never
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
@@ -567,33 +568,69 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
branches on `GDK_BACKEND`:
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
`DISPLAY` is set.
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
(`WebView::NeedsCompositingWorkaround()`). That forced X11 backend in a Wayland session is the only
default-path case that really runs through XWayland; `DISPLAY` and `WAYLAND_DISPLAY` alone do not
identify it, because a native Wayland app inherits both. `XInitThreads()` runs only when `DISPLAY` is
set.
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
(see `references/platforms.md`), not these environment variables.
**Pitfall.**
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
initialises; never unconditionally. Keep JS-side fallbacks so a page never
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
process, before the first WebView.
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
```cpp
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
// Right: keep compositing, skip only the hardware DMABUF path
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
}
```
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path, in the branch where wx
lacks EGL and Orca forces `GDK_BACKEND=x11`, non-replacing (`setenv(..., false)`, so a value the user
already exported wins), before GTK
initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
waits forever for a C++→JS call.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware
acceleration; without the JS timeout the user is stuck when the signal is lost.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
```cpp
// Wrong: unconditional, kills HW acceleration everywhere
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland
if (display && *display && wayland && *wayland)
// Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
// and only in the branch that forces GDK_BACKEND=x11 (wx without EGL), the
// case that runs through XWayland
if (WebView::NeedsCompositingWorkaround() && wayland && *wayland)
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
```
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
+15 -1
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: boost ccache hit rate by disabling precompiled headers (default: ON)"
echo " -p: disable precompiled headers (on by default): more ccache hits after header changes, slower misses"
echo " -r: skip RAM and disk checks (low RAM compiling)"
echo " -s: build the Orca Slicer (optional)"
echo " -t: build tests (optional), requires -s flag"
@@ -549,6 +549,20 @@ fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
export CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
fi
+2 -17
View File
@@ -1,22 +1,7 @@
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
if (JPEG_VERSION STREQUAL "6")
message("Using Jpeg Lib 62")
set(jpeg_flag "")
elseif (JPEG_VERSION STREQUAL "7")
message("Using Jpeg Lib 70")
set(jpeg_flag "-DWITH_JPEG7=ON")
else ()
message("Using Jpeg Lib 80")
set(jpeg_flag "-DWITH_JPEG8=ON")
endif ()
endif()
orcaslicer_add_cmake_project(JPEG
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
DEPENDS ${ZLIB_PKG}
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
CMAKE_ARGS
-DENABLE_SHARED=OFF
-DENABLE_STATIC=ON
${jpeg_flag}
)
+131
View File
@@ -0,0 +1,131 @@
# Color picker
The color picker is a local HTML dialog hosted by `ColorPickerDialog`, a
`WebViewHostDialog` subclass. Its header and implementation own the ordered
selection contract and shared filament conversion. Bridge validation, favorites
persistence, placement and gettext strings remain private to the implementation.
Its colors use the existing `ColorRGBA` utility.
It uses a native title bar and fixed window border, allowing dragging but no
manual resizing or maximization. Native title-bar colors use the application's
shared dialog dark-theme update on creation and theme changes.
Like SpeedDial, the host disables browser zoom accelerators, restores native
WebView focus on activation and readiness, and synchronizes layout and the macOS
viewport before repainting the backend. Activation never dismisses this modal
dialog; focus restoration preserves an active editor and its partial input.
The initialized page reports
its intrinsic content height; native code validates the current page identifier
and bounded numeric height, converts CSS pixels to device-independent window
size, and fits within the display's work area. Identical sizes are ignored. The
page observes content rather than viewport height, so resizing does not create
a feedback loop; oversized content remains scrollable.
The panel snapshots the triggering color or More Colors button's screen rectangle.
It aligns below the button, flips above when needed, and clamps to that button's
display work area on initial display. Later content and DPI changes update only
the client size, preserving the position chosen by dragging the title bar.
wxGTK clears the dialog's default
parent-centering constraint and reapplies its native position after showing,
bypassing wxGTK's requested-position cache. Absolute top-level placement remains
subject to the window manager; Wayland compositors control it. The official filament palette's
existing placement is independent of this custom panel.
A solid selection contains one color; a gradient contains exactly two ordered
endpoints. The bridge represents these as `{"type":"solid|gradient",
"colors":["#RRGGBBAA", ...]}`. RGB input is also accepted and becomes opaque.
Native validation rejects malformed hex and incorrect cardinality.
The project filament color action and the official filament picker's More Colors
action enable two-endpoint gradients and independent endpoint alpha. The preset
combo box reads project colors in their stored vector order locally and applies
changes through `sync_colour_config`,
retaining the existing RGB/RGBA profile/project formats and dirty marking.
Sidebar gradient and segmented multicolor swatches draw colors in the supplied
order without sorting them.
Opaque colors use `#RRGGBB`; nonopaque colors use `#RRGGBBAA`, including fully
transparent alpha `00`. Initial selections, alpha-only edits and cancellation
retain stored alpha. Ordered
custom results bypass the official palette's `FilamentColor` set. Choosing an
official swatch retains its existing palette behavior.
The Linux sidebar's Change extruder color menu uses this same custom editor and
the filament swatch anchor, including alpha, ordered gradients and configuration syncing.
Striped colors and gradients with more than two endpoints cannot be represented
by this editor. A notice explains that the current selection is retained; the
editor starts from the first color, and unchanged confirmation is a no-op.
Cancellation is also a no-op. If the WebView backend cannot initialize, a local
unavailable panel offers Cancel and Escape, without opening a system color picker.
Generic color option fields use an ordinary color button on every platform and
open the same panel with gradients and alpha disabled. The panel is anchored to
the field button. Empty or invalid field values remain undefined (the empty
string), distinct from opaque black. Right-click resets to undefined. Cancel does
not notify the field; confirmation and reset notify only when its RGB string
changes. Programmatic assignments remain silent, and reading a field never
writes legacy favorites.
AMS material settings retain their fixed/AMS swatches and first-level popup. Only
its custom-color action opens the solid, opaque editor, anchored to that custom
control. The transient popup releases its grab while the modal is open and returns
to the same selection on cancellation. Confirmation uses the existing default-color
and packed RGBA event path. Device permission/status checks remain at the first-level
entry.
Texture import's color-mapping Add Material action opens a solid, opaque editor
anchored to the mapping row. The existing filament limit is checked before opening.
The first-level material popup closes as an action; its callback is copied before
dismissal, so popup destruction does not invalidate the modal's completion path.
Confirmation invokes the existing virtual-filament callback once; cancellation
adds no material.
Native gettext strings are installed at document start. Static page labels and
palette names use stable English keys; RAL codes, channel symbols, and numeric
labels remain data. The palette color values do not depend on the UI language.
The application locale also sets the document language. Numeric editor validation
uses the application's gettext messages, rather than browser language defaults;
missing translations fall back to the stable English keys.
The pure color/state regressions run with
`node --test resources/web/dialog/ColorPickerDialog/colorpicker.test.js`.
Optional real-browser regressions live beside them in
`colorpicker.browser.test.js`. They use an existing Playwright installation
(`ORCA_PLAYWRIGHT_MODULE` can name its module path) and an installed browser
(`ORCA_BROWSER_EXECUTABLE` can name its executable), without adding a production
dependency. They cover bridge initialization, capability combinations, invalid
editor recovery, ordered endpoint alpha, favorites, gettext keys, and live theme
changes. These browser checks complement native contract/persistence tests; they
do not establish the full native modal confirmation and favorites journey.
Gradient and alpha capabilities are independent and default off. Unsupported
gradients are rejected; disabled alpha normalizes the selected result to opaque.
Favorites retain both capabilities independently of the current caller. The page
stores alpha as a byte and only converts to percentage when displaying or editing
opacity, so switching endpoints does not change alpha precision.
The page starts disabled. Each document supplies a page identifier in its ready
message, receives native initialization, and acknowledges it before native code
accepts confirmation or favorite updates. Repeated initialization does not reset
edits, and messages from an obsolete document are ignored. Native WebView message
handling is deferred and guarded by a lifetime token. Only a validated confirmation
sets the optional result. Cancel, Escape and native window close leave it empty.
Favorites belong to application configuration, separately from the selected result,
in the editor configuration returned by `AppConfig::config_path()` under `data_dir()`.
The `color_picker` section has string `version` equal to `1` and a `favorites`
string containing a JSON array of typed selections. Native helpers normalize and
deduplicate this array in order and limit it to 24 entries. Updates replace the
entire section and immediately call `AppConfig::save()`, so accepted favorite
changes survive cancellation. Empty arrays are saved explicitly. Core `AppConfig`
only stores the raw section; color validation belongs to the GUI boundary.
When the section is absent, valid solid colors from `custom_color_list` are imported
once, without changing that legacy section. The legacy native picker saves decimal
`r,g,b,a` byte strings; hexadecimal RGB and RGBA strings are also supported. Migration
strictly validates all four decimal channels and preserves alpha. Gradient strings
and malformed values are ignored. The new version and an empty array are
saved even when there is nothing to import. A present section with an unknown or
missing version is left untouched and disables favorite writes. Malformed version
1 data is displayed as empty and remains on disk until the user explicitly saves
a valid replacement. Other configuration sections and project/profile color
formats are independent of this storage.
Interface colors consume the host's `data-orca-theme` and `--orca-*` contract through
shared styles. The picker opts into scoped global control roles. Application theme
changes update UI surfaces without reloading the page or changing color data.
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,6 +325,7 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
src/slic3r/GUI/CloneDialog.cpp
src/slic3r/GUI/ColorDecomposeDialog.cpp
src/slic3r/GUI/ColorDecomposeSupport.cpp
src/slic3r/GUI/ColorPickerDialog.cpp
src/slic3r/GUI/ConfigWizard.cpp
src/slic3r/GUI/DeviceCore/DevCalib.cpp
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
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="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 415 B

@@ -592,6 +592,10 @@
"name": "SUNLU PETG @base",
"sub_path": "filament/SUNLU/SUNLU PETG @base.json"
},
{
"name": "School House Spools PETG @base",
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @base.json"
},
{
"name": "eSUN PETG @base",
"sub_path": "filament/eSUN/eSUN PETG @base.json"
@@ -1096,6 +1100,10 @@
"name": "SUNLU Wood PLA @base",
"sub_path": "filament/SUNLU/SUNLU Wood PLA @base.json"
},
{
"name": "School House Spools PLA @base",
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @base.json"
},
{
"name": "Valment PLA @base",
"sub_path": "filament/Valment/Valment PLA @base.json"
@@ -2260,6 +2268,10 @@
"name": "SUNLU PETG @System",
"sub_path": "filament/SUNLU/SUNLU PETG @System.json"
},
{
"name": "School House Spools PETG @System",
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @System.json"
},
{
"name": "eSUN PETG @System",
"sub_path": "filament/eSUN/eSUN PETG @System.json"
@@ -3812,6 +3824,10 @@
"name": "SUNLU Wood PLA @System",
"sub_path": "filament/SUNLU/SUNLU Wood PLA @System.json"
},
{
"name": "School House Spools PLA @System",
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @System.json"
},
{
"name": "Valment PLA @System",
"sub_path": "filament/Valment/Valment PLA @System.json"
@@ -0,0 +1,18 @@
{
"type": "filament",
"name": "School House Spools PETG @System",
"inherits": "School House Spools PETG @base",
"from": "system",
"setting_id": "RSu2orfhR8ktT36h",
"instantiation": "true",
"filament_max_volumetric_speed": [
"13"
],
"filament_long_retractions_when_cut": [
"1"
],
"filament_retraction_distances_when_cut": [
"18"
],
"compatible_printers": []
}
@@ -0,0 +1,80 @@
{
"type": "filament",
"name": "School House Spools PETG @base",
"inherits": "fdm_filament_pet",
"from": "system",
"filament_id": "OFnaCGQ9",
"instantiation": "false",
"cool_plate_temp": [
"0"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp": [
"70"
],
"eng_plate_temp_initial_layer": [
"70"
],
"fan_cooling_layer_time": [
"30"
],
"fan_max_speed": [
"40"
],
"fan_min_speed": [
"10"
],
"filament_cost": [
"16.00"
],
"filament_density": [
"1.25"
],
"filament_flow_ratio": [
"0.95"
],
"filament_max_volumetric_speed": [
"8"
],
"filament_multitool_ramming_flow": [
"8"
],
"filament_vendor": [
"School House Spools"
],
"hot_plate_temp": [
"70"
],
"hot_plate_temp_initial_layer": [
"70"
],
"nozzle_temperature": [
"252"
],
"nozzle_temperature_initial_layer": [
"252"
],
"nozzle_temperature_range_high": [
"270"
],
"nozzle_temperature_range_low": [
"230"
],
"overhang_fan_speed": [
"90"
],
"overhang_fan_threshold": [
"10%"
],
"slow_down_layer_time": [
"12"
],
"textured_plate_temp": [
"70"
],
"textured_plate_temp_initial_layer": [
"70"
]
}
@@ -0,0 +1,18 @@
{
"type": "filament",
"name": "School House Spools PLA @System",
"inherits": "School House Spools PLA @base",
"from": "system",
"setting_id": "3rHJk6SVAvFzyHI0",
"instantiation": "true",
"filament_max_volumetric_speed": [
"21"
],
"filament_long_retractions_when_cut": [
"1"
],
"filament_retraction_distances_when_cut": [
"18"
],
"compatible_printers": []
}
@@ -0,0 +1,35 @@
{
"type": "filament",
"name": "School House Spools PLA @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFHzwmT9",
"instantiation": "false",
"filament_cost": [
"17.00"
],
"filament_density": [
"1.26"
],
"filament_flow_ratio": [
"0.98"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_vendor": [
"School House Spools"
],
"filament_scarf_seam_type": [
"none"
],
"filament_scarf_height": [
"10%"
],
"filament_scarf_gap": [
"0%"
],
"filament_scarf_length": [
"10"
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "RatRig",
"version": "02.04.00.06",
"version": "02.04.00.07",
"force_update": "0",
"description": "RatRig configurations",
"machine_model_list": [
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -107,7 +107,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -107,7 +107,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -107,7 +107,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -131,7 +131,7 @@
"bed_exclude_area": [
"0x0"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single]",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single]",
"machine_end_gcode": "END_PRINT",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
@@ -0,0 +1,155 @@
/* Pure color/state helpers shared by the page and its node:test contract tests. */
(function (root, factory) {
"use strict";
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
else root.ColorPickerModel = api;
})(typeof globalThis === "object" ? globalThis : this, function () {
"use strict";
function parseHex(hex) {
if (typeof hex !== "string" || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return null;
return [1, 3, 5, 7].map((offset) => offset === 7 && hex.length === 7 ? 255 : parseInt(hex.slice(offset, offset + 2), 16));
}
function toHex(color) {
return "#" + color.map((byte) => Math.round(byte).toString(16).padStart(2, "0")).join("").toUpperCase();
}
const MAX_GRADIENT_COLORS = 7; // Most colors a gradient may have. A caller allows 2 (the default) up to this many.
const NEW_GRADIENT_HUE_SHIFT = 75; // Hue step used for every newly created gradient color.
const gradientLimit = (value) => Math.max(2, Math.min(MAX_GRADIENT_COLORS, Math.floor(Number(value)) || 2));
// `maxGradientColors` caps how many colors a gradient may have (2 when omitted).
function normalizeSelection(value, options = {}, maxGradientColors = 2) {
if (!value || !["solid", "gradient"].includes(value.type) || !Array.isArray(value.colors)) return null;
if (value.type === "gradient" && !options.allow_gradient) return null;
const count = value.colors.length;
if (value.type === "solid" ? count !== 1 : count < 2 || count > gradientLimit(maxGradientColors)) return null;
const colors = value.colors.map(parseHex);
if (colors.some((color) => color === null)) return null;
if (!options.allow_alpha) colors.forEach((color) => { color[3] = 255; });
return {type: value.type, colors: colors.map(toHex)};
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
let h = 0, s = 0;
const l = (max + min) / 2;
if (delta) {
s = delta / (1 - Math.abs(2 * l - 1));
if (max === r) h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
else if (max === g) h = ((b - r) / delta + 2) / 6;
else h = ((r - g) / delta + 4) / 6;
}
return [h * 360, s * 100, l * 100];
}
function hslToRgb(h, s, l) {
h = ((h % 360) + 360) % 360 / 60; s /= 100; l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(h % 2 - 1)), m = l - c / 2;
const sectors = [[c,x,0], [x,c,0], [0,c,x], [0,x,c], [x,0,c], [c,0,x]];
return sectors[Math.floor(h)].map((channel) => Math.round((channel + m) * 255));
}
function contrastColor(color, hueShift = 90) {
if (!Number.isFinite(hueShift)) hueShift = 90;
const degrees = Math.max(-180, Math.min(180, hueShift));
const amount = Math.abs(degrees) / 180;
const [h, s, l] = rgbToHsl(color[0], color[1], color[2]);
const grey = s < 10;
const target = grey ? (l < 50 ? 100 : 0) : 100 - l;
const rgb = hslToRgb(h + degrees, grey ? 0 : s, l + (target - l) * amount);
return [...rgb, color[3]];
}
// options.max_gradient_colors (2..7, default 2) is how many colors this page lets a gradient have.
function createState(options = {}, selection = {type: "solid", colors: ["#808080"]}) {
const capabilities = {allow_gradient: options.allow_gradient === true, allow_alpha: options.allow_alpha === true};
// gradient stays null until a selection provides one or the user switches to gradient mode.
const state = {options: capabilities, maxGradientColors: gradientLimit(options.max_gradient_colors),
mode: "solid", active: 0, solid: [128,128,128,255], gradient: null};
if (!applySelection(state, selection)) return null;
return state;
}
function applySelection(state, value) {
const selection = normalizeSelection(value, state.options, state.maxGradientColors);
if (!selection) return false;
state.mode = selection.type;
const colors = selection.colors.map(parseHex);
if (state.mode === "solid") state.solid = colors[0];
else {
state.gradient = colors;
state.active = Math.min(state.active, colors.length - 1); // A gradient with fewer colors must not leave the selected stop past its end.
}
return true;
}
function activeColor(state) { return state.mode === "solid" ? state.solid : state.gradient[state.active]; }
function setMode(state, mode) {
if (mode !== "solid" && (mode !== "gradient" || !state.options.allow_gradient)) return false;
// A new gradient starts at the current solid color and ends at its contrast color.
if (mode === "gradient" && !state.gradient) state.gradient = [[...state.solid], contrastColor(state.solid, NEW_GRADIENT_HUE_SHIFT)];
state.mode = mode;
return true;
}
function addGradientColor(state) {
if (state.mode !== "gradient" || state.gradient.length >= state.maxGradientColors) return false;
state.gradient.splice(state.active + 1, 0, contrastColor(state.gradient[state.active], NEW_GRADIENT_HUE_SHIFT));
state.active += 1;
return true;
}
function removeGradientColor(state) {
if (state.mode !== "gradient" || state.gradient.length <= 2) return false;
state.gradient.splice(state.active, 1);
state.active = Math.min(state.active, state.gradient.length - 1);
return true;
}
function setRgb(state, rgb) {
if (rgb.length !== 3 || rgb.some((v) => !Number.isFinite(v) || v < 0 || v > 255)) return false;
const color = activeColor(state);
rgb.forEach((v, i) => { color[i] = Math.round(v); });
return true;
}
function setAlphaPercent(state, value) {
if (!state.options.allow_alpha || !Number.isFinite(value) || value < 0 || value > 100) return false;
activeColor(state)[3] = Math.round(value * 255 / 100);
return true;
}
function setHex(state, hex) {
const color = parseHex(hex);
if (!color) return false;
if (hex.length === 7) color[3] = activeColor(state)[3];
if (!state.options.allow_alpha) color[3] = 255;
if (state.mode === "solid") state.solid = color;
else state.gradient[state.active] = color;
return true;
}
function exportSelection(state) {
const colors = state.mode === "solid" ? [state.solid] : state.gradient;
return normalizeSelection({type: state.mode, colors: colors.map(toHex)}, state.options, state.maxGradientColors);
}
// Favorites retain full capabilities regardless of what this caller can select.
function normalizeFavorites(values) {
if (!Array.isArray(values)) return [];
return values.slice(0, 24).map((value) => normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, MAX_GRADIENT_COLORS)).filter(Boolean);
}
function selectionBackground(value) {
return value.type === "gradient" ? `linear-gradient(to right, ${value.colors.join(", ")})` : `linear-gradient(${value.colors[0]}, ${value.colors[0]})`;
}
return {MAX_GRADIENT_COLORS, NEW_GRADIENT_HUE_SHIFT, contrastColor, parseHex, toHex, normalizeSelection, rgbToHsl, hslToRgb, createState, applySelection, activeColor, setMode,
addGradientColor, removeGradientColor, setRgb, setAlphaPercent, setHex, exportSelection, normalizeFavorites, selectionBackground};
});
@@ -0,0 +1,314 @@
// Optional real-DOM regressions. Use an existing Playwright installation:
// ORCA_PLAYWRIGHT_MODULE=<module path> ORCA_BROWSER_EXECUTABLE=<browser path>
// ORCA_BROWSER_ENGINE=webkit exercises the macOS WebView layout engine.
// node --test resources/web/dialog/ColorPickerDialog/colorpicker.browser.test.js
const {test, before, after} = require("node:test");
const assert = require("node:assert/strict");
const {readFileSync} = require("node:fs");
const {resolve} = require("node:path");
const {pathToFileURL} = require("node:url");
const engineName = process.env.ORCA_BROWSER_ENGINE || "chromium";
let engine;
try { engine = require(process.env.ORCA_PLAYWRIGHT_MODULE || "playwright")[engineName]; }
catch (error) { if (error.code !== "MODULE_NOT_FOUND") throw error; }
const options = {skip: engine ? false : "Use an existing Playwright installation via ORCA_PLAYWRIGHT_MODULE"};
const url = pathToFileURL(resolve(__dirname, "index.html")).href;
let browser;
before(async () => { if (engine) browser = await engine.launch({headless: true, ...(process.env.ORCA_BROWSER_EXECUTABLE ? {executablePath: process.env.ORCA_BROWSER_EXECUTABLE} : {})}); });
after(async () => { if (browser) await browser.close(); });
async function open(t, strings = {}, language = "") {
const page = await browser.newPage({viewport: {width: 600, height: 660}}), errors = [];
page.on("pageerror", error => errors.push(error.message));
t.after(async () => { await page.close(); assert.deepEqual(errors, []); });
await page.addInitScript(({table, language}) => {
window.ORCA_UI_STRINGS = table;
window.ORCA_COLOR_PICKER_LANGUAGE = language;
window.messages = [];
window.wx = {postMessage: text => messages.push(JSON.parse(text))};
}, {table: strings, language});
await page.goto(url);
const ready = await page.evaluate(() => messages[0]);
assert.equal(ready.command, "ready");
return {page, ready, send: payload => page.evaluate(value => ColorPickerDialog.handleMessage(value), payload)};
}
function init(ready, gradient = false, alpha = false) {
return {command: "init", page_id: ready.page_id, options: {allow_gradient: gradient, allow_alpha: alpha},
selection: {type: "solid", colors: ["#01234567"]},
ui: {sidebar_open: true, palette: "basic"},
favorites: [{type: "gradient", colors: ["#FF000040", "#0000FF90"]}, {type: "solid", colors: ["#ABCDEF40"]}]};
}
async function count(page, command) { return page.evaluate(value => messages.filter(message => message.command === value).length, command); }
for (const gradient of [false, true]) for (const alpha of [false, true]) {
test(`Native capabilities gate gradient=${gradient} and alpha=${alpha}`, options, async t => {
const {page, ready, send} = await open(t);
assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
assert.equal(await send({...init(ready), options: {allow_gradient: "yes", allow_alpha: alpha}}), false);
assert.equal(await send({...init(ready), page_id: "stale"}), false);
assert.equal(await send(init(ready, gradient, alpha)), true);
assert.equal(await page.locator("#modeControls").isVisible(), gradient);
assert.equal(await page.locator("#alphaControls").isVisible(), alpha);
assert.equal(await page.locator("#userColors button:not(.empty)").count(), gradient ? 2 : 1);
await page.locator("#userColors button:not(.empty)").last().click();
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
assert.deepEqual(selection, {type: "solid", colors: [alpha ? "#ABCDEF40" : "#ABCDEFFF"]});
assert.equal(await send(init(ready, gradient, alpha)), true);
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection, "repeated ready/init retains edits");
await page.locator("#colorPreview").click();
const favorites = await page.evaluate(() => messages.at(-1).favorites);
assert(favorites.some(favorite => favorite.colors[0] === "#ABCDEF40"), "original favorites retain their alpha");
assert(favorites.some(favorite => favorite.type === "gradient"), "filtered gradients remain stored");
await page.locator("#confirmBtn").click();
assert.deepEqual(await page.evaluate(() => messages.at(-1).selection), selection);
});
}
test("Visible incomplete editors block confirmation and favorites until recovery", options, async t => {
const {page, ready, send} = await open(t); await send(init(ready, true, true));
for (const [id, value, valid] of [["rValue", "", "21"], ["oValue", "", "50"], ["hexInput", "GG", "12345680"]]) {
await page.locator("#" + id).fill(value);
const confirms = await count(page, "confirm"), saves = await count(page, "update_favorites");
await page.locator("#confirmBtn").evaluate(element => element.click()); await page.locator("#colorPreview").click();
assert.equal(await count(page, "confirm"), confirms); assert.equal(await count(page, "update_favorites"), saves);
await page.locator("#" + id).fill(valid); await page.locator("#" + id).press("Enter");
assert.equal(await count(page, "confirm"), confirms + 1);
}
await page.locator("#hValue").fill(""); // HSL is now visible beside RGB, so incomplete HSL must block.
const hslConfirms = await count(page, "confirm");
await page.locator("#confirmBtn").evaluate(element => element.click());
assert.equal(await count(page, "confirm"), hslConfirms);
await page.locator("#hValue").fill("100");
const confirms = await count(page, "confirm"); await page.locator("#confirmBtn").click();
assert.equal(await count(page, "confirm"), confirms + 1);
await page.locator("#rValue").fill(""); await page.locator("#rValue").press("Escape");
assert.equal(await page.evaluate(() => messages.at(-1).command), "cancel");
});
test("Numeric channel and opacity edits clamp to their field bounds", options, async t => {
const {page, ready, send} = await open(t); await send(init(ready, true, true));
for (const [id, input, expected] of [["rValue", "999", "255"], ["rValue", "-1", "0"],
["hValue", "361", "360"], ["oValue", "101", "100"], ["oValue", "-1", "0"]]) {
await page.locator("#" + id).fill(input);
assert.equal(await page.locator("#" + id).inputValue(), expected);
assert.equal(await page.locator("#" + id).evaluate(element => element.checkValidity()), true);
}
assert.equal((await page.evaluate(() => ColorPickerDialog.exportSelection())).colors[0].slice(-2), "00");
});
test("Endpoint edits and favorites retain ordered byte alpha", options, async t => {
const {page, ready, send} = await open(t);
await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
await page.locator(".gradient-thumb[data-index='1']").click();
await page.locator('.color-item[title="Red"]').first().click();
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "gradient", colors: ["#FF000001", "#FF000080"]});
await page.locator("#hexInput").fill("0000FF80"); await page.locator(".gradient-thumb[data-index='0']").click();
assert.equal(await page.locator("#hexInput").inputValue(), "FF000001");
await page.locator("#colorPreview").click();
assert.deepEqual(await page.evaluate(() => messages.at(-1).favorites[0]), {type: "gradient", colors: ["#FF000001", "#0000FF80"]});
});
test("Read-only favorites remain selectable and late documents cannot initialize", options, async t => {
const {page, ready, send} = await open(t); await send({...init(ready), favorites_writable: false});
assert.equal(await page.locator("#colorPreview").isDisabled(), true);
await page.locator("#userColors button:not(.empty)").first().click();
assert.equal(await page.locator("#hexInput").inputValue(), "ABCDEF");
await page.reload();
assert.equal(await send(init(ready)), false); assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
});
test("Document strings translate palette names and editor errors while retaining RAL codes and numeric labels", options, async t => {
const source = readFileSync(resolve(__dirname, "../../../../src/slic3r/GUI/ColorPickerDialog.cpp"), "utf8");
const keys = [...source.matchAll(/_u8L\("([^"\n]+)"\)/g)].map(match => match[1]);
const strings = Object.fromEntries(keys.map(key => [key, "Translated: " + key]));
const {page, ready, send} = await open(t, strings); await send({...init(ready, true, true), options: {allow_gradient: true, allow_alpha: true, max_gradient_colors: 7}, preserve_multi_color: true});
assert.equal(await page.title(), strings["Color Picker"]);
for (const element of await page.locator("[data-i18n]").all()) assert.equal(await element.textContent(), strings[await element.getAttribute("data-i18n")]);
for (const attribute of ["aria-label", "title"]) for (const element of await page.locator(`[data-i18n-${attribute}]`).all())
assert.equal((await element.getAttribute(attribute)).split("\n")[0], strings[await element.getAttribute(`data-i18n-${attribute}`)]);
assert.equal(await page.locator("#selectionNotice").textContent(), strings["The current multi-color selection is kept until you choose a different color."]);
for (const palette of ["basic", "orca", "ral", "gradient"]) {
await page.selectOption("#paletteCombo", palette);
const names = await page.evaluate(value => ColorPickerPalettes[value].map(entry => entry.name), palette);
assert.deepEqual(await page.locator(".color-name").allTextContents(), names.map(name => {
const ral = name.match(/^(\d{4}) (.+)$/); return ral ? `${ral[1]} ${strings[ral[2]]}` : /^%\d+$/.test(name) ? name : strings[name];
}));
}
await page.locator("#hexInput").fill("bad");
assert.equal(await page.locator("#hexInput").evaluate(element => element.validationMessage), strings["Enter a complete hexadecimal color"]);
await page.locator("#hexInput").fill("12345680");
for (const [id, invalid, valid, key] of [["rValue", "", "18", "Invalid input"],
["oValue", "", "50", "Invalid input"]]) {
const confirms = await count(page, "confirm");
await page.locator("#" + id).fill(invalid);
await page.locator("#confirmBtn").evaluate(element => element.click());
await page.locator("#colorPreview").click(); // Saving invokes the same translated validation while OK is disabled.
assert.equal(await count(page, "confirm"), confirms);
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), strings[key]);
await page.locator("#" + id).fill(valid);
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), "");
await page.locator("#confirmBtn").click();
assert.equal(await count(page, "confirm"), confirms + 1);
}
await page.locator("#hValue").fill("361");
await page.locator("#colorPreview").click();
assert.equal(await page.locator("#hValue").inputValue(), "360", "out-of-range hue clamps at its boundary");
await page.locator(".color-item").first().click();
assert.equal(await page.locator("#hValue").evaluate(element => element.validationMessage), "", "a new palette selection clears the previous editor error");
});
test("Document language and escaped labels preserve untranslated English fallback", options, async t => {
const cancel = '取消 "Cancel" \\ /\n下一行';
const {page, ready, send} = await open(t, {Cancel: cancel}, "zh-CN");
await send(init(ready));
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
assert.equal(await page.locator("#cancelBtn").textContent(), cancel);
assert.equal(await page.title(), "Color Picker");
assert.equal(await page.locator(".color-name").first().textContent(), "Red");
assert.deepEqual(await page.locator("label[for=hValue],label[for=sValue],label[for=lValue],label[for=rValue],label[for=gValue],label[for=bValue]").allTextContents(), ["H", "S", "L", "R", "G", "B"]);
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "solid", colors: ["#012345FF"]});
});
test("Global control colors follow the system while host semantic colors follow the app", options, async t => {
const {page, ready, send} = await open(t); await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
const initial = await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()}));
for (const theme of ["light", "dark"]) for (const system of ["light", "dark"]) {
await page.emulateMedia({colorScheme: system});
await page.evaluate(value => {
document.documentElement.dataset.orcaTheme = value;
const colors = value === "light" ? {bg: "#ffffff", fg: "#262e30", muted: "#363636", border: "#dbdbdb", accent: "#009688"} : {bg: "#2d2d31", fg: "#efeff0", muted: "#b2b3b5", border: "#36363b", accent: "#00675b"};
for (const [key, color] of Object.entries(colors)) document.documentElement.style.setProperty("--orca-" + key, color);
document.documentElement.style.setProperty("--orca-accent-fg", "#ffffff");
}, theme);
await page.evaluate(async () => {
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
});
const colors = await page.evaluate(() => {
const body = getComputedStyle(document.body);
const tokenColor = token => {
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
};
const mode = document.querySelector("label[for=modeSwitch]");
return {background: body.backgroundColor, foreground: body.color,
modeBackground: getComputedStyle(mode).backgroundColor,
accent: getComputedStyle(mode, "::before").backgroundColor,
hostPanel: tokenColor("--panel"), hostText: tokenColor("--text")};
});
assert.equal(colors.background, system === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)", `${theme} app / ${system} system body`);
assert.equal(colors.foreground, system === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
assert.equal(colors.modeBackground, system === "light" ? "rgb(240, 240, 240)" : "rgb(36, 36, 40)");
assert.equal(colors.accent, theme === "light" ? "rgb(0, 150, 136)" : "rgb(0, 103, 91)", "shared accent follows the injected app theme");
assert.equal(colors.hostPanel, theme === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)");
assert.equal(colors.hostText, theme === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
assert.deepEqual(await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()})), initial);
for (const id of ["modeSwitch"]) for (const checked of [false, true]) {
if (await page.locator("#" + id).isChecked() !== checked) await page.locator(`label[for=${id}]`).click();
await page.evaluate(async () => {
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
});
const result = await page.locator(`label[for=${id}]`).evaluate((element, selected) => {
const tokenColor = token => {
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
};
const labels = element.querySelectorAll(".switch-label");
return {selected: getComputedStyle(labels[selected ? 1 : 0]).color,
unselected: getComputedStyle(labels[selected ? 0 : 1]).color,
accentForeground: tokenColor("--button-fg-light"), mutedForeground: tokenColor("--fg-color-label")};
}, checked);
assert.equal(result.selected, result.accentForeground, `${theme} app / ${system} system ${id} selected text uses the accent foreground`);
assert.equal(result.selected, "rgb(254, 254, 254)", "global selected text stays light in both system schemes");
assert.equal(result.unselected, result.mutedForeground, `${theme} app / ${system} system ${id} unselected text stays muted`);
assert.equal(result.unselected, system === "light" ? "rgb(54, 54, 54)" : "rgb(178, 179, 181)");
const selected = await page.evaluate(() => ColorPickerDialog.exportSelection());
assert.equal(selected.type, checked ? "gradient" : "solid");
if (checked) assert.deepEqual(selected, initial.selection, "returning to gradient keeps ordered endpoint byte alpha");
}
}
});
test("Sidebar resizes from a compact viewport and shrinks after every collapse", options, async t => {
const {page, ready, send} = await open(t);
await page.setViewportSize({width: 340, height: 380});
await send({...init(ready, true, true), ui: {sidebar_open: false, palette: "basic"}});
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
for (const [index, sidebarOpen] of [false, true, false, true, false].entries()) {
const width = sidebarOpen ? 510 : 340;
await page.waitForFunction(expected => messages.filter(message => message.command === "resize").at(-1)?.width === expected, width);
const size = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1));
assert.equal(size.height, 380);
await page.setViewportSize({width: size.width, height: size.height});
await page.waitForTimeout(100);
assert(await page.evaluate(() => {
const body = document.body.getBoundingClientRect();
const picker = document.querySelector(".right-panel").getBoundingClientRect();
return body.width === innerWidth && picker.right <= body.right && picker.bottom <= body.bottom;
}), "the host viewport contains the full picker");
const resizes = await count(page, "resize");
await page.waitForTimeout(100);
assert.equal(await count(page, "resize"), resizes, "fitting the host does not cause a resize loop");
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
if (index < 4) await page.locator("#paletteToggle").click();
}
});
test("Floating panel fits capabilities and wrapped translations without a resize feedback loop", options, async t => {
const strings = {Color: "A long translated color label", Gradient: "A long translated gradient label", Cancel: "A longer translated cancel action", OK: "A longer translated confirmation action",
"The current multi-color selection is kept until you choose a different color.": "A translated explanation that wraps across the compact floating panel and keeps the original multi-color selection until another color is chosen."};
for (const gradient of [false, true]) for (const alpha of [false, true]) {
const {page, ready, send} = await open(t, strings);
await page.setViewportSize({width: 550, height: 520});
await send({...init(ready, gradient, alpha), preserve_multi_color: true});
await page.waitForFunction(() => messages.some(message => message.command === "resize"));
const height = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1).height);
assert(height >= 300 && height <= 900);
assert.equal(height, await page.locator("body").evaluate(element => Math.ceil(element.getBoundingClientRect().height)));
assert(await page.evaluate(() => messages.findIndex(message => message.command === "initialized") < messages.findIndex(message => message.command === "resize")));
await page.setViewportSize({width: 550, height});
await page.waitForTimeout(100);
const resizes = await count(page, "resize");
await page.setViewportSize({width: 550, height: height + 100});
await page.waitForTimeout(100);
assert.equal(await count(page, "resize"), resizes, "extra viewport height never changes intrinsic content height");
const controls = await page.evaluate(() => {
const body = document.body.getBoundingClientRect();
return ["confirmBtn", "cancelBtn", ...(document.getElementById("alphaControls").classList.contains("hidden") ? [] : ["oValue"])].map(id => {
const rect = document.getElementById(id).getBoundingClientRect();
return rect.left >= body.left && rect.right <= body.right && rect.bottom <= body.bottom;
});
});
assert(controls.every(Boolean), "wrapped buttons and opacity fit inside the panel");
const edited = await page.evaluate(() => ColorPickerDialog.exportSelection());
await page.evaluate(() => {
document.getElementById("selectionNotice").textContent += " Additional wrapped text. ".repeat(15);
});
await page.waitForFunction(previous => messages.filter(message => message.command === "resize").length > previous, resizes);
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), edited, "content resize leaves the selection untouched");
// The compact layout now puts saved swatches to the right of both previews.
assert(await page.evaluate(() => {
const favorites = document.getElementById("userColors").getBoundingClientRect();
const preview = document.getElementById("colorPreview").getBoundingClientRect();
return favorites.left >= preview.right;
}), "favorites do not overlap the preview");
await page.locator("#confirmBtn").scrollIntoViewIfNeeded();
assert.equal(await page.locator("#confirmBtn").isVisible(), true);
}
});
test("Native focus hook waits for initialization and preserves active partial edits", options, async t => {
const {page, ready, send} = await open(t);
await page.evaluate(() => ColorPickerDialog.focusInput());
assert.notEqual(await page.evaluate(() => document.activeElement.id), "hexInput");
await send(init(ready, true, true));
await page.evaluate(() => ColorPickerDialog.focusInput());
assert.equal(await page.evaluate(() => document.activeElement.id), "hexInput");
await page.locator("#rValue").fill("");
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
await page.evaluate(() => ColorPickerDialog.focusInput());
assert.equal(await page.evaluate(() => document.activeElement.id), "rValue");
assert.equal(await page.locator("#rValue").inputValue(), "");
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
assert.equal(await count(page, "confirm"), 0);
await page.locator("#rValue").press("Escape");
assert.equal(await count(page, "cancel"), 1);
});
@@ -0,0 +1,464 @@
"use strict";
(() => {
if (window.ORCA_COLOR_PICKER_LANGUAGE) document.documentElement.lang = window.ORCA_COLOR_PICKER_LANGUAGE;
const T = (key) => window.ORCA_UI_STRINGS?.[key] || key;
document.title = T("Color Picker");
for (const element of document.querySelectorAll("[data-i18n]")) element.textContent = T(element.dataset.i18n);
for (const attribute of ["aria-label", "title", "placeholder"])
for (const element of document.querySelectorAll(`[data-i18n-${attribute}]`))
element.setAttribute(attribute, T(element.getAttribute(`data-i18n-${attribute}`)));
function paletteName(name) {
const ral = name.match(/^(\d{4}) (.+)$/);
return ral ? `${ral[1]} ${T(ral[2])}` : /^%\d+$/.test(name) ? name : T(name);
}
const FAVORITE_SLOTS = 16;
const SPECTRUM_FOLLOWS_SATURATION = true; // true: the spectrum is drawn at the current saturation, false: it always shows full saturation
const MULTI_COLOR_GRADIENTS = true; // true: gradients may have more than 2 colors (− / + buttons and the multi-color palette entries), false: exactly 2 colors.
// up to the native limit the host sends as options.max_gradient_colors.
let maxGradientColors = 2; // Stays 2 until init, and with a host that does not send a limit.
const model = window.ColorPickerModel;
const palettes = window.ColorPickerPalettes;
const byId = (id) => document.getElementById(id);
const canvas = byId("spectrum"), context = canvas.getContext("2d");
const paletteCombo = byId("paletteCombo"), palette = byId("palette"), preview = byId("colorPreview"), confirmBtn = byId("confirmBtn");
const modeSwitch = byId("modeSwitch"), satSlider = byId("satSlider"), opacitySlider = byId("opacitySlider");
const gradientBar = byId("gradientCtrlBar"), gradientStops = byId("gradientStops"), gradientAdd = byId("gradientAdd"), gradientRemove = byId("gradientRemove");
const hostGradientLimit = (options) => options.max_gradient_colors === undefined ? 2 : options.max_gradient_colors;
const originalPreview = byId("originalPreview"), palettePanel = byId("palettePanel"), paletteToggle = byId("paletteToggle");
const paletteSelect = byId("paletteSelect"), paletteFilter = byId("paletteFilter"), paletteSearchToggle = byId("paletteSearchToggle");
const rgbInputs = ["r", "g", "b"].map((name) => byId(name + "Value"));
const hslInputs = ["h", "s", "l"].map((name) => byId(name + "Value"));
const hexInput = byId("hexInput"), opacityInput = byId("oValue");
const pageId = Date.now().toString(36) + Math.random().toString(36).slice(2);
let state = model.createState(), favorites = [], initialized = false, favoritesWritable = true;
// The selection the dialog opened with; the original swatch shows it and restores it on click.
let originalSelection = model.exportSelection(state);
let lastContentHeight = 0, lastContentWidth = 0, resizePending = false;
// Last HSL shown or set. Kept while it still produces the current color: hue 360 and 0 are
// both red, and greys, black and white have no hue or saturation in RGB.
let hslMemory = [0, 0, 50];
function displayHsl(color) {
const rgb = model.hslToRgb(...hslMemory);
if (!rgb.every((value, index) => value === color[index])) hslMemory = model.rgbToHsl(...color.slice(0, 3));
return hslMemory;
}
// Copy of `values` with one entry replaced.
const replaced = (values, index, value) => values.map((current, i) => i === index ? value : current);
function setHslChannel(index, value) {
hslMemory = replaced(displayHsl(model.activeColor(state)), index, value);
model.setRgb(state, model.hslToRgb(...hslMemory));
}
const round1 = (value) => Math.round(value * 10) / 10;
function emit(command, data = {}) {
window.dispatchEvent(new CustomEvent("color-picker-message", {detail: {command, ...data}}));
if (window.wx && typeof window.wx.postMessage === "function")
window.wx.postMessage(JSON.stringify({command, page_id: pageId, ...data}));
}
function scheduleResize() {
if (!initialized || resizePending) return;
resizePending = true;
requestAnimationFrame(() => {
resizePending = false;
const rect = document.body.getBoundingClientRect();
const height = Math.min(900 , Math.max(300, Math.ceil(rect.height)));
const width = Math.min(1200, Math.max(300, Math.ceil(rect.width )));
if (height !== lastContentHeight || width !== lastContentWidth) {
lastContentHeight = height; lastContentWidth = width;
emit("resize", {width, height});
}
});
}
new ResizeObserver(scheduleResize).observe(document.body);
function setSwatch(element, selection) {
element.style.setProperty("--swatch-color", model.selectionBackground(selection));
}
// `editing` is the control the user is typing in. Its text is left alone so the caret,
// partial input like "05" and fine steps that round-trip through bytes aren't overwritten.
function render(resetEditors = false, editing = null) {
const color = model.activeColor(state), hsl = displayHsl(color);
const show = (input, value) => {
if (input === editing) return;
if (resetEditors) input.setCustomValidity("");
if (resetEditors || input.checkValidity()) input.value = value;
};
show(hexInput, model.toHex(color).slice(1, state.options.allow_alpha ? 9 : 7));
rgbInputs.forEach((input, index) => show(input, color[index]));
// Shown in whole numbers; decimals can still be typed, and hslMemory keeps exact values.
hslInputs.forEach((input, index) => show(input, Math.round(hsl[index])));
// Percentage is a display only until explicitly edited. Never round it back
// into the alpha byte on endpoint switches, RGB edits, or confirmation.
show(opacityInput, Math.round(color[3] / 255 * 100));
if (opacitySlider !== editing) opacitySlider.value = color[3] / 255 * 100;
const rgb = color.slice(0, 3).join(",");
opacitySlider.style.setProperty("--track-gradient", `linear-gradient(to right, rgba(${rgb},0), rgb(${rgb}))`);
opacitySlider.style.setProperty("--thumb-color", `rgba(${rgb},${color[3] / 255})`);
if (satSlider !== editing) satSlider.value = hsl[1];
satSlider.style.setProperty("--track-gradient", `linear-gradient(to right, hsl(${hsl[0]},0%,${hsl[2]}%), hsl(${hsl[0]},100%,${hsl[2]}%))`);
satSlider.style.setProperty("--thumb-color", `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`);
modeSwitch.checked = state.mode === "gradient";
const selection = model.exportSelection(state);
// Kept in the layout (visibility only) so the grid doesn't shift between Solid and Gradient.
gradientBar.classList.toggle("invisible", state.mode !== "gradient");
if (state.mode === "gradient") renderGradient(selection);
setSwatch(preview, selection);
preview.title = `${T("Save current color")}\n${selection.colors.join(" → ")}`;
if (SPECTRUM_FOLLOWS_SATURATION && hsl[1] !== spectrumSaturation) { spectrumSaturation = hsl[1]; paintSpectrum(); }
updateIndicator(hsl);
updateConfirm();
}
// One thumb per gradient color, evenly spaced; rebuilt only when the number of colors changes.
function renderGradient(selection) {
const count = selection.colors.length;
setSwatch(gradientStops.querySelector(".gradient-track"), selection);
let thumbs = [...gradientStops.querySelectorAll(".gradient-thumb")];
if (thumbs.length !== count) {
thumbs.forEach((thumb) => thumb.remove());
thumbs = selection.colors.map((_, index) => {
const thumb = document.createElement("button");
thumb.type = "button"; thumb.className = "gradient-thumb"; thumb.dataset.index = index;
thumb.style.setProperty("--stop", index / (count - 1));
thumb.setAttribute("aria-label", count === 2 ? T(index ? "Gradient end" : "Gradient start") : `${T("Gradient color")} ${index + 1}`);
gradientStops.appendChild(thumb);
return thumb;
});
}
thumbs.forEach((thumb, index) => {
thumb.style.setProperty("--thumb-color", selection.colors[index]);
thumb.setAttribute("aria-pressed", String(state.active === index));
});
const multi = maxGradientColors > 2; // −/+ buttons only exist when more than 2 colors are allowed; − also needs a color to spare.
gradientAdd.parentElement.classList.toggle("hidden", !multi);
gradientRemove.classList.toggle("hidden", !multi || count <= 2);
gradientAdd.classList.toggle("hidden", !multi);
// aria-disabled instead of disabled: a disabled button gets no hover, so its tooltip would never show.
const full = count >= maxGradientColors;
gradientAdd.setAttribute("aria-disabled", String(full));
gradientAdd.title = full ? T("Up to %d colors supported.").replace("%d", maxGradientColors) : T("Add color");
}
// Every editor the user can see; alpha only when this caller allows it.
function editors() {
const inputs = [hexInput, ...rgbInputs, ...hslInputs];
if (state.options.allow_alpha) inputs.push(opacityInput);
return inputs;
}
// OK stays disabled while any visible editor holds an invalid or incomplete value.
function updateConfirm() {
confirmBtn.disabled = !editors().every((input) => input.checkValidity());
}
// Matches the filter against the shown (translated) name, the English name and the hex digits.
function paletteMatches(entry, query) {
if (!query) return true;
const haystack = [paletteName(entry.name), entry.name, entry.hex || "", entry.grad || ""].join(" ").toLowerCase();
return query.split(/\s+/).every((word) => haystack.includes(word.replace(/^#/, "")));
}
function populatePalette() {
palette.replaceChildren();
const query = paletteSearchToggle.getAttribute("aria-pressed") === "true" ? paletteFilter.value.trim().toLowerCase() : "";
for (const entry of palettes[paletteCombo.value] || []) {
const selection = {type: entry.grad ? "gradient" : "solid", colors: entry.grad ? entry.grad.split("-") : [entry.hex]};
if (!model.normalizeSelection(selection, state.options, maxGradientColors) || !paletteMatches(entry, query)) continue;
const row = document.createElement("button"); row.type = "button"; row.className = "color-item";
const swatch = document.createElement("span"); swatch.className = "color-swatch";
const name = document.createElement("span"); name.className = "color-name"; name.textContent = paletteName(entry.name);
setSwatch(swatch, model.normalizeSelection(selection, {allow_gradient: true, allow_alpha: true}, maxGradientColors));
row.append(swatch, name); row.title = paletteName(entry.name);
row.addEventListener("click", () => {
// Solid palette colors edit the active endpoint while gradient mode is on.
const changed = entry.grad ? model.applySelection(state, selection) : model.setHex(state, entry.hex);
if (changed) render(true);
});
palette.appendChild(row);
}
byId("paletteEmpty").classList.toggle("hidden", palette.childElementCount > 0);
palette.classList.toggle("hidden", palette.childElementCount === 0);
}
// Sidebar state the host keeps in the app config: open/closed and the chosen palette.
const uiState = () => ({sidebar_open: palettePanel.classList.contains("open"), palette: paletteCombo.value});
function setSidebarOpen(open) {
palettePanel.classList.toggle("open", open);
paletteToggle.setAttribute("aria-expanded", String(open));
}
// The search button swaps the combo box for a filter box; closing it clears the filter.
function setPaletteSearch(open) {
paletteSearchToggle.setAttribute("aria-pressed", String(open));
paletteSelect.classList.toggle("hidden", open);
paletteFilter.classList.toggle("hidden", !open);
if (open) paletteFilter.focus();
else paletteFilter.value = "";
populatePalette();
}
function drawFavorites() {
const grid = byId("userColors"); grid.replaceChildren();
// Favorites keep every color, but only those this page can select are shown.
const visible = favorites.filter((favorite) => model.normalizeSelection(favorite, {...state.options, allow_alpha: true}, maxGradientColors));
for (let index = 0; index < FAVORITE_SLOTS; ++index) {
const swatch = document.createElement("button"); swatch.type = "button"; swatch.className = "saved-swatch";
const favorite = visible[index];
if (favorite) {
setSwatch(swatch, favorite); swatch.title = favorite.colors.join(" → ");
swatch.addEventListener("click", () => { if (model.applySelection(state, favorite)) render(true); });
} else { swatch.classList.add("empty"); swatch.disabled = true; }
grid.appendChild(swatch);
}
}
function init(payload) {
if (!payload || typeof payload !== "object" || (payload.options !== undefined && (!payload.options || typeof payload.options !== "object"))) return false;
maxGradientColors = Math.min(MULTI_COLOR_GRADIENTS ? model.MAX_GRADIENT_COLORS : 2, hostGradientLimit(payload.options || {}));
// A gradient the host allows but this page does not (switch off) opens as its first color, like
// other multi-color selections the picker cannot edit; OK then replaces it, Cancel keeps it.
let selection = payload.selection, preserve = payload.preserve_multi_color === true;
if (selection?.type === "gradient" && selection.colors?.length > maxGradientColors) {
selection = {type: "solid", colors: [selection.colors[0]]};
preserve = true;
}
const next = model.createState({...payload.options, max_gradient_colors: maxGradientColors}, selection);
if (!next) return false;
byId("selectionNotice").hidden = !preserve;
state = next; favorites = model.normalizeFavorites(payload.favorites || []);
originalSelection = model.exportSelection(state);
setSwatch(originalPreview, originalSelection);
originalPreview.title = `${T("Original color")}\n${originalSelection.colors.join(" → ")}`;
favoritesWritable = payload.favorites_writable !== false;
preview.disabled = !favoritesWritable;
byId("modeControls").classList.toggle("hidden", !state.options.allow_gradient);
for (const option of paletteCombo.options) option.hidden = option.value === "gradient" && !state.options.allow_gradient;
const ui = payload.ui || {}; // Restore the sidebar the way it was left; a palette this caller can't use falls back to basic.
const saved = [...paletteCombo.options].find((option) => option.value === ui.palette && !option.hidden);
if (saved) paletteCombo.value = saved.value;
if (!state.options.allow_gradient && paletteCombo.value === "gradient") paletteCombo.value = "basic";
setSidebarOpen(ui.sidebar_open === true);
byId("alphaControls").classList.toggle("hidden", !state.options.allow_alpha);
hexInput.maxLength = state.options.allow_alpha ? 9 : 7;
populatePalette(); drawFavorites(); render(true);
initialized = true;
byId("pickerEditors").disabled = false;
return true;
}
function focusInput() {
if (!initialized) return;
// Restore initial keyboard focus without replacing an editor's in-progress input.
const active = document.activeElement;
if (!active || active === document.body || active === document.documentElement)
hexInput.focus({preventScroll: true});
}
function handleMessage(payload) {
if (!payload || typeof payload !== "object" || payload.page_id !== pageId) return false;
if (payload.command === "init") {
const options = payload.options;
const hostLimit = options && hostGradientLimit(options);
if (!options || typeof options.allow_gradient !== "boolean" || typeof options.allow_alpha !== "boolean" ||
!Number.isInteger(hostLimit) || hostLimit < 2 || hostLimit > model.MAX_GRADIENT_COLORS ||
!model.normalizeSelection(payload.selection, options, hostLimit) || !validFavorites(payload.favorites) ||
(payload.favorites_writable !== undefined && typeof payload.favorites_writable !== "boolean") ||
(payload.preserve_multi_color !== undefined && typeof payload.preserve_multi_color !== "boolean") ||
(payload.ui !== undefined && (!payload.ui || typeof payload.ui !== "object" ||
(payload.ui.sidebar_open !== undefined && typeof payload.ui.sidebar_open !== "boolean") ||
(payload.ui.palette !== undefined && typeof payload.ui.palette !== "string")))) return false;
if (initialized) return true;
if (!init(payload)) return false;
emit("initialized");
scheduleResize();
return true;
}
if (payload.command === "favorites" && initialized && validFavorites(payload.favorites)) {
favorites = model.normalizeFavorites(payload.favorites); drawFavorites(); return true;
}
return false;
}
function validFavorites(values) {
return Array.isArray(values) && values.length <= 24 && values.every((value) => model.normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, model.MAX_GRADIENT_COLORS));
}
// Saturation the spectrum is currently painted at (always 100 when the option is off).
let spectrumSaturation = 100;
function drawSpectrum() {
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, Math.round(rect.width)); canvas.height = Math.max(1, Math.round(rect.height));
paintSpectrum();
}
function paintSpectrum() {
const hue = context.createLinearGradient(0, 0, canvas.width, 0);
for (let i = 0; i <= 6; ++i) hue.addColorStop(i / 6, `hsl(${i * 60},100%,50%)`);
context.fillStyle = hue; context.fillRect(0, 0, canvas.width, canvas.height);
const shade = context.createLinearGradient(0, 0, 0, canvas.height);
shade.addColorStop(0, "#FFFFFF"); shade.addColorStop(0.5, "#FFFFFF00"); shade.addColorStop(0.5, "#00000000"); shade.addColorStop(1, "#000000");
context.fillStyle = shade; context.fillRect(0, 0, canvas.width, canvas.height);
// HSL desaturation blends each color toward the grey of the same lightness, and on this
// canvas that grey only depends on the row (white at the top, black at the bottom).
if (spectrumSaturation < 100) {
const grey = context.createLinearGradient(0, 0, 0, canvas.height);
grey.addColorStop(0, "#FFFFFF"); grey.addColorStop(1, "#000000");
context.globalAlpha = 1 - spectrumSaturation / 100;
context.fillStyle = grey; context.fillRect(0, 0, canvas.width, canvas.height);
context.globalAlpha = 1;
}
}
const indicator = byId("colorIndicator");
function updateIndicator(hsl) {
indicator.style.left = hsl[0] / 360 * (canvas.width - 1) + "px";
indicator.style.top = (1 - hsl[2] / 100) * (canvas.height - 1) + "px";
}
// Spectrum position maps straight to hue (x, 0..360) and lightness (y, 100..0); saturation is
// the third dimension and comes from the slider below. hslMemory keeps these exact values,
// including hue 360 at the right edge and the hue of the white and black corners.
function sampleSpectrum(event) {
const rect = canvas.getBoundingClientRect();
const fraction = (client, start, size, pixels) =>
Math.max(0, Math.min(pixels - 1, Math.floor((client - start) * pixels / size))) / Math.max(1, pixels - 1);
const saturation = displayHsl(model.activeColor(state))[1];
hslMemory = [round1(fraction(event.clientX, rect.left, rect.width, canvas.width) * 360), saturation,
round1((1 - fraction(event.clientY, rect.top, rect.height, canvas.height)) * 100)];
model.setRgb(state, model.hslToRgb(...hslMemory));
// A pick replaces every editor's value, so clear any invalid state left from typing.
render(true);
}
const clamp = (control, value) => Math.min(Number(control.max), Math.max(Number(control.min), value));
function bindValue(control, apply) {
// Number fields are required, so an empty field is invalid; a range input always has a value.
if (control.type === "number") control.required = true;
const commit = (value) => { apply(value); render(false, control); };
control.addEventListener("input", () => {
control.setCustomValidity("");
// Out-of-range numbers snap to the field's min/max instead of becoming invalid.
if (control.value !== "") {
const clamped = clamp(control, Number(control.value));
if (clamped !== Number(control.value)) control.value = clamped;
}
if (control.checkValidity()) commit(Number(control.value));
});
control.addEventListener("wheel", (event) => {
event.preventDefault();
const step = control.type === "range" ? 5 : 1;
control.value = clamp(control, (Number(control.value) || 0) + (event.deltaY > 0 ? -step : step));
control.setCustomValidity("");
commit(Number(control.value));
}, {passive: false});
// Normalize the text (e.g. "05" → "5") once the user leaves the field.
control.addEventListener("blur", () => { if (control.checkValidity()) render(); });
}
rgbInputs.forEach((input, index) => bindValue(input, (value) => model.setRgb(state, replaced(model.activeColor(state).slice(0, 3), index, value))));
hslInputs.forEach((input, index) => bindValue(input, (value) => setHslChannel(index, value)));
bindValue(satSlider, (value) => setHslChannel(1, value));
bindValue(opacityInput, (value) => model.setAlphaPercent(state, value));
bindValue(opacitySlider, (value) => model.setAlphaPercent(state, value));
// The opacity slider takes the gradient bar's row while the opacity field or the slider is in
// use: it opens when the field is clicked or focused, and closes on a click or focus elsewhere.
const thirdRow = byId("thirdRow"), alphaControls = byId("alphaControls");
const inOpacityControls = (target) => target instanceof Element && target.closest("#alphaControls, #opacitySliderBar");
const setOpacitySliderOpen = (open) => thirdRow.classList.toggle("opacity-open", open);
for (const type of ["focusin", "click"]) alphaControls.addEventListener(type, () => setOpacitySliderOpen(true));
for (const type of ["pointerdown", "focusin"])
document.addEventListener(type, (event) => { if (!inOpacityControls(event.target)) setOpacitySliderOpen(false); });
hexInput.addEventListener("input", () => {
const text = hexInput.value;
const valid = model.setHex(state, text.startsWith("#") ? text : "#" + text);
hexInput.setCustomValidity(valid ? "" : T("Enter a complete hexadecimal color"));
// render() leaves the typed text alone, so alpha isn't appended after the sixth digit.
if (valid) render(false, hexInput);
});
hexInput.addEventListener("blur", () => { if (hexInput.checkValidity()) render(); });
modeSwitch.addEventListener("change", () => {
setOpacitySliderOpen(false); // Close the opacity slider before the row changes, so it doesn't linger over the gradient bar.
model.setMode(state, modeSwitch.checked ? "gradient" : "solid");
render(true);
});
// Click on the track selects the nearest thumb. Keyboard users tab to a thumb and press Enter or Space.
gradientStops.addEventListener("click", (event) => {
if (state.mode !== "gradient") return;
// A thumb is read from its index: a keyboard-triggered click has no pointer position.
const thumb = event.target.closest(".gradient-thumb"), rect = gradientStops.getBoundingClientRect();
const last = state.gradient.length - 1;
// Thumbs sit at 10px + (width - 20px) * i / last, matching the CSS.
const fraction = Math.max(0, Math.min(1, (event.clientX - rect.left - 10) / Math.max(1, rect.width - 20)));
const index = thumb ? Number(thumb.dataset.index) : Math.round(fraction * last);
if (index !== state.active) { state.active = index; render(true); }
});
gradientAdd.addEventListener("click", () => { if (gradientAdd.getAttribute("aria-disabled") !== "true" && model.addGradientColor(state)) render(true); });
gradientRemove.addEventListener("click", () => { if (model.removeGradientColor(state)) render(true); });
originalPreview.addEventListener("click", () => { if (model.applySelection(state, originalSelection)) render(true); });
paletteToggle.addEventListener("click", () => {
setSidebarOpen(!palettePanel.classList.contains("open"));
if (initialized) emit("update_ui", uiState());
});
paletteCombo.addEventListener("change", () => {
populatePalette();
if (initialized) emit("update_ui", uiState());
});
paletteSearchToggle.addEventListener("click", () => setPaletteSearch(paletteSearchToggle.getAttribute("aria-pressed") !== "true"));
paletteFilter.addEventListener("input", populatePalette);
// Pointer capture ends by itself on pointerup/pointercancel, so it doubles as the drag state.
canvas.addEventListener("pointerdown", (event) => { canvas.setPointerCapture(event.pointerId); sampleSpectrum(event); });
canvas.addEventListener("pointermove", (event) => { if (canvas.hasPointerCapture(event.pointerId)) sampleSpectrum(event); });
// The spectrum takes the panel's free height, which also changes with the notice, alpha and
// gradient controls, so redraw on any size change rather than on window resize only.
new ResizeObserver(() => { drawSpectrum(); updateIndicator(displayHsl(model.activeColor(state))); }).observe(canvas.parentElement);
// Bubble phase on document runs after each editor's own handler has set its validity.
document.addEventListener("input", updateConfirm);
// Hex keeps its own message from its input handler; number fields get a translated one.
function validateEditors() {
const inputs = editors();
for (const input of inputs.filter((input) => input !== hexInput)) {
input.setCustomValidity("");
const {valid, rangeUnderflow, rangeOverflow} = input.validity;
if (!valid) input.setCustomValidity(T(rangeUnderflow || rangeOverflow ? "Value is out of range." : "Invalid input"));
}
return inputs.every((input) => input.reportValidity());
}
function confirm() {
if (initialized && validateEditors()) emit("confirm", {selection: model.exportSelection(state)});
}
preview.addEventListener("click", () => {
if (!initialized || !favoritesWritable || !validateEditors()) return;
const selection = model.exportSelection(state);
const key = JSON.stringify(selection);
favorites = [selection, ...favorites.filter((favorite) => JSON.stringify(favorite) !== key)].slice(0, FAVORITE_SLOTS);
drawFavorites(); emit("update_favorites", {favorites});
});
confirmBtn.addEventListener("click", confirm);
byId("cancelBtn").addEventListener("click", () => emit("cancel"));
document.addEventListener("keydown", (event) => {
// Escape in the filter box closes the search first; a second Escape cancels the dialog.
if (event.key === "Escape" && event.target === paletteFilter) { event.preventDefault(); setPaletteSearch(false); paletteSearchToggle.focus(); }
else if (event.key === "Escape") { event.preventDefault(); emit("cancel"); }
else if (event.key === "Enter" && event.target instanceof HTMLInputElement && ["text", "number"].includes(event.target.type)) {
event.preventDefault(); confirm();
}
});
window.ColorPickerDialog = {
init, handleMessage, focusInput, exportSelection: () => model.exportSelection(state),
setFavorites: (values) => { favorites = model.normalizeFavorites(values); drawFavorites(); }
};
// The host passes the saved sidebar state before the page loads (the same values come again in
// init), so the first paint already matches the window size the host opened with.
const earlyUi = window.ORCA_COLOR_PICKER_UI;
if (earlyUi && typeof earlyUi === "object") {
if (typeof earlyUi.palette === "string" && [...paletteCombo.options].some((option) => option.value === earlyUi.palette))
paletteCombo.value = earlyUi.palette;
if (typeof earlyUi.sidebar_open === "boolean") setSidebarOpen(earlyUi.sidebar_open);
}
setSwatch(originalPreview, originalSelection);
drawSpectrum(); populatePalette(); drawFavorites(); render(true); emit("ready");
})();
@@ -0,0 +1,103 @@
"use strict";
const {test} = require("node:test");
const assert = require("node:assert/strict");
const model = require("./colorpicker-model.js");
test("complete RGB/RGBA hex accepts mixed case and rejects partial or unbounded input", () => {
assert.deepEqual(model.parseHex("#Ab12cD80"), [171,18,205,128]);
for (const hex of ["", "123456", "#123", "#1234567", "#123456789", "#12345Z", "#123456GG", " #123456", null]) assert.equal(model.parseHex(hex), null);
for (let byte = 0; byte <= 255; ++byte) {
const hex = model.toHex([byte, byte, byte, byte]);
assert.deepEqual(model.parseHex(hex), [byte,byte,byte,byte]);
}
});
test("HSL conversion preserves primary channels and hue wraps at 360", () => {
for (const rgb of [[255,0,0], [0,255,0], [0,0,255], [128,128,128], [37,129,240]]) assert.deepEqual(model.hslToRgb(...model.rgbToHsl(...rgb)), rgb);
assert.deepEqual(model.hslToRgb(360,100,50), [255,0,0]);
assert.deepEqual(model.hslToRgb(240,100,50), [0,0,255]);
});
test("capabilities default off and reject gradient through every model entry", () => {
const gradient = {type: "gradient", colors: ["#FF000080", "#0000FF20"]};
const state = model.createState();
assert.deepEqual(state.options, {allow_gradient: false, allow_alpha: false});
assert.equal(model.createState({}, gradient), null);
assert.equal(model.applySelection(state, gradient), false);
assert.equal(model.setMode(state, "gradient"), false);
assert.equal(model.setAlphaPercent(state, 20), false);
assert.equal(model.createState({allow_alpha: true}, gradient), null);
const opaque = model.createState({allow_gradient: true}, gradient);
assert.deepEqual(model.exportSelection(opaque), {type: "gradient", colors: ["#FF0000FF", "#0000FFFF"]});
});
test("gradient endpoint edits keep order and preserve unedited byte alpha", () => {
const initial = {type: "gradient", colors: ["#FF000001", "#0000FF80"]};
const state = model.createState({allow_gradient: true, allow_alpha: true}, initial);
state.active = 0; model.setRgb(state, [0,255,0]);
state.active = 1; model.setHex(state, "#112233");
assert.deepEqual(model.exportSelection(state), {type: "gradient", colors: ["#00FF0001", "#11223380"]});
model.setAlphaPercent(state, 0);
assert.deepEqual(model.exportSelection(state).colors, ["#00FF0001", "#11223300"]);
model.setAlphaPercent(state, 100);
assert.equal(model.exportSelection(state).colors[1], "#112233FF");
assert.deepEqual(initial.colors, ["#FF000001", "#0000FF80"]);
});
test("favorites retain alpha and gradient even when the selection disables them", () => {
const favorite = {type: "solid", colors: ["#ABCDEF80"]};
const gradient = {type: "gradient", colors: ["#FFFFFF00", "#00000010"]};
const favorites = model.normalizeFavorites([favorite, gradient]);
const state = model.createState();
assert.equal(model.applySelection(state, favorites[0]), true);
assert.deepEqual(model.exportSelection(state), {type: "solid", colors: ["#ABCDEFFF"]});
assert.deepEqual(favorites, [favorite, gradient]);
assert.equal(model.applySelection(state, favorites[1]), false);
assert.equal(model.normalizeFavorites(Array(25).fill(favorite)).length, 24);
});
test("invalid cardinality and channel edits cannot change a selection", () => {
const state = model.createState({allow_alpha: true});
const before = model.exportSelection(state);
for (const value of [{type: "solid", colors: []}, {type: "solid", colors: ["#112233", "#445566"]}, {type: "gradient", colors: ["#112233"]}, {type: "other", colors: ["#112233"]}]) assert.equal(model.applySelection(state, value), false);
assert.equal(model.setHex(state, "#GG2233"), false);
for (const value of [-1, 101, NaN, Infinity]) assert.equal(model.setAlphaPercent(state, value), false);
assert.equal(model.setRgb(state, [256,0,0]), false);
assert.deepEqual(model.exportSelection(state), before);
});
test("multi-color gradients stay at 2 colors unless the caller raises the limit", () => {
const five = {type: "gradient", colors: ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"]};
assert.equal(model.normalizeSelection(five, {allow_gradient: true}), null);
assert.equal(model.createState({allow_gradient: true}, five), null);
const state = model.createState({allow_gradient: true, max_gradient_colors: 7}, five);
assert.equal(state.gradient.length, 5);
assert.equal(model.normalizeSelection({...five, colors: Array(8).fill("#000000")}, {allow_gradient: true}, 7), null);
assert.equal(model.createState({allow_gradient: true, max_gradient_colors: 99}).maxGradientColors, model.MAX_GRADIENT_COLORS);
assert.equal(model.normalizeFavorites([five]).length, 1, "favorites keep every color");
});
test("adding and removing gradient colors keeps order, selection and the 2..max limits", () => {
const state = model.createState({allow_gradient: true, allow_alpha: true, max_gradient_colors: 4}, {type: "gradient", colors: ["#FF000080", "#0000FFFF"]});
assert.equal(model.removeGradientColor(state), false, "never fewer than 2");
assert.equal(model.addGradientColor(state), true);
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080", "#0000FFFF"], "selected color turned 75° in hue, alpha kept");
assert.equal(model.NEW_GRADIENT_HUE_SHIFT, 75);
assert.equal(state.active, 1);
state.active = 2; assert.equal(model.addGradientColor(state), true);
assert.equal(state.active, 3); assert.equal(state.gradient.length, 4);
assert.equal(model.addGradientColor(state), false, "limit reached");
state.active = 1; assert.equal(model.removeGradientColor(state), true);
assert.deepEqual(model.exportSelection(state).colors.slice(0, 2), ["#FF000080", "#0000FFFF"]);
assert.equal(state.active, 1);
assert.equal(model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]}), true);
state.active = 1; model.addGradientColor(state); model.addGradientColor(state);
model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]});
assert.equal(state.active, 1, "a shorter gradient clamps the selected color");
});
test("a new gradient starts at the solid color and its 75° hue neighbour", () => {
const state = model.createState({allow_gradient: true, allow_alpha: true}, {type: "solid", colors: ["#FF000080"]});
assert.equal(model.setMode(state, "gradient"), true);
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080"]);
});
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en" data-orca-page="color-picker">
<head><title data-i18n="Color Picker">Color Picker</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../include/global.css">
<link rel="stylesheet" href="../css/theme.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<p id="selectionNotice" hidden data-i18n="The current multi-color selection is kept until you choose a different color.">The current multi-color selection is kept until you choose a different color.</p>
<fieldset id="pickerEditors" disabled>
<div class="main-wrapper">
<div class="left-panel" id="palettePanel">
<div class="palette thin-scroll" id="palette" tabindex="-1"></div>
<p class="palette-empty hidden" id="paletteEmpty" data-i18n="No matching colors">No matching colors</p>
<div class="palette-toolbar">
<div class="palette-select ComboBox" id="paletteSelect">
<div class="arrow-icon"></div>
<select id="paletteCombo" tabindex="-1">
<option value="basic" data-i18n="Basic Colors">Basic Colors</option>
<option value="orca" data-i18n="Orca Palette">Orca Palette</option>
<option value="ral" data-i18n="RAL Classic" >RAL Classic</option>
<option value="gradient" hidden data-i18n="Gradients" >Gradients</option>
</select>
</div>
<input type="search" id="paletteFilter" class="palette-filter hidden" autocomplete="off" spellcheck="false"
placeholder="Filter colors" data-i18n-placeholder="Filter colors" aria-label="Filter colors" data-i18n-aria-label="Filter colors">
<button type="button" id="paletteSearchToggle" class="palette-search-button" aria-pressed="false" title="Search colors" data-i18n-title="Search colors">
<div class="icon-search icon16"></div>
<div class="icon-close icon16"></div>
</button>
</div>
</div>
<div class="right-panel">
<div class="spectrum-row">
<div class="saturation-slider">
<input class="valueSlider" type="range" id="satSlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Saturation" data-i18n-aria-label="Saturation">
</div>
<div class="spectrum-container">
<canvas id="spectrum"></canvas>
<div id="colorIndicator"></div>
</div>
</div>
<div class="editor-grid">
<div class="editor-column">
<div id="modeControls" class="hidden">
<input class="switch-check" type="checkbox" id="modeSwitch">
<label class="switch-slider" for="modeSwitch">
<div class="switch-label" data-i18n="Solid">Solid</div>
<div class="switch-label" data-i18n="Gradient">Gradient</div>
</label>
</div>
<div class="hex-row">
<div class="inline-field field-hex">
<label for="hexInput">#</label>
<input type="text" id="hexInput" required value="808080" maxlength="8" autocomplete="off" spellcheck="false" aria-label="Hex color" data-i18n-aria-label="Hex color">
</div>
<div class="inline-field value-field hidden" id="alphaControls" title="Opacity percent">
<label for="oValue">
<svg viewBox="0 0 12 12" width="12" height="12">
<path d="M0,1v10c0,.55.45,1,1,1h10c.55,0,1-.45,1-1V1c0-.55-.45-1-1-1H1C.45,0,0,.45,0,1ZM6,6v5H1v-5h5V1h5v5h-5Z" style="fill:#999"/>
</svg>
</label>
<input type="number" id="oValue" min="0" max="100" aria-label="Opacity percent" data-i18n-aria-label="Opacity percent">
</div>
</div>
<!-- Third row: gradient endpoints, replaced by the opacity slider while the opacity field is in use. -->
<div id="thirdRow" class="third-row">
<!-- Gradient colors: evenly spaced thumbs (built by colorpicker.js); a click selects the nearest. -->
<div id="gradientCtrlBar" class="invisible" role="group" aria-label="Gradient" data-i18n-aria-label="Gradient">
<div id="gradientStops" class="gradient-stops">
<div class="gradient-track"></div>
</div>
<div class="gradient-step-buttons hidden">
<button type="button" id="gradientRemove" class="gradient-step-button hidden" title="Remove color" data-i18n-title="Remove color">
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/></svg>
</button>
<button type="button" id="gradientAdd" class="gradient-step-button hidden" title="Add color" data-i18n-title="Add color">
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/><rect x="4" y="0" width="2" height="10" fill="currentColor"/></svg>
</button>
</div>
</div>
<div id="opacitySliderBar" hidden>
<input class="valueSlider" type="range" id="opacitySlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Opacity" data-i18n-aria-label="Opacity">
</div>
</div>
</div>
<div class="editor-column">
<div class="inline-field value-field" title="Hue">
<label for="hValue">H</label>
<input type="number" id="hValue" min="0" max="360" step="1">
</div>
<div class="inline-field value-field" title="Saturation">
<label for="sValue">S</label>
<input type="number" id="sValue" min="0" max="100" step="1">
</div>
<div class="inline-field value-field" title="Lightness">
<label for="lValue">L</label>
<input type="number" id="lValue" min="0" max="100" step="1">
</div>
</div>
<div class="editor-column">
<div class="inline-field value-field" title="Red">
<label for="rValue">R</label>
<input type="number" id="rValue" min="0" max="255" step="1">
</div>
<div class="inline-field value-field" title="Green">
<label for="gValue">G</label>
<input type="number" id="gValue" min="0" max="255" step="1">
</div>
<div class="inline-field value-field" title="Blue">
<label for="bValue">B</label>
<input type="number" id="bValue" min="0" max="255" step="1">
</div>
</div>
</div>
<div class="bottom-container">
<button id="originalPreview" type="button" class="color-preview original-preview" title="Original color" data-i18n-title="Original color">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#ff6f00;"/>
<g style="fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;">
<polyline points="5.5 5.5 5.5 9.5 9.5 9.5" />
<path d="M5.84,9.5c.82-2.33,3.05-4,5.66-4,3.31,0,6,2.69,6,6s-2.69,6-6,6c-2.95,0-5.41-2.13-5.91-4.94"/>
</g>
</svg>
</button>
<button id="colorPreview" class="color-preview" title="Save current color" data-i18n-title="Save current color">
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#009688"/>
<path d="M11.85,5.11l1.43,4.39h4.61c.35,0,.5.45.22.66l-3.73,2.71,1.43,4.39c.11.34-.28.62-.56.41l-3.73-2.71-3.73,2.71c-.29.21-.67-.07-.56-.41l1.43-4.39-3.73-2.71c-.29-.21-.14-.66.22-.66h4.61s1.43-4.39,1.43-4.39c.11-.34.59-.34.7,0Z" style="fill:none; stroke:#fff; stroke-linecap:round; stroke-linejoin:round;"/>
</svg>
</button>
<div class="saved-grid" id="userColors"></div>
</div>
<div class="dialog-actions">
<button type="button" id="paletteToggle" class="icon-button" aria-expanded="false" aria-controls="palettePanel" title="Palettes" data-i18n-title="Palettes">
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="2" fill="none" stroke="currentColor"/><path d="M5.5 2.5v11" stroke="currentColor"/></svg>
</button>
<div class="stretch-spacer"></div>
<button id="confirmBtn" class="ButtonTypeChoice ButtonStyleConfirm" data-i18n="OK">OK</button>
<button id="cancelBtn" class="ButtonTypeChoice ButtonStyleRegular" data-i18n="Cancel">Cancel</button>
</div>
</div>
</div>
</fieldset>
<script src="palettes.js"></script><script src="colorpicker-model.js"></script><script src="colorpicker.js"></script></body></html>
@@ -0,0 +1,459 @@
"use strict";
(() => {
const cssColors = [
{hex: "#FF0000", name: "Red"},
{hex: "#DC143C", name: "Crimson"},
{hex: "#FF4500", name: "Orange red"},
{hex: "#FF6347", name: "Tomato"},
{hex: "#FFA500", name: "Orange"},
{hex: "#FF8C00", name: "Dark orange"},
{hex: "#FFD700", name: "Gold"},
{hex: "#DEB887", name: "Burly wood"},
{hex: "#FFE4C4", name: "Bisque"},
{hex: "#FFE4E1", name: "Misty rose"},
{hex: "#FFFF00", name: "Yellow"},
{hex: "#FFFFE0", name: "Light yellow"},
{hex: "#FFFACD", name: "Lemon chiffon"},
{hex: "#F5F5DC", name: "Beige"},
{hex: "#FAFAD2", name: "Light golden rod yellow"},
{hex: "#EEE8AA", name: "Pale golden rod"},
{hex: "#F0E68C", name: "Khaki"},
{hex: "#BDB76B", name: "Dark khaki"},
{hex: "#FFDAB9", name: "Peach puff"},
{hex: "#FFE4B5", name: "Moccasin"},
{hex: "#FFEFD5", name: "Papaya whip"},
{hex: "#FFEBCD", name: "Blanched almond"},
{hex: "#FAEBD7", name: "Antique white"},
{hex: "#FFDEAD", name: "Navajo white"},
{hex: "#F5DEB3", name: "Wheat"},
{hex: "#DAA520", name: "Golden rod"},
{hex: "#D2691E", name: "Chocolate"},
{hex: "#CD853F", name: "Peru"},
{hex: "#B22222", name: "Fire brick"},
{hex: "#A0522D", name: "Sienna"},
{hex: "#8B4513", name: "Saddle brown"},
{hex: "#A52A2A", name: "Brown"},
{hex: "#BC8F8F", name: "Rosy brown"},
{hex: "#FA8072", name: "Salmon"},
{hex: "#FFA07A", name: "Light salmon"},
{hex: "#E9967A", name: "Dark salmon"},
{hex: "#F4A460", name: "Sandy brown"},
{hex: "#FF7F50", name: "Coral"},
{hex: "#F08080", name: "Light coral"},
{hex: "#CD5C5C", name: "Indian red"},
{hex: "#8B0000", name: "Dark red"},
{hex: "#800000", name: "Maroon"},
{hex: "#ADFF2F", name: "Green yellow"},
{hex: "#9ACD32", name: "Yellow green"},
{hex: "#7FFF00", name: "Chartreuse"},
{hex: "#7CFC00", name: "Lawn green"},
{hex: "#32CD32", name: "Lime green"},
{hex: "#00FF7F", name: "Spring green"},
{hex: "#00FA9A", name: "Medium spring green"},
{hex: "#00FF00", name: "Lime"},
{hex: "#008000", name: "Green"},
{hex: "#228B22", name: "Forest green"},
{hex: "#006400", name: "Dark green"},
{hex: "#6B8E23", name: "Olive drab"},
{hex: "#808000", name: "Olive"},
{hex: "#556B2F", name: "Dark olive green"},
{hex: "#98FB98", name: "Pale green"},
{hex: "#90EE90", name: "Light green"},
{hex: "#3CB371", name: "Medium sea green"},
{hex: "#2E8B57", name: "Sea green"},
{hex: "#8FBC8F", name: "Dark sea green"},
{hex: "#5F9EA0", name: "Cadet blue"},
{hex: "#66CDAA", name: "Medium aqua marine"},
{hex: "#40E0D0", name: "Turquoise"},
{hex: "#48D1CC", name: "Medium turquoise"},
{hex: "#00CED1", name: "Dark turquoise"},
{hex: "#AFEEEE", name: "Pale turquoise"},
{hex: "#00FFFF", name: "Aqua"},
{hex: "#00FFFF", name: "Cyan"},
{hex: "#E0FFFF", name: "Light cyan"},
{hex: "#F5FFFA", name: "Mint cream"},
{hex: "#F0FFF0", name: "Honey dew"},
{hex: "#7FFFD4", name: "Aquamarine"},
{hex: "#20B2AA", name: "Light sea green"},
{hex: "#008B8B", name: "Dark cyan"},
{hex: "#008080", name: "Teal"},
{hex: "#2F4F4F", name: "Dark slate gray"},
{hex: "#778899", name: "Light slate gray"},
{hex: "#708090", name: "Slate gray"},
{hex: "#00BFFF", name: "Deep sky blue"},
{hex: "#1E90FF", name: "Dodger Blue"},
{hex: "#87CEEB", name: "Sky blue"},
{hex: "#87CEFA", name: "Light sky blue"},
{hex: "#ADD8E6", name: "Light blue"},
{hex: "#B0E0E6", name: "Powder blue"},
{hex: "#B0C4DE", name: "Light steel blue"},
{hex: "#4682B4", name: "Steel blue"},
{hex: "#6495ED", name: "Cornflower blue"},
{hex: "#4169E1", name: "Royal blue"},
{hex: "#0000FF", name: "Blue"},
{hex: "#0000CD", name: "Medium blue"},
{hex: "#191970", name: "Midnight blue"},
{hex: "#00008B", name: "Dark blue"},
{hex: "#000080", name: "Navy"},
{hex: "#6A5ACD", name: "Slate blue"},
{hex: "#483D8B", name: "Dark slate blue"},
{hex: "#7B68EE", name: "Medium slate blue"},
{hex: "#8A2BE2", name: "Blue violet"},
{hex: "#4B0082", name: "Indigo"},
{hex: "#9400D3", name: "Dark violet"},
{hex: "#9932CC", name: "Dark orchid"},
{hex: "#BA55D3", name: "Medium orchid"},
{hex: "#DA70D6", name: "Orchid"},
{hex: "#EE82EE", name: "Violet"},
{hex: "#9370DB", name: "Medium purple"},
{hex: "#800080", name: "Purple"},
{hex: "#663399", name: "Rebecca purple"},
{hex: "#C71585", name: "Medium violet red"},
{hex: "#DB7093", name: "PaleV violet red"},
{hex: "#FF1493", name: "Deep pink"},
{hex: "#FF69B4", name: "Hot pink"},
{hex: "#FFB6C1", name: "Light pink"},
{hex: "#FFC0CB", name: "Pink"},
{hex: "#DDA0DD", name: "Plum"},
{hex: "#D8BFD8", name: "Thistle"},
{hex: "#FF00FF", name: "Magenta"},
{hex: "#FF00FF", name: "Fuchsia"},
{hex: "#8B008B", name: "Dark magenta"},
{hex: "#FAF0E6", name: "Linen"},
{hex: "#FDF5E6", name: "Old lace"},
{hex: "#FFF5EE", name: "Sea shell"},
{hex: "#FFFFF0", name: "Ivory"},
{hex: "#FFFAF0", name: "Floral white"},
{hex: "#FFF0F5", name: "Lavender blush"},
{hex: "#F8F8FF", name: "Ghost white"},
{hex: "#F0F8FF", name: "Alice blue"},
{hex: "#E6E6FA", name: "Lavender"},
{hex: "#F5F5F5", name: "White smoke"},
{hex: "#DCDCDC", name: "Gainsboro"},
];
const palettes = {
basic: [
{hex: "#FF0000", name: "Red"},
{hex: "#FF8000", name: "Orange"},
{hex: "#FFFF00", name: "Yellow"},
{hex: "#80FF00", name: "Lime"},
{hex: "#00FF00", name: "Green"},
{hex: "#00FFFF", name: "Cyan"},
{hex: "#0000FF", name: "Blue"},
{hex: "#8000FF", name: "Purple"},
{hex: "#FF00FF", name: "Magenta"},
{hex: "#FF8080", name: "Pink"},
{hex: "#804000", name: "Brown"},
{hex: "#000000", name: "Black"},
{hex: "#191919", name: "%90"},
{hex: "#333333", name: "%80"},
{hex: "#4c4c4c", name: "%70"},
{hex: "#666666", name: "%60"},
{hex: "#808080", name: "%50"},
{hex: "#999999", name: "%40"},
{hex: "#b3b3b3", name: "%30"},
{hex: "#cccccc", name: "%20"},
{hex: "#e6e6e6", name: "%10"},
{hex: "#FFFFFF", name: "White"},
...cssColors
],
orca: [
{hex: "#ED1C24", name: "Coral fire"},
{hex: "#C41E3A", name: "Sea anemone"},
{hex: "#800000", name: "Deep abyss"},
{hex: "#B22222", name: "Lobster red"},
{hex: "#A0522D", name: "Sandy clay"},
{hex: "#8B4513", name: "Driftwood"},
{hex: "#D2691E", name: "Sea buckthorn"},
{hex: "#F26722", name: "Sunset reef"},
{hex: "#FF8C00", name: "Tiger coral"},
{hex: "#FA8173", name: "Pink coral"},
{hex: "#FFA07A", name: "Seashell glow"},
{hex: "#F4E2C1", name: "Sand"},
{hex: "#FFD700", name: "Golden kelp"},
{hex: "#F7B763", name: "Sunlit dune"},
{hex: "#FFAA33", name: "Amber nautilus"},
{hex: "#FFEB31", name: "Sea lemon"},
{hex: "#D4ED91", name: "Pale starfish"},
{hex: "#00FF7F", name: "Spring tide"},
{hex: "#32CD32", name: "Lime algae"},
{hex: "#A4C41E", name: "Sea grass"},
{hex: "#7CFC00", name: "Neon plankton"},
{hex: "#228B22", name: "Kelp forest"},
{hex: "#008000", name: "Emerald wave"},
{hex: "#345B2F", name: "Whale shadow"},
{hex: "#2E8B57", name: "Sea turtle"},
{hex: "#00C1AE", name: "Lagoon"},
{hex: "#20B2AA", name: "Tropical current"},
{hex: "#115877", name: "Midnight trench"},
{hex: "#008B8B", name: "Dark kelp"},
{hex: "#00FFFF", name: "Crystal surf"},
{hex: "#40E0D0", name: "Turquoise bay"},
{hex: "#2EBDEF", name: "Sky over reef"},
{hex: "#87CEEB", name: "Shallow sky"},
{hex: "#1E90FF", name: "Electric bel"},
{hex: "#4169E1", name: "Dolphin"},
{hex: "#0000CD", name: "Ocean depths"},
{hex: "#191970", name: "Whale"},
{hex: "#4B0082", name: "Indigo abyss"},
{hex: "#7841CE", name: "Sea urchin"},
{hex: "#9370DB", name: "Lavender coral"},
{hex: "#800080", name: "Violet tide"},
{hex: "#8B008B", name: "Deep sea orchid"},
{hex: "#FF00FF", name: "Neon jelly"},
{hex: "#ED1E79", name: "Fuchsia coral"},
{hex: "#FF69B4", name: "Pink jellyfish"},
{hex: "#FFFFFF", name: "Foam"},
{hex: "#D3D3D3", name: "Seashell white"},
{hex: "#808080", name: "Pebble gray"},
{hex: "#696969", name: "Storm cloud"},
{hex: "#000000", name: "Ink black"}
],
ral: [
{hex: "#716F52", name: "1000 Green beige"},
{hex: "#9B8E79", name: "1001 Beige"},
{hex: "#D4BB87", name: "1002 Sand yellow"},
{hex: "#F4A460", name: "1003 Signal yellow"},
{hex: "#EFBF04", name: "1004 Golden yellow"},
{hex: "#D7A500", name: "1005 Honey yellow"},
{hex: "#F2D185", name: "1006 Maize yellow"},
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
{hex: "#8B7D6B", name: "1011 Brown beige"},
{hex: "#F5DB50", name: "1012 Lemon yellow"},
{hex: "#F3E5AB", name: "1013 Oyster white"},
{hex: "#F8E4BC", name: "1014 Ivory"},
{hex: "#FAE6D3", name: "1015 Light ivory"},
{hex: "#E4B922", name: "1018 Zinc yellow"},
{hex: "#A19D94", name: "1019 Grey beige"},
{hex: "#B9975B", name: "1020 Olive yellow"},
{hex: "#B8860B", name: "1021 Rape yellow"},
{hex: "#FF8C00", name: "1023 Traffic yellow"},
{hex: "#CC7722", name: "1024 Ochre yellow"},
{hex: "#FDBA37", name: "1026 Traffic orange yellow"},
{hex: "#F3A55C", name: "1027 Curry"},
{hex: "#E87C33", name: "1028 Melon yellow"},
{hex: "#F2A641", name: "1032 Broom yellow"},
{hex: "#E09F39", name: "1033 Dahlia yellow"},
{hex: "#F0B76E", name: "1034 Pastel yellow"},
{hex: "#D1BCA0", name: "1035 Pearl beige"},
{hex: "#D4AF37", name: "1036 Pearl gold"},
{hex: "#F7C64F", name: "1037 Sun yellow"},
{hex: "#F28C38", name: "2000 Yellow orange"},
{hex: "#E55B2B", name: "2001 Deep orange"},
{hex: "#E34234", name: "2002 Vermilion"},
{hex: "#F27B33", name: "2003 Pearl orange"},
{hex: "#F37322", name: "2004 Pure orange"},
{hex: "#F75C03", name: "2005 Luminous orange"},
{hex: "#F46C1F", name: "2007 Luminous bright orange"},
{hex: "#E14423", name: "2008 Bright red orange"},
{hex: "#F28500", name: "2009 Traffic orange"},
{hex: "#F46A0A", name: "2010 Signal orange"},
{hex: "#E0490C", name: "2011 Deep orange"},
{hex: "#F46F42", name: "2012 Salmon orange"},
{hex: "#E6761C", name: "2013 Pearl orange"},
{hex: "#E8A18B", name: "2015 Pearl pink"},
{hex: "#B8612C", name: "2016 Copper orange"},
{hex: "#8B4513", name: "2017 Signal brown"},
{hex: "#C14A0F", name: "2018 Fir orange"},
{hex: "#BC2E1F", name: "2020 Signal red"},
{hex: "#D52713", name: "2021 Luminous red"},
{hex: "#9F261E", name: "2022 Carmine red"},
{hex: "#D53832", name: "2023 Pearl carmine red"},
{hex: "#C2341C", name: "2024 Pure red"},
{hex: "#B32D1C", name: "2025 Luminous deep red"},
{hex: "#A62320", name: "2027 Currant red"},
{hex: "#B9301E", name: "2028 Pearl ruby red"},
{hex: "#A91D2E", name: "3000 Fire red"},
{hex: "#A1262A", name: "3001 Signal red"},
{hex: "#8F2626", name: "3002 Carmine red"},
{hex: "#9B2428", name: "3003 Ruby red"},
{hex: "#872241", name: "3004 Purple red"},
{hex: "#722F37", name: "3005 Wine red"},
{hex: "#3D1818", name: "3007 Black red"},
{hex: "#8A3324", name: "3009 Oxide red"},
{hex: "#A22C29", name: "3011 Brown red"},
{hex: "#B22D20", name: "3012 Pine red"},
{hex: "#D23741", name: "3013 Tomato red"},
{hex: "#A8282E", name: "3014 Antique pink"},
{hex: "#C8283C", name: "3015 Light pink"},
{hex: "#D23F39", name: "3016 Coral red"},
{hex: "#D1425E", name: "3017 Rose"},
{hex: "#C4232F", name: "3018 Strawberry red"},
{hex: "#C8102E", name: "3020 Traffic red"},
{hex: "#E36A5A", name: "3022 Salmon pink"},
{hex: "#E63E62", name: "3024 Luminous rose"},
{hex: "#E02A27", name: "3026 Luminous bright red"},
{hex: "#B8315F", name: "3027 Raspberry red"},
{hex: "#C61C23", name: "3028 Pure red"},
{hex: "#AD2932", name: "3031 Orient red"},
{hex: "#D32F2F", name: "3032 Pearl ruby"},
{hex: "#D13C6F", name: "3033 Pearl pink"},
{hex: "#7B2B5F", name: "4001 Red purple"},
{hex: "#912E8C", name: "4002 Red violet"},
{hex: "#9C366F", name: "4003 Heather violet"},
{hex: "#6A1F49", name: "4004 Bordeaux violet"},
{hex: "#4B2663", name: "4005 Blue violet"},
{hex: "#5C2D3C", name: "4006 Mahogany"},
{hex: "#5E3559", name: "4007 Grey purple"},
{hex: "#9C3568", name: "4008 Signal violet"},
{hex: "#5E6E3A", name: "4009 Fir green"},
{hex: "#5B5B8A", name: "4010 Sabine grey"},
{hex: "#8F3A70", name: "4011 Pearl violet"},
{hex: "#5E3252", name: "4012 Pearl blackberry"},
{hex: "#1B4F72", name: "5001 Green blue"},
{hex: "#18669E", name: "5002 Ultramarine blue"},
{hex: "#0F4C8A", name: "5003 Sapphire blue"},
{hex: "#0D324A", name: "5004 Black blue"},
{hex: "#1B5583", name: "5005 Signal blue"},
{hex: "#2C5F9D", name: "5007 Brilliant blue"},
{hex: "#3B5AA9", name: "5008 Grey blue"},
{hex: "#0077A3", name: "5009 Azure blue"},
{hex: "#0A558C", name: "5010 Gentian blue"},
{hex: "#2B547E", name: "5011 Steel blue"},
{hex: "#0066A1", name: "5012 Light blue"},
{hex: "#124780", name: "5013 Cobalt blue"},
{hex: "#1F4E79", name: "5014 Pigeon blue"},
{hex: "#0384A7", name: "5015 Sky blue"},
{hex: "#006A9E", name: "5017 Traffic blue"},
{hex: "#005F87", name: "5018 Turquoise blue"},
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
{hex: "#04749F", name: "5020 Ocean blue"},
{hex: "#009AAA", name: "5021 Water blue"},
{hex: "#1A3D5F", name: "5022 Night blue"},
{hex: "#047AA6", name: "5023 Distant blue"},
{hex: "#5B9BB8", name: "5024 Pastel blue"},
{hex: "#3C6A98", name: "5025 Pearl gentian blue"},
{hex: "#1C3F5F", name: "5026 Pearl midnight blue"},
{hex: "#36673A", name: "6000 Patina green"},
{hex: "#0E5C34", name: "6001 Emerald green"},
{hex: "#437034", name: "6002 Leaf green"},
{hex: "#3F3F2A", name: "6003 Olive green"},
{hex: "#2B4E52", name: "6004 Blue green"},
{hex: "#3B5F3F", name: "6005 Moss green"},
{hex: "#4A563C", name: "6006 Grey olive"},
{hex: "#2B4638", name: "6007 Pine green"},
{hex: "#4A4139", name: "6008 Pine brown"},
{hex: "#3B3A2E", name: "6009 Fir green"},
{hex: "#3E7A5F", name: "6010 Grass green"},
{hex: "#5E716A", name: "6011 Reseda green"},
{hex: "#3A4E48", name: "6012 Brown green"},
{hex: "#6B8E23", name: "6013 Reed green"},
{hex: "#6B8E23", name: "6014 Yellow green"},
{hex: "#1E3E2F", name: "6015 Black green"},
{hex: "#1E4D4B", name: "6016 Turquoise green"},
{hex: "#4C7C47", name: "6017 May green"},
{hex: "#9CBF60", name: "6018 Yellow green"},
{hex: "#2E5A3A", name: "6019 Pine green"},
{hex: "#2E5A3A", name: "6020 Chrome oxide green"},
{hex: "#9DC0A5", name: "6021 Pale green"},
{hex: "#4A563C", name: "6022 Brown green"},
{hex: "#009E60", name: "6024 Traffic green"},
{hex: "#3F7044", name: "6025 Fern green"},
{hex: "#4F7942", name: "6026 Opal green"},
{hex: "#3B5F3F", name: "6027 Pine green"},
{hex: "#3B5F3F", name: "6028 Pine green"},
{hex: "#00A550", name: "6029 Mint green"},
{hex: "#3E5A40", name: "6031 Bronze green"},
{hex: "#3D7E5A", name: "6032 Pearl green"},
{hex: "#007D6E", name: "6033 Sax turquoise"},
{hex: "#3D7E5A", name: "6034 Pearl turquoise"},
{hex: "#3D7E5A", name: "6035 Pearl green"},
{hex: "#3D7E5A", name: "6036 Pearl jade"},
{hex: "#00A550", name: "6037 Pure green"},
{hex: "#00A550", name: "6038 Luminous green"},
{hex: "#00A550", name: "6039 Mint green"},
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
{hex: "#A8A9AD", name: "7001 Silver grey"},
{hex: "#6F7368", name: "7002 Olivestone"},
{hex: "#8F9080", name: "7003 Moss grey"},
{hex: "#8E9291", name: "7004 Signal grey"},
{hex: "#8B8788", name: "7005 Mouse grey"},
{hex: "#9A8373", name: "7006 Beige grey"},
{hex: "#828C6A", name: "7008 Khaki grey"},
{hex: "#6F7368", name: "7009 Green grey"},
{hex: "#5F6563", name: "7010 Tarpaulin grey"},
{hex: "#4A5656", name: "7011 Iron grey"},
{hex: "#4D5656", name: "7012 Basalt grey"},
{hex: "#635147", name: "7013 Brown grey"},
{hex: "#6B6B83", name: "7015 Slate grey"},
{hex: "#32373D", name: "7016 Anthracite grey"},
{hex: "#8B8B83", name: "7021 Light grey"},
{hex: "#716347", name: "7022 Umber grey"},
{hex: "#76787A", name: "7023 Concrete grey"},
{hex: "#5E6565", name: "7024 Graphite grey"},
{hex: "#5E6565", name: "7026 Granite grey"},
{hex: "#8F9494", name: "7030 Stone grey"},
{hex: "#4A5656", name: "7031 Iron grey"},
{hex: "#B2BFC0", name: "7032 Pebble grey"},
{hex: "#9B9B9B", name: "7033 Cement grey"},
{hex: "#8F9D9B", name: "7034 Yellow grey"},
{hex: "#CBD0CC", name: "7035 Light grey"},
{hex: "#6D7A80", name: "7036 Platinum grey"},
{hex: "#8A9191", name: "7037 Dusty grey"},
{hex: "#7E8181", name: "7038 Agate grey"},
{hex: "#6E7D7E", name: "7039 Quartz grey"},
{hex: "#8A9191", name: "7040 Window grey"},
{hex: "#8A9191", name: "7042 Traffic grey A"},
{hex: "#6E7D7E", name: "7043 Traffic grey B"},
{hex: "#9B9B9B", name: "7044 Silk grey"},
{hex: "#A1A7B0", name: "7045 Telegrey 1"},
{hex: "#7E848C", name: "7046 Telegrey 2"},
{hex: "#7E848C", name: "7047 Telegrey 4"},
{hex: "#6D7A80", name: "7048 Pearl mouse grey"},
{hex: "#786D5F", name: "8000 Green brown"},
{hex: "#8B7355", name: "8001 Ochre brown"},
{hex: "#8B6F47", name: "8003 Clay brown"},
{hex: "#8B6F47", name: "8004 Copper brown"},
{hex: "#5C4033", name: "8007 Chocolate brown"},
{hex: "#6E5C4A", name: "8008 Olive brown"},
{hex: "#8B4513", name: "8011 Nut brown"},
{hex: "#8B4513", name: "8012 Red brown"},
{hex: "#5C3317", name: "8014 Sepia brown"},
{hex: "#854636", name: "8015 Chestnut brown"},
{hex: "#8B4513", name: "8016 Mahogany brown"},
{hex: "#5C3317", name: "8017 Chocolate brown"},
{hex: "#786D5F", name: "8019 Grey brown"},
{hex: "#854636", name: "8022 Chestnut brown"},
{hex: "#8B6F47", name: "8023 Pearl copper"},
{hex: "#8B7355", name: "8024 Beige brown"},
{hex: "#8B6F47", name: "8025 Pearl mocha brown"},
{hex: "#8B6F47", name: "8027 Clay brown"},
{hex: "#8B6F47", name: "8028 Earth brown"},
{hex: "#8B6F47", name: "8029 Pearl brown"},
{hex: "#FAF0E6", name: "9001 Cream"},
{hex: "#EDECE6", name: "9002 Grey white"},
{hex: "#FFFFFF", name: "9003 Signal white"},
{hex: "#000000", name: "9004 Signal black"},
{hex: "#1D1D1D", name: "9005 Jet black"},
{hex: "#C0C0C0", name: "9006 White aluminium"},
{hex: "#8C7853", name: "9007 Grey aluminium"},
{hex: "#FFFFFF", name: "9010 Pure white"},
{hex: "#1D1D1D", name: "9011 Graphite black"},
{hex: "#FFFFFF", name: "9016 Traffic white"},
{hex: "#000000", name: "9017 Traffic black"},
{hex: "#FAF0E6", name: "9018 Papyrus white"},
{hex: "#B2BFC0", name: "9022 Pearl light grey"}
],
gradient: [
{grad: "#FFF47B-#CD9800", name: "Metal: Gold"},
{grad: "#373737-#F5F4F4", name: "Metal: Silver"},
{grad: "#C39292-#C73725", name: "Metal: Copper"},
{grad: "#DED699-#C99620", name: "Metal: Brass"},
{grad: "#6E4316-#B0742E", name: "Metal: Bronze"},
// More than two colors: only listed when the page enables multi-color gradients.
{grad: "#FF0000-#FFFF00-#00FF00-#00FFFF-#0000FF-#FF00FF-#FF0000", name: "Nature: Rainbow"},
{grad: "#D8FEE7-#FFF5BD", name: "Nature: Pearl"},
{grad: "#1E5AA8-#3D8BD9-#7DB9F0-#C9E6FF", name: "Nature: Clear Sky"},
{grad: "#2B1B5A-#7A2E7E-#D9466E-#F57C3A-#FFC857", name: "Nature: Sunset"},
{grad: "#3B2416-#5A3A24-#45291A-#6B4630-#3B2416", name: "Wood: Dark"},
{grad: "#8B5A2B-#A8733F-#946235-#B5844F-#8B5A2B", name: "Wood: Medium"},
{grad: "#D9B98C-#E8CFA6-#D3AE7C-#EBD5B0-#D9B98C", name: "Wood: Light"},
]
};
window.ColorPickerPalettes = palettes;
})();
@@ -0,0 +1,534 @@
:root {
--thumb-size : 20px;
--field-height: 28px;
--number-width: 52px;
--preview-size: 52px;
--checkerboard: conic-gradient(var(--checker-light) 25%, var(--checker-dark) 0 50%, var(--checker-light) 0 75%, var(--checker-dark) 0);
}
* {
padding: 0;
border: 0;
margin: 0;
user-select: none;
-webkit-user-select: none;
box-sizing: border-box;
min-width: 0;
min-height: 0;
}
html, body {
/* Report the full content width even before the native host expands its viewport.
WebKit's fit-content can clip the sidebar and retain the expanded width on close. */
width: max-content;
overflow: hidden;
}
body {
font-family: var(--orca-font, system-ui, sans-serif);
background: var(--bg-color);
color: var(--fg-color-text);
padding: 10px;
}
#selectionNotice {
width: 0;
min-width: 100%;
margin-bottom: 8px;
color: var(--fg-color-label);
}
.main-wrapper {
display: flex;
gap: 10px;
height: 360px;
}
/* ---------- Palette sidebar ---------- */
.left-panel {
flex: 0 0 160px;
width: 160px;
display: none;
flex-direction: column;
}
.left-panel.open {
display: flex;
padding-right: 0px;
border-right: 1px solid var(--border-color);
}
#paletteCombo {
font-size: 13px;
width: 100%;
}
/* Combo box or filter box, with the search / close button on the right. */
.palette-toolbar {
display: flex;
align-items: center;
gap: 0;
height: 25px;
margin-right:5px;
}
.palette-toolbar > .palette-select { flex: 1 }
.palette-filter {
flex: 1;
height: 25px;
padding: 0 6px;
font-size: 13px;
font-family: inherit;
color: var(--fg-color-text);
background: var(--bg-color);
border: 1px solid var(--main-color);
outline: none;
-webkit-user-select: text;
user-select: text;
}
.palette-filter::-webkit-search-cancel-button { -webkit-appearance: none }
.palette-filter:focus-visible{
background-color: var(--focus-bg-box);
}
.palette-search-button {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
background-color: var(--button-bg-normal);
border: 1px solid var(--border-color);
border-left: 0;
}
.palette-search-button:hover {
background-color: var(--button-bg-hover);
}
.palette-search-button .icon-search {
--icon-url: var(--icon-search);
}
.palette-search-button .icon-close {
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g style="fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round"><line x1="3.5" y1="2.5" x2="13.5" y2="12.5"/><line x1="3.5" y1="12.5" x2="13.5" y2="2.5"/></g></svg>');;
}
.palette-search-button .icon-close,
.palette-search-button[aria-pressed="true"] .icon-search { display: none }
.palette-search-button[aria-pressed="true"] .icon-close { display: block }
.palette-empty {
flex: 1;
padding: 8px 2px;
font-size: 13px;
color: var(--fg-color-label);
}
.palette {
flex: 1;
overflow-y: auto;
padding-right: 1px;
margin-bottom: 5px;
}
.color-item {
display: flex;
align-items: center;
padding: 1px 2px 1px 0;
border:1px solid transparent;
font-size: 13px;
color: var(--fg-color-text);
}
.color-item:hover {
border-color: var(--main-color);
}
.color-swatch {
flex: 0 0 18px;
height: 18px;
border-radius: 3px;
margin-right: 5px;
margin: 0 5px 0 1px;
border: 1px solid var(--border-color);
}
.color-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ---------- Picker ---------- */
.right-panel {
flex: 0 0 330px;
/* WebKit uses width, rather than flex-basis, for the parent's intrinsic width. */
width: 330px;
display: flex;
flex-direction: column;
}
.spectrum-row {
--spectrum-height: 150px;
display: flex;
gap: 8px;
height: var(--spectrum-height);
margin-bottom: 8px;
}
.spectrum-container {
position: relative;
flex: 1;
border-radius: 7px;
overflow: hidden;
}
#spectrum {
position: absolute; /* Absolute so the canvas's pixel size never feeds back into the flex layout */
inset: 0;
width: 100%;
height: 100%;
touch-action: none;
}
#pickerEditors:disabled #spectrum { pointer-events: none }
#colorIndicator {
position: absolute;
width: var(--thumb-size);
height: var(--thumb-size);
border: 2px solid var(--color-marker-outline);
box-shadow: 0 0 0 2px var(--color-marker-shadow);
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%);
z-index: 10;
}
.saturation-slider {
position: relative;
flex: 0 0 var(--thumb-size);
}
#satSlider {
position: absolute;
top: 50%;
left: 50%;
flex: none;
width: var(--spectrum-height);
transform: translate(-50%, -50%) rotate(-90deg);
}
.valueSlider {
height: 14px;
border-radius: 8px;
outline: none;
-webkit-appearance: none;
background: transparent;
}
.valueSlider::-webkit-slider-runnable-track {
height: 14px;
border-radius: 8px;
background: var(--track-gradient);
border: 1px solid var(--border-color);
}
.valueSlider::-webkit-slider-thumb {
-webkit-appearance: none;
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 50%;
border: 2px solid var(--color-marker-outline);
box-shadow: 0 0 0 2px var(--color-marker-shadow);
background: var(--thumb-color);
margin-top: -4px;
}
/* Left column: mode switch, hex + alpha, gradient endpoints. Right: H S L and R G B columns. */
.editor-grid {
--editor-grid-gap: 5px;
display: flex;
gap: var(--editor-grid-gap);
margin-bottom: 10px;
}
.editor-column:first-of-type {flex:1}
.editor-column {
display: grid;
grid-template-rows: repeat(3, var(--field-height));
gap: var(--editor-grid-gap);
}
.hex-row {
display: flex;
gap: var(--editor-grid-gap);
}
.switch-check {
display: none !important;
}
.switch-label {
font-size: 13px;
padding: 2px 10px;
text-align: center;
line-height: 18px;
z-index: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.switch-slider {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
border-radius: 20px;
background: var(--bg-color-alt);
border: 2px solid var(--bg-color-alt);
height:26px;
}
.switch-slider:before {
content: '';
position: absolute;
width: 50%;
height: 100%;
left: 0%;
border-radius:20px;
background: var(--main-color);
transition: all 0.2s;
}
.switch-check:checked + .switch-slider:before {
left: 50%;
}
.switch-check:checked + .switch-slider div:first-child,
.switch-check + .switch-slider div:last-child{
color: var(--fg-color-label);
transition: all 0.2s;
}
.switch-check:checked + .switch-slider div:last-child,
.switch-check + .switch-slider div:first-child{
color: var(--button-fg-light);
transition: all 0.2s;
}
/* H S L / R G B / Hex / Alpha input boxes */
.inline-field {
display: flex;
align-items: center;
padding-left: 5px;
border: 1px solid var(--border-color);
background: var(--bg-color);
}
.inline-field > label {
width: 8px;
font-size: 12px;
color: var(--fg-color-text);
opacity: 0.6;
}
.inline-field > input {
flex: 1;
border: 0;
background: transparent;
padding: 0 2px 0 5px;
color: var(--fg-color-text);
font-size: 15px;
line-height: var(--field-height);
appearance: textfield; /* No spinner buttons; Up/Down keys and the wheel still step by 1. */
}
.inline-field:focus-within {
border-color: var(--main-color);
background: var(--focus-bg-box);
}
.inline-field:has(input:invalid) {
border-color: var(--control-invalid);
}
.inline-field > input:focus {
outline: none;
}
.inline-field > input[type="number"]::-webkit-inner-spin-button,
.inline-field > input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none }
.value-field { width: var(--number-width)}
.field-hex { flex: 1 }
#alphaControls {
width: calc(var(--number-width) + 2px)
}
#alphaControls > input {
padding-left: 7px;
}
/* Third row holds the gradient bar and the opacity slider in the same cell. The slider only
shows while the opacity field or the slider itself is in use, and hides the gradient bar. */
.third-row { position: relative }
.third-row > * {
position: absolute;
inset: 0;
height: 100%;
align-items: center;
}
.third-row.opacity-open > #opacitySliderBar { display: flex }
.third-row.opacity-open > #gradientCtrlBar { display: none }
#opacitySlider { width: 100%; }
#opacitySlider::-webkit-slider-runnable-track {
background: var(--track-gradient), var(--checkerboard); /* Transparent end shows the checkerboard through the color. */
background-size: auto, 8px 8px;
}
/* Gradient colors: thumbs are evenly spaced and fixed; a click anywhere selects the nearest. */
#gradientCtrlBar {
display: flex;
}
.gradient-stops {
position: relative;
flex: 1;
height: 100%;
}
/* Kept 10px away from the gradient bar so a click near its right end can't hit − or +. */
.gradient-step-buttons {
display: flex;
align-items: center;
gap: 6px;
margin-left: 10px;
margin-right: 4px;
}
.gradient-step-button {
display: flex;
align-items: center;
justify-content: center;
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 4px;
background: var(--button-bg-normal);
color: var(--icon-color);
}
.gradient-step-button svg { display: block; flex: none }
.gradient-step-button:hover:not([aria-disabled="true"]) { background: var(--button-bg-hover) }
.gradient-step-button[aria-disabled="true"] { opacity: 0.4; cursor: default }
.gradient-track {
position: absolute;
inset: 0;
top: 50%;
height: var(--thumb-size);
transform: translateY(-50%);
border-radius: 4px;
border: 1px solid var(--border-color);
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
background-size: auto, 8px 8px;
}
.gradient-thumb {
position: absolute;
top: 50%;
width: 10px;
height: 10px;
border-radius: 1px;
border: 1px solid var(--color-marker-outline);
box-shadow: 0 0 0 1px var(--color-marker-shadow);
background: var(--thumb-color);
transform: translate(-50%, -50%);
}
/* Thumb i of n sits at 10px + (100% - 20px) * i / (n - 1), set inline as --stop. */
.gradient-thumb { left: calc(10px + (100% - 20px) * var(--stop, 0)) }
.gradient-thumb[aria-pressed="true"] {
width: var(--thumb-size);
height: var(--thumb-size);
border: 2px solid var(--color-marker-outline);
box-shadow: 0 0 0 2px var(--color-marker-shadow);
border-radius: 4px;
}
.gradient-thumb:hover {
box-shadow: 0 0 0 3px var(--color-marker-shadow);
}
/* ---------- Original / current / favorites ---------- */
.bottom-container {
display: flex;
align-items: center;
gap: 4px;
height: var(--preview-size);
}
.color-preview {
display: flex;
align-items: center;
justify-content: center;
width: var(--preview-size);
height: var(--preview-size);
border-radius: 4px;
border: 1px solid var(--border-color);
}
.color-preview svg {
display: none;
pointer-events: none;
}
.color-preview:hover svg {
display: block;
}
.saved-grid {
display: grid;
grid-template-columns: repeat(8, minmax(0, 24px));
grid-template-rows: repeat(2, minmax(0, 24px));
gap: 4px;
}
.saved-swatch {
position: relative;
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid var(--border-color);
cursor: pointer;
}
.saved-swatch.empty {
background: var(--bg-color);
cursor: default;
}
/* Each layer is separate so transparent RGBA shows the checkerboard beneath it. */
.color-swatch, .saved-swatch, .color-preview {
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
background-size: auto, 12px 12px;
}
.color-item { width: 100%; background: transparent; text-align: left; font-family: inherit; }
/* ---------- Actions ---------- */
.dialog-actions {
display: flex;
align-items: center;
gap: 8px;
padding-top: 12px;
}
.dialog-actions button { margin: 0 }
#confirmBtn[disabled] { opacity: 0.5 }
.stretch-spacer { flex: 1 }
.icon-button {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: var(--button-bg-normal);
color: var(--fg-color-text);
}
.icon-button:hover {
background: var(--button-bg-hover);
}
.icon-button[aria-expanded="true"] {
background: var(--main-color);
color: var(--button-fg-light);
}
.icon-button[aria-expanded="true"]:hover {
background: var(--main-color-hover);
}
.hidden { display: none !important }
.invisible { visibility: hidden }
.color-item:focus-visible, .saved-swatch:focus-visible,
select:focus-visible, button:focus-visible { outline: 2px solid var(--main-color); outline-offset: -1px; }
@@ -18,7 +18,7 @@
<script src="../js/plugin-config-ui.js"></script>
<script src="./index.js"></script>
<script src="./plugin-sort.js"></script>
<script src="../js/fuzzy-search.js"></script>
<script src="../../js/fuzzy-search.js"></script>
<script src="./plugin-search.js"></script>
</head>
<body onLoad="OnInit()">
+9
View File
@@ -55,6 +55,13 @@
/* Speed dial icon tile treatment */
--speed-tile-bg: #efefef;
--speed-tile-border: #d8d8d8;
/* Transparency and control markers are UI surfaces, never color data. */
--checker-light: #eeeeee;
--checker-dark: #aaaaaa;
--color-marker-outline: #ffffff;
--color-marker-shadow: rgba(0, 0, 0, 0.7);
--control-invalid: var(--plugin-status-danger);
}
:root[data-orca-theme="dark"] {
@@ -93,6 +100,8 @@
/* Speed dial icon tile treatment */
--speed-tile-bg: #34343b;
--speed-tile-border: #50505a;
--checker-light: #68686f;
--checker-dark: #414148;
}
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
+6 -2
View File
@@ -156,12 +156,16 @@ function SetCloudMode( bOn )
m_CloudMode=bOn;
$("body").toggleClass("cloud-mode", bOn);
if( bOn )
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
}
}
function GotoCloud()
{
$(".BtnItem").removeClass("BtnItemSelected");
$(".BtnShortcut").addClass("BtnItemSelected");
SetCloudMode(true);
SendSimpleCommand("homepage_open_cloud");
}
@@ -336,10 +336,10 @@ modules:
sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f
dest: external-packages/PNG
# libjpeg-turbo 3.0.1
# libjpeg-turbo 3.2.0
- type: file
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip
sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f
url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip
sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63
dest: external-packages/JPEG
# Freetype 2.12.1
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1
# WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
# binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
# Work-around https://github.com/bambulab/BambuStudio/issues/3440
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
+21 -15
View File
@@ -163,6 +163,7 @@ using json = nlohmann::json;
#include <X11/Xlib.h>
#endif
#include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif
#ifdef SLIC3R_GUI
@@ -1156,8 +1157,10 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path still applies it on
// XWayland sessions as a conservative fallback for older WebKit.
// Wizard / login / store. The default path applies it only where it
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really
// runs through XWayland), and only when the linked WebKitGTK predates
// the fix (< 2.42), as a conservative fallback for older WebKit.
// ------------------------------------------------------------------
{
const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1165,6 +1168,14 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list).
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
// Force the shared-memory transport, which keeps compositing and threaded
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
}
if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -1204,23 +1215,18 @@ int CLI::run(int argc, char **argv)
if (wayland_env && *wayland_env) {
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
::setenv("GDK_BACKEND", "x11", true);
// GTK will use X11 in a Wayland session here, which is the
// one default-path case that really runs through XWayland.
// WebKit2GTK accelerated compositing could fail silently
// there on releases before 2.42, so force it off. Disabling
// it also turns off threaded scrolling, hence the gate.
if (WebView::NeedsCompositingWorkaround())
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
#endif
// WebKit2GTK compositing can fail under XWayland on older
// WebKit releases. Disable it only when both DISPLAY and
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
// use). On pure X11 or native Wayland, compositing is left
// enabled so WebKit retains HW acceleration.
{
const char* display_env_wk = ::getenv("DISPLAY");
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
// XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink
// (no Xlib involved), so the call is skipped.
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink
export WEBKIT_DISABLE_DMABUF_RENDERER=1
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
fi
fi
fi
+27 -18
View File
@@ -432,27 +432,36 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string OozePrevention::pre_toolchange(GCode& gcodegen)
{
std::string gcode;
unsigned int extruder_id = gcodegen.writer().filament()->id();
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) {
// There is no idle temperature defined in filament settings.
// Use the delta value from print config.
if (gcodegen.config().standby_temperature_delta.value != 0) {
// we assume that heating is always slower than cooling, so no need to block
gcode += gcodegen.writer().set_temperature
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
}
} else {
// Use the value from filament settings. That one is absolute, not delta.
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
// Check if this tool will ever be used again in this print.
bool is_last_use = false;
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
const auto& cfg = gcodegen.config();
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
for (unsigned int f : tool_ordering.all_extruders())
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
}
int temp = 0;
if (!is_last_use) {
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) {
if (gcodegen.config().standby_temperature_delta.value == 0)
return std::string();
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
} else {
temp = filament_idle_temp.get_at(extruder_id);
}
}
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
return gcode;
}
+1
View File
@@ -1004,6 +1004,7 @@ protected:
size_t num_objects,
size_t num_islands);
friend class OozePrevention;
friend class Wipe;
friend class WipeTowerIntegration;
friend class PressureEqualizer;
+14 -1
View File
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
sort_remove_duplicates(m_all_printing_extruders);
}
m_last_layer_per_extruder.clear();
for (const LayerTools& lt : m_layer_tools) {
for (unsigned int ext : lt.extruders) {
m_last_layer_per_extruder[ext] = lt.print_z;
}
}
if (prime_multi_material && ! m_all_printing_extruders.empty()) {
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
// Then set m_first_printing_extruder to the 1st extruder primed.
@@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
return -1;
}
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
{
auto it = m_last_layer_per_extruder.find(extruder_id);
if (it == m_last_layer_per_extruder.end())
return true;
return print_z >= it->second - EPSILON;
}
} // namespace Slic3r
+3
View File
@@ -370,6 +370,7 @@ public:
std::vector<int> & initial_filaments);
bool has_non_support_filament(const PrintConfig &config);
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
private:
void initialize_layers(std::vector<coordf_t> &zs);
@@ -413,6 +414,8 @@ private:
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
// no-ops; downstream emission falls through to the per-object branch).
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
bool m_sorted = false;
+4 -5
View File
@@ -858,11 +858,10 @@ void PrintObject::contour_z()
BOOST_LOG_TRIVIAL(debug) << "Contouring in parallel - start";
TriangleMesh mesh = this->m_model_object->raw_mesh();
if (m_model_object->instances.size() != 1) {
throw RuntimeError("ContourZ: unexpected number of instances");
}
ModelInstance *inst = m_model_object->instances.front();
// 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;
Point center_offset = this->center_offset();
Geometry::Transformation trans = inst->get_transformation();
+5 -70
View File
@@ -91,16 +91,12 @@ bool DecodedHeightTexture::texel_tap(const Vec2f &uv, bool tile_enabled, Texture
const float fy = v * float(height);
const int x0 = std::clamp(int(std::floor(fx)), 0, width - 1);
const int y0 = std::clamp(int(std::floor(fy)), 0, height - 1);
// Neighbour for bilinear filtering. Repeat wraps, because its next tile really does start at the
// opposite edge. The other two clamp: an untiled placement has nothing past its border, and a
// mirrored tile folds back on itself there, so the texel past the edge is the edge again. Wrapping
// a mirrored seam blends the two opposite edges of the image together, which is both a visible
// line and a disagreement with the preview - the GPU path binds GL_MIRRORED_REPEAT, which folds.
const bool wrap_neighbour = tile_enabled && tile_method != TextureTileMethod::MirroredRepeat;
// Neighbour for bilinear filtering: wrap for tiling methods, clamp at the edge otherwise (a
// repeating neighbour would incorrectly blend against the opposite edge of the image).
tap.x0 = x0;
tap.y0 = y0;
tap.x1 = wrap_neighbour ? (x0 + 1) % width : std::min(x0 + 1, width - 1);
tap.y1 = wrap_neighbour ? (y0 + 1) % height : std::min(y0 + 1, height - 1);
tap.x1 = tile_enabled ? (x0 + 1) % width : std::min(x0 + 1, width - 1);
tap.y1 = tile_enabled ? (y0 + 1) % height : std::min(y0 + 1, height - 1);
tap.tx = fx - std::floor(fx);
tap.ty = fy - std::floor(fy);
return true;
@@ -696,68 +692,7 @@ TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer)
for (int i = 0; i < 8; ++i)
top += bins[size_t(i)];
out.flat_share = float(double(top) / double(npx));
// How much of the principal colour axis carries pixels. Sampled rather than exhaustive: the
// axis is a bulk property, and a stride keeps this a few milliseconds on a large image.
{
constexpr size_t MAX_SAMPLES = size_t(1) << 16;
const size_t stride = std::max<size_t>(1, npx / MAX_SAMPLES);
std::vector<Vec3f> pts;
pts.reserve(npx / stride + 1);
for (size_t i = 0; i < npx; i += stride)
pts.push_back(tex.has_color() ?
Vec3f(float(tex.rgb[i * 3]), float(tex.rgb[i * 3 + 1]), float(tex.rgb[i * 3 + 2])) :
Vec3f::Constant(float(tex.pixels[i])));
if (pts.size() >= 16) {
Vec3f mean = Vec3f::Zero();
for (const Vec3f &p : pts)
mean += p;
mean /= float(pts.size());
// Power iteration on the covariance: the axis, without forming the matrix.
Vec3f axis(1.f, 0.3f, 0.2f);
for (int it = 0; it < 24; ++it) {
Vec3f next = Vec3f::Zero();
for (const Vec3f &p : pts) {
const Vec3f d = p - mean;
next += d.dot(axis) * d;
}
const float n = next.norm();
if (n < 1e-6f)
break;
axis = next / n;
}
std::vector<float> proj(pts.size());
for (size_t i = 0; i < pts.size(); ++i)
proj[i] = (pts[i] - mean).dot(axis);
std::sort(proj.begin(), proj.end());
// Trimmed, so a handful of stray pixels cannot stretch the range and make a flat
// image look like it spans one.
const float lo = proj[size_t(0.01 * double(proj.size()))];
const float hi = proj[std::min(proj.size() - 1, size_t(0.99 * double(proj.size())))];
const float span = hi - lo;
if (span > 1e-3f) {
constexpr int BUCKETS = 32;
std::array<size_t, BUCKETS> hist{};
for (const float s : proj)
if (s >= lo && s <= hi)
++hist[size_t(std::min(BUCKETS - 1, int((s - lo) / span * BUCKETS)))];
// A bucket counts as carrying pixels at a fifth of a percent of them, which is
// above the noise a dithered image leaves between its colours.
const size_t floor_count = size_t(0.002 * double(proj.size()));
int filled = 0;
for (const size_t c : hist)
filled += c >= floor_count;
out.axis_coverage = float(filled) / float(BUCKETS);
}
}
}
// Both signals, because either alone misreads a common case: the histogram calls a
// two-colour gradient flat (measured 0.90 on a green-to-red ramp, and 0.96 on a painted
// model texture), while coverage alone would call a noisy two-colour logo a gradient. An
// image whose axis could not be measured keeps coverage at 0, so the flat-share test decides
// alone - which is what an image of a single colour needs.
out.flat_colors = out.flat_share >= 0.85f && out.axis_coverage < 0.25f;
out.flat_colors = out.flat_share >= 0.85f;
}
std::lock_guard<std::mutex> lock(g_texture_detail_cache.mutex);
auto &entries = g_texture_detail_cache.entries;
-9
View File
@@ -504,15 +504,6 @@ struct TextureDetail
// verdict: a "flat-colour" image (tiles, logos, camouflage) whose colours should each print in a
// single filament, versus a photograph or gradient where interleaved filament mixes pay off.
float flat_share = 0.f;
// Share of the image's principal colour axis that actually carries pixels, in 32 steps. A ramp
// covers it end to end; a few flat colours sit in spikes with gaps between them. The histogram
// above cannot tell those apart - a two-colour gradient occupies as few bins as the two colours
// on their own - so both are needed before an image counts as flat.
//
// Zero when there was nothing to measure: an image of one colour, or too few pixels to fit an
// axis through. That is the right default, because it leaves the flat-share test deciding on its
// own, exactly as it did before this signal existed.
float axis_coverage = 0.f;
bool flat_colors = false;
};
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer);
+8
View File
@@ -129,6 +129,8 @@ 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
@@ -220,6 +222,7 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoUtils.hpp
GUI/GLCanvas3D.cpp
GUI/GLCanvas3D.hpp
GUI/SequentialPrintClearance.hpp
GUI/GLModel.cpp
GUI/GLModel.hpp
GUI/GLSelectionRectangle.cpp
@@ -687,6 +690,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/host/PluginResourceTextures.cpp
plugin/host/PluginResourceTextures.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
@@ -710,6 +715,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+290 -116
View File
@@ -1,4 +1,9 @@
#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"
@@ -36,11 +41,9 @@
#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>
@@ -82,6 +85,96 @@ static std::string float_to_string_with_precision(float value, int precision = 3
return stream.str();
}
static std::string colour_to_ams_string(const wxColour& color)
{
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
return col_buf;
}
static wxColour mix_colour(const wxColour& left, const wxColour& right, double ratio)
{
ratio = std::max(0.0, std::min(1.0, ratio));
auto mix_channel = [ratio](unsigned char l, unsigned char r) {
return static_cast<unsigned char>(std::round(l + (r - l) * ratio));
};
return wxColour(mix_channel(left.Red(), right.Red()),
mix_channel(left.Green(), right.Green()),
mix_channel(left.Blue(), right.Blue()),
mix_channel(left.Alpha(), right.Alpha()));
}
static std::vector<ColorPickerPopup::ColorItem> collect_ams_color_items(DevFilaSystem* fila_system)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (!fila_system) return items;
for (const auto& ams_pair : fila_system->GetAmsList()) {
DevAms* ams = ams_pair.second;
if (!ams) continue;
for (const auto& tray_pair : ams->GetTrays()) {
DevAmsTray* tray = tray_pair.second;
if (!tray || !tray->is_tray_info_ready()) continue;
ColorPickerPopup::ColorItem item;
for (const std::string& col : tray->cols) {
item.colors.emplace_back(DevAmsTray::decode_color(col));
}
if (item.colors.empty() && !tray->color.empty()) {
item.colors.emplace_back(DevAmsTray::decode_color(tray->color));
}
if (item.colors.empty()) continue;
item.ctype = item.colors.size() > 1 ? tray->ctype : 2;
items.emplace_back(std::move(item));
}
}
return items;
}
static std::vector<ColorPickerPopup::ColorItem> collect_preset_color_items(const std::string& filament_id)
{
std::vector<ColorPickerPopup::ColorItem> items;
if (filament_id.empty()) return items;
auto* clr_query = GUI::wxGetApp().get_filament_color_code_query();
if (!clr_query) return items;
auto* agent = wxGetApp().getAgent();
const std::string catalog_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id;
FilamentColorCodes* color_codes = clr_query->GetFilaInfoMap(wxString::FromUTF8(catalog_id));
if (!color_codes || !color_codes->GetFilamentColor2CodeMap()) return items;
std::set<std::string> seen;
for (const auto& color_pair : *color_codes->GetFilamentColor2CodeMap()) {
const FilamentColor& fila_color = color_pair.first;
FilamentColorCode* color_code = color_pair.second;
if (!color_code || fila_color.GetColors().empty()) continue;
ColorPickerPopup::ColorItem item;
item.ctype = fila_color.ColorCount() < 2 ? 2 :
(fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR ? 0 : 1);
item.name = color_code->GetFilaColorName();
std::string key = std::to_string(item.ctype);
for (const wxColour& color : fila_color.GetColors()) {
item.colors.emplace_back(color);
char rgba[9];
std::snprintf(rgba, sizeof(rgba), "%02X%02X%02X%02X",
static_cast<unsigned>(color.Red()), static_cast<unsigned>(color.Green()),
static_cast<unsigned>(color.Blue()), static_cast<unsigned>(color.Alpha()));
key += "|" + std::string(rgba);
}
if (item.colors.empty() || !seen.insert(key).second) continue;
items.emplace_back(std::move(item));
}
return items;
}
AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id)
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_color_picker_popup(ColorPickerPopup(this))
@@ -737,8 +830,19 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
wxColour color = m_clr_picker->m_colour;
char col_buf[10];
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
std::string tray_color = colour_to_ams_string(color);
std::vector<std::string> tray_colors;
int tray_ctype = 2;
// Only firmware advertising fun2[23] receives the manual multi-color fields.
if (obj->is_support_filament_manual_multi_color) {
for (const wxColour& selected_color : m_clr_picker->m_cols) {
tray_colors.emplace_back(colour_to_ams_string(selected_color));
}
if (tray_colors.empty()) {
tray_colors.emplace_back(tray_color);
}
tray_ctype = tray_colors.size() > 1 ? m_clr_picker->ctype : 2;
}
if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) {
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
@@ -757,13 +861,33 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
<< ", tray_info_idx (filament_id) = " << ams_filament_id
<< ", setting_id = " << ams_setting_id
<< ", tray_type = " << m_filament_type
<< ", tray_color = " << col_buf
<< ", tray_color = " << tray_color
<< ", nozzle_temp_min = " << nozzle_temp_min_int
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
// set filament
if (m_is_third) {
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int);
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, tray_color, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int,
tray_colors, tray_ctype);
// Refresh the edited tray immediately while its MQTT acknowledgement is pending.
DevAmsTray* tray = nullptr;
if (is_virtual_tray()) {
for (auto& virtual_tray : obj->vt_slot) {
if (virtual_tray.id == std::to_string(ams_id)) {
tray = &virtual_tray;
break;
}
}
} else {
tray = obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
}
if (tray) {
tray->color = tray_color;
tray->cols = tray_colors;
tray->ctype = tray_ctype;
tray->set_hold_count();
}
}
//reset param
@@ -920,8 +1044,15 @@ void AMSMaterialsSetting::set_ctype(int ctype)
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
{
unsigned int color_num = event.GetInt();
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
if (colors.empty()) {
unsigned int color_num = event.GetInt();
colors.emplace_back(color_num >> 24 & 0xFF, color_num >> 16 & 0xFF, color_num >> 8 & 0xFF, color_num & 0xFF);
}
set_ctype(m_color_picker_popup.get_selected_ctype());
set_color(colors.front());
set_colors(colors);
}
void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
@@ -936,14 +1067,12 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
}
}
std::vector<wxColour> ams_colors;
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id));
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour, m_clr_picker->m_cols, m_clr_picker->ctype);
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
m_color_picker_popup.set_ams_colours(ams_colors);
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
m_color_picker_popup.Popup();
}
@@ -1732,7 +1861,46 @@ void ColorPicker::doRender(wxDC& dc)
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
if (m_selected) radius -= FromDIP(1);
if (alpha == 0) {
if (m_show_full && m_cols.size() > 1) {
if (ctype == 0) {
const double center_x = size.x / 2.0;
const double center_y = size.y / 2.0;
const double draw_radius = radius;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int x = 0; x < size.x; ++x) {
const double dx = x + 0.5 - center_x;
if (std::abs(dx) > draw_radius) continue;
const double half_height = std::sqrt(std::max(0.0, draw_radius * draw_radius - dx * dx));
const int top = std::max(0, static_cast<int>(std::ceil(center_y - half_height)));
const int bottom = std::min(size.y - 1, static_cast<int>(std::floor(center_y + half_height)));
const double pos = size.x > 1 ? static_cast<double>(x) / (size.x - 1) : 0.0;
const double scaled = pos * (m_cols.size() - 1);
const size_t idx = std::min(static_cast<size_t>(scaled), m_cols.size() - 2);
const double ratio = scaled - idx;
if (top <= bottom) {
dc.SetBrush(wxBrush(mix_colour(m_cols[idx], m_cols[idx + 1], ratio)));
dc.DrawRectangle(x, top, 1, bottom - top + 1);
}
}
}
else {
float ev_angle = 360.0 / m_cols.size();
const float overlap = 2.0f;
wxPoint center(size.x / 2, size.y / 2);
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_cols.front()));
dc.DrawCircle(center.x, center.y, radius);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
float startAngle = 270.0f + i * ev_angle;
float endAngle = startAngle + ev_angle;
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle - overlap, endAngle + overlap);
}
}
}
else if (alpha == 0) {
wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize();
int center_x = (size.x - bmp_size.x) / 2;
int center_y = (size.y - bmp_size.y) / 2;
@@ -1774,52 +1942,6 @@ void ColorPicker::doRender(wxDC& dc)
dc.SetPen(wxPen(wxColour("#6B6B6B")));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(size.x / 2, size.y / 2, radius);
if (m_cols.size() > 1) {
if (ctype == 0) {
int left = FromDIP(0);
float total_width = size.x;
int gwidth = std::round(total_width / (m_cols.size() - 1));
for (int i = 0; i < m_cols.size() - 1; i++) {
if ((left + gwidth) > (size.x)) {
gwidth = size.x - left;
}
auto rect = wxRect(left, 0, gwidth, size.y);
dc.GradientFillLinear(rect, m_cols[i], m_cols[i + 1], wxEAST);
left += gwidth;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
else {
float ev_angle = 360.0 / m_cols.size();
float startAngle = 270.0;
float endAngle = 270.0;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int i = 0; i < m_cols.size(); i++) {
dc.SetBrush(m_cols[i]);
endAngle += ev_angle;
endAngle = endAngle > 360.0 ? endAngle - 360.0 : endAngle;
wxPoint center(size.x / 2, size.y / 2);
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle, endAngle);
startAngle += ev_angle;
startAngle = startAngle > 360.0 ? startAngle - 360.0 : startAngle;
}
if (wxGetApp().dark_mode()) {
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
}
else {
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
}
}
}
}
if (m_is_empty) {
@@ -1888,19 +2010,21 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
//other
wxFlexGridSizer* fg_sizer;
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
fg_sizer->SetFlexibleDirection(wxBOTH);
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
for (wxColour col : m_def_colors) {
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col);
cp->set_colors({col});
cp->ctype = 2;
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp);
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
m_default_color_pickers.push_back(cp);
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour);
@@ -1963,16 +2087,11 @@ 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(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_other_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
@@ -1983,7 +2102,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
m_def_color_box->Fit();
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
@@ -1991,55 +2110,45 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
wxGetApp().UpdateDarkUIWin(this);
}
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
{
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]));
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)));
}
} // 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);
wxCommandEvent evt(EVT_SELECTED_COLOR);
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);
}
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));
wxPostEvent(GetParent(), evt);
}
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
{
m_ams_fg_sizer->Clear(false);
if (m_ams_color_pickers.size() > 0) {
for (ColorPicker* col_pick:m_ams_color_pickers) {
std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy();
m_color_pickers.erase(iter);
}
@@ -2049,17 +2158,22 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
}
m_ams_colors = ams;
for (wxColour col : m_ams_colors) {
m_ams_color_items = ams;
for (const ColorItem& item : m_ams_color_items) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(col);
cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
m_color_pickers.push_back(cp);
m_ams_color_pickers.push_back(cp);
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour);
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
@@ -2072,9 +2186,68 @@ void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
Fit();
}
void ColorPickerPopup::set_def_colour(wxColour col)
void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors)
{
m_other_fg_sizer->Clear(false);
if (!m_preset_color_pickers.empty()) {
for (ColorPicker* col_pick : m_preset_color_pickers) {
auto iter = std::find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
if (iter != m_color_pickers.end()) {
col_pick->Hide();
col_pick->Destroy();
m_color_pickers.erase(iter);
}
}
m_preset_color_pickers.clear();
}
for (const ColorItem& item : preset_colors) {
if (item.colors.empty()) continue;
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
cp->set_color(item.colors.front());
cp->set_colors(item.colors);
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
cp->set_show_full(true);
cp->set_selected(false);
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
if (!item.name.empty()) {
cp->SetToolTip(item.name);
}
m_color_pickers.push_back(cp);
m_preset_color_pickers.push_back(cp);
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
evt.SetInt(g_col);
wxPostEvent(GetParent(), evt);
});
}
const bool show_default_colours = m_preset_color_pickers.empty();
for (ColorPicker* cp : m_default_color_pickers) {
cp->Show(show_default_colours);
}
for (ColorPicker* cp : (show_default_colours ? m_default_color_pickers : m_preset_color_pickers)) {
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
}
m_other_fg_sizer->Layout();
Layout();
Fit();
}
void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols, int ctype)
{
m_def_col = col;
if (cols.empty()) {
cols.push_back(col);
}
m_def_cols = cols;
m_def_ctype = m_def_cols.size() > 1 ? ctype : 2;
for (ColorPicker* cp : m_color_pickers) {
if (cp->m_selected) {
@@ -2083,7 +2256,8 @@ void ColorPickerPopup::set_def_colour(wxColour col)
}
for (ColorPicker* cp : m_color_pickers) {
if (cp->m_colour == m_def_col) {
if (!cp->IsShown()) continue;
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
cp->set_selected(true);
break;
}
+19 -5
View File
@@ -82,15 +82,22 @@ public:
class ColorPickerPopup : public PopupWindow
{
public:
struct ColorItem
{
std::vector<wxColour> colors;
int ctype = 2;
wxString name;
};
ScalableBitmap m_ts_bitmap_custom;
wxStaticBitmap* m_ts_stbitmap_custom;
StaticBox* m_custom_cp;
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<wxColour> m_ams_colors;
std::vector<ColorItem> m_ams_color_items;
std::vector<ColorPicker*> m_color_pickers;
std::vector<ColorPicker*> m_ams_color_pickers;
@@ -98,13 +105,20 @@ public:
ColorPickerPopup(wxWindow* parent);
~ColorPickerPopup() {};
void on_custom_clr_picker(wxMouseEvent& event);
void set_ams_colours(std::vector<wxColour> ams);
void set_def_colour(wxColour col);
void set_ams_colours(const std::vector<ColorItem>& ams);
void set_preset_colours(const std::vector<ColorItem>& preset_colors);
void set_def_colour(wxColour col, std::vector<wxColour> cols = {}, int ctype = 2);
const std::vector<wxColour>& get_selected_colours() const { return m_def_cols; }
int get_selected_ctype() const { return m_def_ctype; }
void paintEvent(wxPaintEvent& evt);
virtual void OnDismiss() wxOVERRIDE;
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
public:
private:
std::vector<ColorPicker*> m_default_color_pickers;
std::vector<ColorPicker*> m_preset_color_pickers;
std::vector<wxColour> m_def_cols;
int m_def_ctype = 2;
};
+3 -1
View File
@@ -747,7 +747,9 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
// Orca: prevent resetting under gcode viewer mode
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
const auto plater = GUI::wxGetApp().mainframe->m_plater;
// Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply()
// (loading presets, config or input files, e.g. from the CLI) therefore dereferences a null `mainframe` and crashes.
const auto plater = GUI::wxGetApp().plater();
if (plater && plater->only_gcode_mode()) {
invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
}
File diff suppressed because it is too large Load Diff
+92
View File
@@ -0,0 +1,92 @@
#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
+7 -1
View File
@@ -1798,7 +1798,8 @@ int MachineObject::command_ams_calibrate(int ams_id)
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type,
int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors, int tray_ctype)
{
int tag_tray_id = 0;
int tag_ams_id = ams_id;
@@ -1827,6 +1828,10 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
j["print"]["nozzle_temp_min"] = nozzle_temp_min;
j["print"]["nozzle_temp_max"] = nozzle_temp_max;
j["print"]["tray_type"] = tray_type;
if (!tray_colors.empty()) {
j["print"]["cols"] = tray_colors;
j["print"]["ctype"] = tray_ctype;
}
return this->publish_json(j);
}
@@ -5478,6 +5483,7 @@ void MachineObject::parse_new_info(json print)
is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1);
is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
is_support_filament_manual_multi_color = (get_flag_bits_no_border(fun2, 23) == 1);
if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) {
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
+2 -1
View File
@@ -674,6 +674,7 @@ public:
bool is_support_remote_dry = false;
bool is_support_check_track_switch_match_slice_printer{false};
bool is_support_pa_mode{false};
bool is_support_filament_manual_multi_color{false};
std::optional<bool> m_firmware_support_print_tpu_left;
bool installed_upgrade_kit{false};
@@ -804,7 +805,7 @@ public:
int command_ams_switch_filament(bool switch_filament);
int command_ams_air_print_detect(bool air_print_detect);
int command_ams_calibrate(int ams_id);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors = {}, int tray_ctype = 2);
int command_ams_select_tray(std::string tray_id);
int command_ams_refresh_rfid(int ams_id, int slot_id);
int command_ams_control(std::string action);
+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.m_colors.emplace(wxColour(color_str));
fila_color.AddColor(wxColour(color_str));
}
}
}
+11
View File
@@ -87,6 +87,17 @@ struct FilamentColor
public:
size_t ColorCount() const noexcept { return m_colors.size(); }
// Preserve catalog endpoint order for display while retaining set-based lookup identity.
const std::vector<wxColour>& GetColors() const noexcept { return m_color_list; }
void AddColor(const wxColour& color)
{
if (m_colors.insert(color).second) m_color_list.push_back(color);
}
private:
std::vector<wxColour> m_color_list;
public:
void EndSet(int ctype)
{
+74 -165
View File
@@ -2,6 +2,7 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Field.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "wxExtensions.hpp"
#include "Plater.hpp"
@@ -45,7 +46,11 @@
#include <wx/arrstr.h>
#include <wx/anybutton.h>
#include <wx/colour.h>
#include <wx/clrpicker.h>
#include <wx/button.h>
#include <wx/brush.h>
#include <wx/pen.h>
#include <optional>
#include <variant>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/dc.h>
@@ -2618,92 +2623,41 @@ void PluginConfigField::msw_rescale()
void ColourPicker::BUILD()
{
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
// 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));
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);
// 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);
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);
if (parent_is_custom_ctrl && m_opt.height < 0)
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();
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();
});
temp->SetToolTip(get_tooltip_text(clr_str));
// 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));
}
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
void ColourPicker::draw_bmp_btn()
{
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);
if (!window) return;
auto* btn = static_cast<wxButton*>(window);
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
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);
@@ -2712,11 +2666,11 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return bmp;
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
wxGCDC dc2(dc);
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
int padding = btn->FromDIP(5);
dc2.SetPen(*wxTRANSPARENT_PEN);
@@ -2735,117 +2689,72 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
return bmp;
};
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
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
}
void ColourPicker::set_value(const boost::any& value, bool change_event)
void ColourPicker::set_value(const std::string& value, bool change_event)
{
m_disable_change_event = !change_event;
const wxString clr_str(boost::any_cast<wxString>(value));
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
set_value(boost::any(wxString::FromUTF8(value)), change_event);
}
#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;
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();
}
boost::any& ColourPicker::get_value()
{
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;
m_value = m_colour == wxTransparentColour ? std::string() :
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_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();
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
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)
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
if (parent_is_custom_ctrl) window->SetSize(size);
else window->SetMinSize(size);
draw_bmp_btn();
}
void ColourPicker::sys_color_changed()
{
#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
if (window) wxGetApp().UpdateDarkUI(window, true);
draw_bmp_btn();
}
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)
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
{
#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();
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))));
}
#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()
+10 -24
View File
@@ -22,10 +22,10 @@
#include <boost/any.hpp>
#include "I18N.hpp"
#include <wx/colourdata.h>
#include <wx/colour.h>
#include <wx/spinctrl.h>
#include <wx/bmpcbox.h>
#include <wx/clrpicker.h>
#include <optional>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp"
@@ -610,41 +610,27 @@ 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) {
m_disable_change_event = !change_event;
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
m_disable_change_event = false;
}
void set_value(const std::string& value, bool 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 { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
void enable() override { if (window) window->Enable(); }
void disable() override{ if (window) window->Disable(); }
wxWindow* getWindow() override { return window; }
private:
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
// 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;
};
class PointCtrl : public Field {
+8 -30
View File
@@ -23,7 +23,6 @@
#include <string>
#include <tuple>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
@@ -278,19 +277,6 @@ 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
@@ -430,24 +416,16 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
{
if (colors.empty()) return wxNullBitmap;
// 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);
if (force_gradient && colors.size() >= 2) {
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);
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);
}
}
+49 -37
View File
@@ -1,4 +1,8 @@
#include "FilamentPickerDialog.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <utility>
#include "GUI.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -79,7 +83,8 @@ 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)
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
std::optional<FilamentColorPickerValue> ordered_color)
: DPIDialog(parent ? parent : wxGetApp().mainframe,
wxID_ANY,
_L("Select Filament"),
@@ -92,6 +97,7 @@ 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);
@@ -441,6 +447,9 @@ 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);
@@ -511,24 +520,27 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
Layout();
}
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
{
std::vector<wxColour> wx_colors = {custom_color};
// Update preview bitmap
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
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(custom_color);
m_color_demo->SetBackgroundColour(wx_colors.front());
m_color_demo->Refresh();
} else {
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
}
// Update preview labels for custom color
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
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_idx->SetLabel("");
Layout();
}
@@ -557,16 +569,6 @@ 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
@@ -588,27 +590,37 @@ void FilamentPickerDialog::BindEvents()
// Pause click detection while color picker is open
StopClickDetection();
wxColourData original_data = GetSingleColorData();
wxColourData result = show_sys_picker_dialog(this, original_data);
// 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();
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);
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;
m_custom_selection = selected;
m_ordered_color = filament_color_picker_result(*selected);
m_selection_changed = true;
std::vector<wxColour> colors;
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);
UpdateButtonStates(nullptr);
});
}
+10 -4
View File
@@ -5,9 +5,10 @@
#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>
@@ -25,10 +26,13 @@ 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);
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
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; }
@@ -68,7 +72,7 @@ private:
// UI update methods
void UpdatePreview(const FilamentColorCode& filament);
void UpdateCustomColorPreview(const wxColour& custom_color);
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
void UpdateButtonStates(wxBitmapButton* selected_btn);
// Shaped window methods
@@ -77,7 +81,6 @@ private:
// Data loading
bool LoadFilamentData(const wxString& fila_id);
wxColourData GetSingleColorData();
// Flash effect
void StartFlashing();
@@ -98,6 +101,9 @@ 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;
+15 -91
View File
@@ -65,6 +65,7 @@
#include <string_view>
#include "slic3r/GUI/Selection.hpp"
#include "GLCanvas3D.hpp"
#include "SequentialPrintClearance.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
#endif
@@ -6397,17 +6398,16 @@ void GLCanvas3D::update_sequential_clearance()
//BBS: add the height logic
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
Polygons polygons;
std::vector<std::pair<Polygon, float>> height_polygons;
polygons.reserve(instances_count);
height_polygons.reserve(instances_count);
std::vector<struct height_info> convex_and_bounding_boxes;
struct height_info
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
};
std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
convex_and_bounding_boxes.reserve(instances_count);
std::map<ObjectID, int> print_order;
for (const PrintObject* print_object : fff_print()->objects())
for (const PrintInstance& instance : print_object->instances())
print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order);
for (size_t i = 0; i < instance_transforms.size(); ++i) {
const size_t first_instance = convex_and_bounding_boxes.size();
const auto& instances = instance_transforms[i];
double rotation_z0 = instances.front()->get_rotation().z();
int index = 0;
@@ -6427,96 +6427,20 @@ void GLCanvas3D::update_sequential_clearance()
Polygon convex_hull(std::move(inst_pts));
BoundingBox bouding_box = convex_hull.bounding_box();
BoundingBox plate_bb = plate->get_bounding_box_crd();
const ObjectID instance_id = m_model->objects[i]->instances[index]->id();
double instance_height = m_model->objects[i]->get_instance_max_z(index++);
//skip the object for not current plate
if (!plate_bb.overlap(bouding_box))
continue;
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull});
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull, instance_id});
polygons.emplace_back(std::move(convex_hull));
}
sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order);
}
//sort the print instance
std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(),
[](auto &l, auto &r) {
auto ly1 = l.bounding_box.min.y();
auto ly2 = l.bounding_box.max.y();
auto ry1 = r.bounding_box.min.y();
auto ry2 = r.bounding_box.max.y();
auto inter_min = std::max(ly1, ry1);
auto inter_max = std::min(ly2, ry2);
auto lx = l.bounding_box.min.x();
auto rx = r.bounding_box.min.x();
if (inter_max - inter_min > 0)
return (lx < rx) || ((lx == rx)&&(ly1 < ry1));
else
return (ly1 < ry1);
});
/*bool has_interlaced_objects = false;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
has_interlaced_objects = true;
break;
}
}
if (has_interlaced_objects)
break;
}*/
int bounding_box_count = convex_and_bounding_boxes.size();
double printable_height = fff_print()->config().printable_height;
double hc1 = fff_print()->config().extruder_clearance_height_to_lid;
double hc2 = fff_print()->config().extruder_clearance_height_to_rod;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
double height = (k == (bounding_box_count - 1))?printable_height:hc1;
/*if (has_interlaced_objects) {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc2)) {
height_polygons.emplace_back(std::make_pair(convex, hc2));
}
}
else {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc1)) {
height_polygons.emplace_back(std::make_pair(convex, hc1));
}
}*/
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
height = hc2;
break;
}
}
if (height < convex_and_bounding_boxes[k].instance_height)
height_polygons.emplace_back(std::make_pair(convex, height));
}
const auto& config = fff_print()->config();
const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height,
config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod);
// sends instances 2d hulls to be rendered
set_sequential_print_clearance_visible(true);
+108 -15
View File
@@ -252,9 +252,11 @@
#include "slic3r/GUI/ParamsDialog.hpp"
#include "slic3r/GUI/ParamsPanel.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/host/PluginPages.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include <wx/defs.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#include <cwchar>
@@ -333,6 +335,23 @@ static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent*
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
}
// Full {web} page URL for a same-site OrcaCloud path, or empty when it cannot be built. An already
// open cloud pane is navigated straight to it, without re-running the session handoff.
static wxString cloud_page_url(NetworkAgent* agent, const std::string& path)
{
if (path.empty())
return {};
auto orca = get_orca_cloud_agent(agent);
if (!orca)
return {};
std::string base = orca->get_cloud_base_url();
while (!base.empty() && base.back() == '/')
base.pop_back();
if (base.empty())
return {};
return from_u8(base + path);
}
void start_ping_test()
{
return;
@@ -3063,6 +3082,37 @@ void GUI_App::init_plugin_gui_wiring()
refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key);
});
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_register(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_register(capability);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_deregister(capability);
Slic3r::GUI::bump_texture_library_generation();
});
// Packs already loaded before this wiring, or in tests where no callback fires.
PluginResourceTextures::instance().initial_sweep();
Slic3r::GUI::bump_texture_library_generation();
}
bool GUI_App::on_init_inner()
@@ -5434,12 +5484,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({});
}
else if (command_str.compare("homepage_open_cloud") == 0) {
// Deferred: set_cloud_mode() may create a second native webview, which must
// not run inside this webview's script-message callback.
CallAfter([this]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(true, cloud_webview_handoff());
});
this->open_orca_cloud();
}
else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() {
@@ -5588,6 +5633,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
return "";
}
void GUI_App::open_orca_cloud(const std::string& next)
{
// Remembered so a login while the pane is open re-seeds the same page rather than the default.
m_cloud_open_path = next;
// Deferred: set_cloud_mode() may create the second native webview, so this must not run inside
// the caller's frame (a webview script-message callback or an ImGui frame).
CallAfter([this]() {
if (mainframe != nullptr)
mainframe->select_tab(TAB_ID_HOME);
// The Home page is built lazily, so the tab switch may not have created it yet: ensure() builds
// it if needed, matching what showing the tab does, rather than dropping the open request.
if (WebViewPanel* home = WebViewPanel::ensure())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path), cloud_page_url(m_agent, m_cloud_open_path));
});
}
void GUI_App::handle_script_message(std::string msg, const std::string& provider)
{
try {
@@ -5865,7 +5926,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/
}
wxString GUI_App::cloud_webview_handoff()
wxString GUI_App::cloud_webview_handoff(const std::string& next)
{
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5873,7 +5934,7 @@ wxString GUI_App::cloud_webview_handoff()
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return {};
return from_u8(orca->build_web_session_start_url());
return from_u8(orca->build_web_session_start_url(next));
}
void GUI_App::request_cloud_web_session(const std::string& state)
@@ -6016,7 +6077,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active())
home->set_cloud_mode(true, cloud_webview_handoff());
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
}
@@ -6863,7 +6924,7 @@ void GUI_App::push_notification(const MachineObject* obj, wxString msg, wxStrin
}
}
void GUI_App::reload_settings()
bool GUI_App::reload_settings()
{
if (preset_bundle && m_agent) {
// Load user's personal presets
@@ -6871,7 +6932,7 @@ void GUI_App::reload_settings()
int result = m_agent->get_user_presets(&user_presets);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync";
return;
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
@@ -6946,13 +7007,30 @@ void GUI_App::reload_settings()
refresh_synced_ui();
else
CallAfter(refresh_synced_ui);
return true;
}
return false;
}
void GUI_App::check_legacy_user_filament_presets(bool by_user)
{
// One check at a time: the startup check can come due while the manual one is open, and on macOS the menu bar
// stays usable while a modal dialog is up.
if (m_legacy_filament_check_running)
return;
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return;
// The local copies may be older than the cloud ones another machine changed: migrating them would push changes
// made against stale copies and conflict with the cloud. The startup check waits for the presets to be pulled.
if (!by_user && m_user_preset_pull_pending) {
m_legacy_filament_check_due = true;
return;
}
// This check also answers a startup check that is still waiting.
m_legacy_filament_check_due = false;
m_legacy_filament_check_running = true;
ScopeGuard running_guard([this] { m_legacy_filament_check_running = false; });
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) {
if (by_user)
@@ -6997,6 +7075,9 @@ void GUI_App::check_legacy_user_filament_presets(bool by_user)
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return;
}
// Ask the sync loop to push on its next pass instead of its next 60 s round, which shortens the time the migrated
// presets stay unpushed.
m_sync_user_presets_now.store(true, std::memory_order_release);
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
@@ -7763,6 +7844,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
// BBS
m_user_sync_token.reset(new int(0));
m_user_preset_pull_pending = true;
if (with_progress_dlg) {
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores
// repeat triggers while it is on screen (prevents stacking modal dialogs).
@@ -7836,8 +7918,16 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
finishFn(ret == 0);
if (ret == 0 && m_agent && !t.expired())
reload_settings();
// Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded.
if (ret == 0 && m_agent && !t.expired() && reload_settings())
CallAfter([this, t] {
// A stopped or restarted sync: the next sync, if one starts, pulls the presets again.
if (t.expired() || is_closing())
return;
m_user_preset_pull_pending = false;
if (m_legacy_filament_check_due)
check_legacy_user_filament_presets(false);
});
// For orca specific syncing
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
@@ -7852,8 +7942,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds.
while (!t.expired()) {
++tick_tock;
// Sync once immediately, then every 60s, or right away when a force-push asked for it.
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
// Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked
// for it. The request is consumed on every tick, so one made before a scheduled pass is served by it
// instead of running a second pass straight after.
const bool push_now = m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel);
if (tick_tock % 120 == 0 || push_now) {
tick_tock = 0;
if (m_agent) {
if (!m_agent->is_user_login()) {

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