mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 11:51:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99de46b9d0 | ||
|
|
b8d40ed61f | ||
|
|
54e6df0235 | ||
|
|
79ed246d26 | ||
|
|
92b5301bf6 | ||
|
|
20c157b0f4 | ||
|
|
4cf16febf6 | ||
|
|
70e52a105f | ||
|
|
b525cb79e1 | ||
|
|
86ab598899 |
@@ -1,131 +0,0 @@
|
||||
# Color picker
|
||||
|
||||
The color picker is a local HTML dialog hosted by `ColorPickerDialog`, a
|
||||
`WebViewHostDialog` subclass. Its header and implementation own the ordered
|
||||
selection contract and shared filament conversion. Bridge validation, favorites
|
||||
persistence, placement and gettext strings remain private to the implementation.
|
||||
Its colors use the existing `ColorRGBA` utility.
|
||||
It uses a native title bar and fixed window border, allowing dragging but no
|
||||
manual resizing or maximization. Native title-bar colors use the application's
|
||||
shared dialog dark-theme update on creation and theme changes.
|
||||
Like SpeedDial, the host disables browser zoom accelerators, restores native
|
||||
WebView focus on activation and readiness, and synchronizes layout and the macOS
|
||||
viewport before repainting the backend. Activation never dismisses this modal
|
||||
dialog; focus restoration preserves an active editor and its partial input.
|
||||
The initialized page reports
|
||||
its intrinsic content height; native code validates the current page identifier
|
||||
and bounded numeric height, converts CSS pixels to device-independent window
|
||||
size, and fits within the display's work area. Identical sizes are ignored. The
|
||||
page observes content rather than viewport height, so resizing does not create
|
||||
a feedback loop; oversized content remains scrollable.
|
||||
The panel snapshots the triggering color or More Colors button's screen rectangle.
|
||||
It aligns below the button, flips above when needed, and clamps to that button's
|
||||
display work area on initial display. Later content and DPI changes update only
|
||||
the client size, preserving the position chosen by dragging the title bar.
|
||||
wxGTK clears the dialog's default
|
||||
parent-centering constraint and reapplies its native position after showing,
|
||||
bypassing wxGTK's requested-position cache. Absolute top-level placement remains
|
||||
subject to the window manager; Wayland compositors control it. The official filament palette's
|
||||
existing placement is independent of this custom panel.
|
||||
A solid selection contains one color; a gradient contains exactly two ordered
|
||||
endpoints. The bridge represents these as `{"type":"solid|gradient",
|
||||
"colors":["#RRGGBBAA", ...]}`. RGB input is also accepted and becomes opaque.
|
||||
Native validation rejects malformed hex and incorrect cardinality.
|
||||
|
||||
The project filament color action and the official filament picker's More Colors
|
||||
action enable two-endpoint gradients and independent endpoint alpha. The preset
|
||||
combo box reads project colors in their stored vector order locally and applies
|
||||
changes through `sync_colour_config`,
|
||||
retaining the existing RGB/RGBA profile/project formats and dirty marking.
|
||||
Sidebar gradient and segmented multicolor swatches draw colors in the supplied
|
||||
order without sorting them.
|
||||
Opaque colors use `#RRGGBB`; nonopaque colors use `#RRGGBBAA`, including fully
|
||||
transparent alpha `00`. Initial selections, alpha-only edits and cancellation
|
||||
retain stored alpha. Ordered
|
||||
custom results bypass the official palette's `FilamentColor` set. Choosing an
|
||||
official swatch retains its existing palette behavior.
|
||||
The Linux sidebar's Change extruder color menu uses this same custom editor and
|
||||
the filament swatch anchor, including alpha, ordered gradients and configuration syncing.
|
||||
|
||||
Striped colors and gradients with more than two endpoints cannot be represented
|
||||
by this editor. A notice explains that the current selection is retained; the
|
||||
editor starts from the first color, and unchanged confirmation is a no-op.
|
||||
Cancellation is also a no-op. If the WebView backend cannot initialize, a local
|
||||
unavailable panel offers Cancel and Escape, without opening a system color picker.
|
||||
|
||||
Generic color option fields use an ordinary color button on every platform and
|
||||
open the same panel with gradients and alpha disabled. The panel is anchored to
|
||||
the field button. Empty or invalid field values remain undefined (the empty
|
||||
string), distinct from opaque black. Right-click resets to undefined. Cancel does
|
||||
not notify the field; confirmation and reset notify only when its RGB string
|
||||
changes. Programmatic assignments remain silent, and reading a field never
|
||||
writes legacy favorites.
|
||||
|
||||
AMS material settings retain their fixed/AMS swatches and first-level popup. Only
|
||||
its custom-color action opens the solid, opaque editor, anchored to that custom
|
||||
control. The transient popup releases its grab while the modal is open and returns
|
||||
to the same selection on cancellation. Confirmation uses the existing default-color
|
||||
and packed RGBA event path. Device permission/status checks remain at the first-level
|
||||
entry.
|
||||
|
||||
Texture import's color-mapping Add Material action opens a solid, opaque editor
|
||||
anchored to the mapping row. The existing filament limit is checked before opening.
|
||||
The first-level material popup closes as an action; its callback is copied before
|
||||
dismissal, so popup destruction does not invalidate the modal's completion path.
|
||||
Confirmation invokes the existing virtual-filament callback once; cancellation
|
||||
adds no material.
|
||||
|
||||
Native gettext strings are installed at document start. Static page labels and
|
||||
palette names use stable English keys; RAL codes, channel symbols, and numeric
|
||||
labels remain data. The palette color values do not depend on the UI language.
|
||||
The application locale also sets the document language. Numeric editor validation
|
||||
uses the application's gettext messages, rather than browser language defaults;
|
||||
missing translations fall back to the stable English keys.
|
||||
|
||||
The pure color/state regressions run with
|
||||
`node --test resources/web/dialog/ColorPickerDialog/colorpicker.test.js`.
|
||||
Optional real-browser regressions live beside them in
|
||||
`colorpicker.browser.test.js`. They use an existing Playwright installation
|
||||
(`ORCA_PLAYWRIGHT_MODULE` can name its module path) and an installed browser
|
||||
(`ORCA_BROWSER_EXECUTABLE` can name its executable), without adding a production
|
||||
dependency. They cover bridge initialization, capability combinations, invalid
|
||||
editor recovery, ordered endpoint alpha, favorites, gettext keys, and live theme
|
||||
changes. These browser checks complement native contract/persistence tests; they
|
||||
do not establish the full native modal confirmation and favorites journey.
|
||||
|
||||
Gradient and alpha capabilities are independent and default off. Unsupported
|
||||
gradients are rejected; disabled alpha normalizes the selected result to opaque.
|
||||
Favorites retain both capabilities independently of the current caller. The page
|
||||
stores alpha as a byte and only converts to percentage when displaying or editing
|
||||
opacity, so switching endpoints does not change alpha precision.
|
||||
|
||||
The page starts disabled. Each document supplies a page identifier in its ready
|
||||
message, receives native initialization, and acknowledges it before native code
|
||||
accepts confirmation or favorite updates. Repeated initialization does not reset
|
||||
edits, and messages from an obsolete document are ignored. Native WebView message
|
||||
handling is deferred and guarded by a lifetime token. Only a validated confirmation
|
||||
sets the optional result. Cancel, Escape and native window close leave it empty.
|
||||
|
||||
Favorites belong to application configuration, separately from the selected result,
|
||||
in the editor configuration returned by `AppConfig::config_path()` under `data_dir()`.
|
||||
The `color_picker` section has string `version` equal to `1` and a `favorites`
|
||||
string containing a JSON array of typed selections. Native helpers normalize and
|
||||
deduplicate this array in order and limit it to 24 entries. Updates replace the
|
||||
entire section and immediately call `AppConfig::save()`, so accepted favorite
|
||||
changes survive cancellation. Empty arrays are saved explicitly. Core `AppConfig`
|
||||
only stores the raw section; color validation belongs to the GUI boundary.
|
||||
|
||||
When the section is absent, valid solid colors from `custom_color_list` are imported
|
||||
once, without changing that legacy section. The legacy native picker saves decimal
|
||||
`r,g,b,a` byte strings; hexadecimal RGB and RGBA strings are also supported. Migration
|
||||
strictly validates all four decimal channels and preserves alpha. Gradient strings
|
||||
and malformed values are ignored. The new version and an empty array are
|
||||
saved even when there is nothing to import. A present section with an unknown or
|
||||
missing version is left untouched and disables favorite writes. Malformed version
|
||||
1 data is displayed as empty and remains on disk until the user explicitly saves
|
||||
a valid replacement. Other configuration sections and project/profile color
|
||||
formats are independent of this storage.
|
||||
|
||||
Interface colors consume the host's `data-orca-theme` and `--orca-*` contract through
|
||||
shared styles. The picker opts into scoped global control roles. Application theme
|
||||
changes update UI surfaces without reloading the page or changing color data.
|
||||
@@ -1,199 +0,0 @@
|
||||
# Multi-extruder and multi-AMS device control
|
||||
|
||||
`DeviceManager`/`MachineObject` model the toolheads, AMS units, and filament slots of a connected
|
||||
printer and drive the device-control UI from that model. The model represents the counts as
|
||||
device-reported runtime values rather than compile-time caps, so a printer with an arbitrary number
|
||||
of toolheads, several AMS units, and a non-uniform number of slots per unit is handled by the same
|
||||
code paths. The printer agent is responsible for translating its printer's state into the shared
|
||||
JSON dialect this model consumes; the agent boundary is described in
|
||||
[printer-agent.md](printer-agent.md), and this document describes the dialect and the model.
|
||||
|
||||
## Model layout
|
||||
|
||||
`MachineObject` owns the device subsystems that this document covers:
|
||||
|
||||
- `DevExtderSystem` — the toolheads (extruders) and their per-toolhead filament/temperature state.
|
||||
- `DevFilaSystem` — the AMS units (`DevAms`) and their trays (`DevAmsTray`).
|
||||
- `DevNozzleSystem` — the nozzles and, on rack printers, `DevNozzleRack`.
|
||||
- `DevFilaSwitch` — a filament switch that can feed several AMS units to more than one extruder.
|
||||
|
||||
Each subsystem exposes its own count — `DevExtderSystem::GetTotalExtderCount()`,
|
||||
`DevFilaSystem::GetAmsCount()`, `DevAms::GetSlotCount()` — and the rest of the application derives
|
||||
its layout from those values rather than from fixed assumptions.
|
||||
|
||||
## Wire dialect
|
||||
|
||||
The dialect is the Bambu-shaped JSON that `DeviceManager` and `MachineObject` already consume. An
|
||||
agent whose printer speaks something else is expected to translate into it; the agent is not free
|
||||
to invent a new shape. Two representations coexist for extruders, and one for AMS/slots.
|
||||
|
||||
### Extruders
|
||||
|
||||
The generic representation is `print.device.extruder`, parsed by
|
||||
`ExtderSystemParser::ParseV2_0` (`DevExtruderSystem.cpp`). It has a packed `state` integer and an
|
||||
`info` array with one object per toolhead:
|
||||
|
||||
```text
|
||||
device.extruder.state packed integer
|
||||
bits 0..3 total extruder count
|
||||
bits 4..7 current extruder id
|
||||
bits 8..11 target extruder id
|
||||
bits 12..14 switch state
|
||||
bits 15..18 currently loading extruder id
|
||||
bit 19 busy for loading
|
||||
|
||||
device.extruder.info[] one entry per toolhead
|
||||
id extruder id
|
||||
filam_bak array of backup-group bits
|
||||
info bit 1 has filament, bit 2 buffer has filament, bit 3 nozzle present
|
||||
temp bits 0..15 current temperature, bits 16..31 target temperature
|
||||
spre slot_id bits 0..7, ams_id bits 8..15 (previous)
|
||||
snow same packing (current)
|
||||
star same packing (target)
|
||||
stat bit 0..15 AMS status, bits 16..31 RFID status
|
||||
hnow current nozzle id
|
||||
```
|
||||
|
||||
The legacy single/dual representation is the pair of scalar fields `nozzle_temper` /
|
||||
`nozzle_target_temper`, plus the `print.ams.tray_tar` / `tray_now` tray pointer, parsed by
|
||||
`ExtderSystemParser::ParseV1_0`. That parser is a fallback and only fills the main extruder when the
|
||||
reported toolhead count is one.
|
||||
|
||||
Physical extruder ids are fixed (`MAIN_EXTRUDER_ID = 0`, `DEPUTY_EXTRUDER_ID = 1`). Bambu printers
|
||||
present the two toolheads as a left/right pair and invert the physical-to-logical mapping; generic
|
||||
printers use the toolhead id itself as the logical id. `DevNozzle::GetLogicExtruderId()` /
|
||||
`GetExtruderId()` centralise that distinction.
|
||||
|
||||
### AMS units and slots
|
||||
|
||||
AMS units arrive as `print.ams.ams[]` and are parsed by `DevFilaSystemParser::ParseV1_0`. Each unit
|
||||
carries an `id` and an `info` bitfield:
|
||||
|
||||
```text
|
||||
info bits 0..3 type: 0 dummy, 1 AMS, 2 AMS-Lite, 3 N3F, 4 N3S, 5 AMS-Lite-mixed (N9)
|
||||
info bits 8..11 bound extruder id (0xE marks a switch-bound unit)
|
||||
info bits 24..27 switch inlet when the unit is switch-bound
|
||||
```
|
||||
|
||||
`DevAmsType` (`DevDefs.h`) enumerates the resulting unit kinds: `EXT_SPOOL`, `AMS`, `AMS_LITE`,
|
||||
`N3F`, `N3S`, and the `AMS_LITE_MIXED` N9 variant. `AMS_LITE_MIXED` reports itself as `AMS_LITE` to
|
||||
generic callers; code that needs the distinction uses `IsAmsLiteMixed()`.
|
||||
|
||||
A unit whose bound-extruder nibble is `0xE` is switch-bound: it is kept only when a `DevFilaSwitch`
|
||||
is installed, in which case its extruder id is pinned to the main extruder and its binding set
|
||||
becomes `{0, 1}`; otherwise the unit is dropped from the AMS list.
|
||||
|
||||
Each unit owns a map of `DevAmsTray` keyed by the tray id from the payload. The slot count is a
|
||||
property of the unit kind and the reported trays:
|
||||
|
||||
- `AMS`, `AMS_LITE`, and `N3F` report four slots on Bambu printers, matching the firmware contract;
|
||||
- `N3S` reports one slot;
|
||||
- `EXT_SPOOL` and any non-Bambu unit report the number of trays actually present.
|
||||
|
||||
The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous
|
||||
within a unit, because the generic tray-index mapping below is cumulative.
|
||||
|
||||
Bulk/extended spools are carried by `print.vir_slot`, which `DeviceManager` parses into
|
||||
`MachineObject::vt_slot`. Two ids are fixed: `VIRTUAL_TRAY_MAIN_ID = 255` (extruder 0) and
|
||||
`VIRTUAL_TRAY_DEPUTY_ID = 254` (extruder 1); the id convention is `vt_id = 255 - extruder_id`, so
|
||||
further extruders use 253, 252, … . The `vir_slot` parser recognises the 255/254 pair; the agent
|
||||
filament descriptor path `DevFilaSystemParser::ParseAgentFilament` is the entry that stores a fuller
|
||||
descending set in `vt_slot`.
|
||||
|
||||
## Mapping
|
||||
|
||||
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
|
||||
index. The Bambu mapping path (`DevMappingUtil`) computes `ams_id * 4 + slot_id` and knows only the
|
||||
Bambu unit kinds; `DevFilaSystem` provides the device model's own mapping below. The two agree for
|
||||
Bambu units and diverge for a non-Bambu unit whose slot count is not four.
|
||||
|
||||
### AMS id to extruder id
|
||||
|
||||
`DevFilaSystem::GetExtruderIdByAmsId()` resolves an AMS id to the extruder it feeds:
|
||||
|
||||
- an AMS unit returns its bound extruder (`DevAms::GetExtruderId()`);
|
||||
- a virtual tray returns `VIRTUAL_TRAY_MAIN_ID - vt_id`;
|
||||
- Bambu's virtual AMS ids map to the main/deputy extruder.
|
||||
|
||||
A unit may be bound to more than one extruder when a `DevFilaSwitch` feeds it; the binding set
|
||||
(`DevAms::m_binded_extruder_set`) and switch inlet are read by switch-aware code while the
|
||||
single-extruder id stays pinned for legacy callers.
|
||||
|
||||
### Tray index
|
||||
|
||||
`DevFilaSystem::GetTrayIndexMap()` maps a global tray index to an `(ams_id, slot_id)` pair and is
|
||||
used for backup-slot translation, calibration addressing, and the mapping popups.
|
||||
|
||||
- Bambu AMS: `ams_id * 4 + slot_id`; N3S: the AMS id; AMS-Lite-mixed (N9): `24 + slot_id`.
|
||||
- Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot
|
||||
id. This depends on each unit's tray ids being 0-based and contiguous.
|
||||
|
||||
### Virtual-slot recognition
|
||||
|
||||
`devPrinterUtil::IsVirtualSlot()` decides whether an id denotes a virtual tray. On Bambu printers it
|
||||
is the fixed pair `{255, 254}`. On generic printers it is the descending range
|
||||
`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. The preset
|
||||
is expected to match the connected device; when the two disagree the virtual range is wrong, which
|
||||
is why a mismatch is reported rather than silently accepted.
|
||||
|
||||
## Device-control UI
|
||||
|
||||
The device-control widgets follow the same device-reported counts.
|
||||
|
||||
- `AMSModel` (`AMSItem.hpp`) mirrors `DevAmsType` numerically: `EXT_AMS`, `GENERIC_AMS`, `AMS_LITE`,
|
||||
`N3F_AMS`, `N3S_AMS`. A plain `AMS` maps to `GENERIC_AMS`.
|
||||
- On non-Bambu printers `use_generic_ams_layout()` routes `GENERIC_AMS` units through a variable-lane
|
||||
rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering.
|
||||
- `AMSControl` picks its layout by toolhead count, not vendor: `CreateAmsSingleNozzle` for one
|
||||
toolhead, `CreateAmsDoubleNozzle` for two, and `CreateAmsMultiNozzle` (one preview and one AMS
|
||||
simplebook per toolhead) for three or more. A two-toolhead generic printer therefore keeps the
|
||||
legacy left/right presentation; that is deliberate.
|
||||
- The status panel's `ExtruderImage` renders one per-toolhead state per nozzle and is resized to
|
||||
`GetTotalExtderCount()`. Non-Bambu printers choose the active toolhead through
|
||||
`m_generic_nozzle_selector` (shown when more than one toolhead exists); Bambu keeps its left/right
|
||||
`m_nozzle_btn_panel`. Nozzle temperature controls are created on demand per toolhead.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Single dialect.** The agent translates its protocol into this JSON; the model does not learn
|
||||
vendor protocols. Extending the model with a new unit kind or field means extending the dialect.
|
||||
- **Agent-produced counts.** Toolhead, AMS, and slot counts come from the device payload. Nothing
|
||||
caps them at two toolheads or four slots except the Bambu contract described above.
|
||||
- **Extruder dialect invariants.** `device.extruder.state`'s count field must equal the length of
|
||||
`info[]`, and `info[]` must be ordered by extruder id with `id` equal to the array index: the
|
||||
parser places toolheads by array position while `GetExtderById()` indexes by id.
|
||||
- **One filament road per toolhead.** Each toolhead renders a single AMS filament road rather than an
|
||||
independent per-toolhead road state. A generic scaffold that allowed the latter was removed as
|
||||
unused; the one-extruder-per-toolhead road is the intended model.
|
||||
- **Preset/device agreement.** The generic layout and virtual-slot range derive their count from the
|
||||
selected printer preset. A printer preset must therefore declare the same number of
|
||||
`nozzle_diameter` entries as the device has toolheads. A mismatch is surfaced only as a log warning
|
||||
(`StatusPanel::check_extruder_count_mismatch()`), deliberately log-only, because the sidebar and
|
||||
extruder logic would otherwise silently disagree.
|
||||
- **Bambu compatibility.** Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed
|
||||
virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do
|
||||
not alter it.
|
||||
- **Tray-index invariant.** The generic tray-index map is cumulative, so payload tray ids must be
|
||||
unique, 0-based, and contiguous per unit. A malformed id is a producer error.
|
||||
- **Protocol markers.** The dialect has no neutral equivalent for some Bambu state, so agents that
|
||||
lack it send empty `print.cfg`, `print.fun`, `print.aux`, and `print.stat`. Their presence selects
|
||||
the new-protocol parse path; they are compatibility markers, not feature switches.
|
||||
|
||||
## Main implementation locations
|
||||
|
||||
- [`DevExtruderSystem`](../../src/slic3r/GUI/DeviceCore/DevExtruderSystem.h) — toolhead model and the
|
||||
`device.extruder` parser
|
||||
- [`DevFilaSystem`](../../src/slic3r/GUI/DeviceCore/DevFilaSystem.h) — AMS/tray model, the AMS parser,
|
||||
and the mapping helpers
|
||||
- [`DevDefs.h`](../../src/slic3r/GUI/DeviceCore/DevDefs.h) — `DevAmsType`, extruder ids, and virtual
|
||||
tray ids
|
||||
- [`DeviceManager.cpp`](../../src/slic3r/GUI/DeviceManager.cpp) — message dispatch, `vir_slot`
|
||||
parsing, and device state assembly
|
||||
- [`AMSControl.cpp`](../../src/slic3r/GUI/Widgets/AMSControl.cpp) — per-toolhead AMS panes
|
||||
(`CreateAmsMultiNozzle`) and the generic layout
|
||||
- [`AMSItem.hpp`](../../src/slic3r/GUI/Widgets/AMSItem.hpp) — `AMSModel` and the variable-lane AMS
|
||||
rendering
|
||||
- [`StatusPanel.cpp`](../../src/slic3r/GUI/StatusPanel.cpp) — `ExtruderImage`, the toolhead selector,
|
||||
and per-toolhead temperature controls
|
||||
- [`MoonrakerPrinterAgent.cpp`](../../src/slic3r/Utils/MoonrakerPrinterAgent.cpp) — producer example
|
||||
(AMS payload and the `device.extruder` encoding)
|
||||
@@ -325,7 +325,6 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
|
||||
src/slic3r/GUI/CloneDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeDialog.cpp
|
||||
src/slic3r/GUI/ColorDecomposeSupport.cpp
|
||||
src/slic3r/GUI/ColorPickerDialog.cpp
|
||||
src/slic3r/GUI/ConfigWizard.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevCalib.cpp
|
||||
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
/* Pure color/state helpers shared by the page and its node:test contract tests. */
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
else root.ColorPickerModel = api;
|
||||
})(typeof globalThis === "object" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
function parseHex(hex) {
|
||||
if (typeof hex !== "string" || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return null;
|
||||
return [1, 3, 5, 7].map((offset) => offset === 7 && hex.length === 7 ? 255 : parseInt(hex.slice(offset, offset + 2), 16));
|
||||
}
|
||||
|
||||
function toHex(color) {
|
||||
return "#" + color.map((byte) => Math.round(byte).toString(16).padStart(2, "0")).join("").toUpperCase();
|
||||
}
|
||||
|
||||
const MAX_GRADIENT_COLORS = 7; // Most colors a gradient may have. A caller allows 2 (the default) up to this many.
|
||||
const NEW_GRADIENT_HUE_SHIFT = 75; // Hue step used for every newly created gradient color.
|
||||
const gradientLimit = (value) => Math.max(2, Math.min(MAX_GRADIENT_COLORS, Math.floor(Number(value)) || 2));
|
||||
|
||||
// `maxGradientColors` caps how many colors a gradient may have (2 when omitted).
|
||||
function normalizeSelection(value, options = {}, maxGradientColors = 2) {
|
||||
if (!value || !["solid", "gradient"].includes(value.type) || !Array.isArray(value.colors)) return null;
|
||||
if (value.type === "gradient" && !options.allow_gradient) return null;
|
||||
const count = value.colors.length;
|
||||
if (value.type === "solid" ? count !== 1 : count < 2 || count > gradientLimit(maxGradientColors)) return null;
|
||||
const colors = value.colors.map(parseHex);
|
||||
if (colors.some((color) => color === null)) return null;
|
||||
if (!options.allow_alpha) colors.forEach((color) => { color[3] = 255; });
|
||||
return {type: value.type, colors: colors.map(toHex)};
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
|
||||
let h = 0, s = 0;
|
||||
const l = (max + min) / 2;
|
||||
if (delta) {
|
||||
s = delta / (1 - Math.abs(2 * l - 1));
|
||||
if (max === r) h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
|
||||
else if (max === g) h = ((b - r) / delta + 2) / 6;
|
||||
else h = ((r - g) / delta + 4) / 6;
|
||||
}
|
||||
return [h * 360, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
h = ((h % 360) + 360) % 360 / 60; s /= 100; l /= 100;
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
const x = c * (1 - Math.abs(h % 2 - 1)), m = l - c / 2;
|
||||
const sectors = [[c,x,0], [x,c,0], [0,c,x], [0,x,c], [x,0,c], [c,0,x]];
|
||||
return sectors[Math.floor(h)].map((channel) => Math.round((channel + m) * 255));
|
||||
}
|
||||
|
||||
function contrastColor(color, hueShift = 90) {
|
||||
if (!Number.isFinite(hueShift)) hueShift = 90;
|
||||
const degrees = Math.max(-180, Math.min(180, hueShift));
|
||||
const amount = Math.abs(degrees) / 180;
|
||||
const [h, s, l] = rgbToHsl(color[0], color[1], color[2]);
|
||||
const grey = s < 10;
|
||||
const target = grey ? (l < 50 ? 100 : 0) : 100 - l;
|
||||
const rgb = hslToRgb(h + degrees, grey ? 0 : s, l + (target - l) * amount);
|
||||
return [...rgb, color[3]];
|
||||
}
|
||||
|
||||
// options.max_gradient_colors (2..7, default 2) is how many colors this page lets a gradient have.
|
||||
function createState(options = {}, selection = {type: "solid", colors: ["#808080"]}) {
|
||||
const capabilities = {allow_gradient: options.allow_gradient === true, allow_alpha: options.allow_alpha === true};
|
||||
// gradient stays null until a selection provides one or the user switches to gradient mode.
|
||||
const state = {options: capabilities, maxGradientColors: gradientLimit(options.max_gradient_colors),
|
||||
mode: "solid", active: 0, solid: [128,128,128,255], gradient: null};
|
||||
if (!applySelection(state, selection)) return null;
|
||||
return state;
|
||||
}
|
||||
|
||||
function applySelection(state, value) {
|
||||
const selection = normalizeSelection(value, state.options, state.maxGradientColors);
|
||||
if (!selection) return false;
|
||||
state.mode = selection.type;
|
||||
const colors = selection.colors.map(parseHex);
|
||||
if (state.mode === "solid") state.solid = colors[0];
|
||||
else {
|
||||
state.gradient = colors;
|
||||
state.active = Math.min(state.active, colors.length - 1); // A gradient with fewer colors must not leave the selected stop past its end.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function activeColor(state) { return state.mode === "solid" ? state.solid : state.gradient[state.active]; }
|
||||
|
||||
function setMode(state, mode) {
|
||||
if (mode !== "solid" && (mode !== "gradient" || !state.options.allow_gradient)) return false;
|
||||
// A new gradient starts at the current solid color and ends at its contrast color.
|
||||
if (mode === "gradient" && !state.gradient) state.gradient = [[...state.solid], contrastColor(state.solid, NEW_GRADIENT_HUE_SHIFT)];
|
||||
state.mode = mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
function addGradientColor(state) {
|
||||
if (state.mode !== "gradient" || state.gradient.length >= state.maxGradientColors) return false;
|
||||
state.gradient.splice(state.active + 1, 0, contrastColor(state.gradient[state.active], NEW_GRADIENT_HUE_SHIFT));
|
||||
state.active += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeGradientColor(state) {
|
||||
if (state.mode !== "gradient" || state.gradient.length <= 2) return false;
|
||||
state.gradient.splice(state.active, 1);
|
||||
state.active = Math.min(state.active, state.gradient.length - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setRgb(state, rgb) {
|
||||
if (rgb.length !== 3 || rgb.some((v) => !Number.isFinite(v) || v < 0 || v > 255)) return false;
|
||||
const color = activeColor(state);
|
||||
rgb.forEach((v, i) => { color[i] = Math.round(v); });
|
||||
return true;
|
||||
}
|
||||
|
||||
function setAlphaPercent(state, value) {
|
||||
if (!state.options.allow_alpha || !Number.isFinite(value) || value < 0 || value > 100) return false;
|
||||
activeColor(state)[3] = Math.round(value * 255 / 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
function setHex(state, hex) {
|
||||
const color = parseHex(hex);
|
||||
if (!color) return false;
|
||||
if (hex.length === 7) color[3] = activeColor(state)[3];
|
||||
if (!state.options.allow_alpha) color[3] = 255;
|
||||
if (state.mode === "solid") state.solid = color;
|
||||
else state.gradient[state.active] = color;
|
||||
return true;
|
||||
}
|
||||
|
||||
function exportSelection(state) {
|
||||
const colors = state.mode === "solid" ? [state.solid] : state.gradient;
|
||||
return normalizeSelection({type: state.mode, colors: colors.map(toHex)}, state.options, state.maxGradientColors);
|
||||
}
|
||||
|
||||
// Favorites retain full capabilities regardless of what this caller can select.
|
||||
function normalizeFavorites(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return values.slice(0, 24).map((value) => normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, MAX_GRADIENT_COLORS)).filter(Boolean);
|
||||
}
|
||||
|
||||
function selectionBackground(value) {
|
||||
return value.type === "gradient" ? `linear-gradient(to right, ${value.colors.join(", ")})` : `linear-gradient(${value.colors[0]}, ${value.colors[0]})`;
|
||||
}
|
||||
|
||||
return {MAX_GRADIENT_COLORS, NEW_GRADIENT_HUE_SHIFT, contrastColor, parseHex, toHex, normalizeSelection, rgbToHsl, hslToRgb, createState, applySelection, activeColor, setMode,
|
||||
addGradientColor, removeGradientColor, setRgb, setAlphaPercent, setHex, exportSelection, normalizeFavorites, selectionBackground};
|
||||
});
|
||||
@@ -1,314 +0,0 @@
|
||||
// Optional real-DOM regressions. Use an existing Playwright installation:
|
||||
// ORCA_PLAYWRIGHT_MODULE=<module path> ORCA_BROWSER_EXECUTABLE=<browser path>
|
||||
// ORCA_BROWSER_ENGINE=webkit exercises the macOS WebView layout engine.
|
||||
// node --test resources/web/dialog/ColorPickerDialog/colorpicker.browser.test.js
|
||||
const {test, before, after} = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {readFileSync} = require("node:fs");
|
||||
const {resolve} = require("node:path");
|
||||
const {pathToFileURL} = require("node:url");
|
||||
const engineName = process.env.ORCA_BROWSER_ENGINE || "chromium";
|
||||
let engine;
|
||||
try { engine = require(process.env.ORCA_PLAYWRIGHT_MODULE || "playwright")[engineName]; }
|
||||
catch (error) { if (error.code !== "MODULE_NOT_FOUND") throw error; }
|
||||
const options = {skip: engine ? false : "Use an existing Playwright installation via ORCA_PLAYWRIGHT_MODULE"};
|
||||
const url = pathToFileURL(resolve(__dirname, "index.html")).href;
|
||||
let browser;
|
||||
before(async () => { if (engine) browser = await engine.launch({headless: true, ...(process.env.ORCA_BROWSER_EXECUTABLE ? {executablePath: process.env.ORCA_BROWSER_EXECUTABLE} : {})}); });
|
||||
after(async () => { if (browser) await browser.close(); });
|
||||
async function open(t, strings = {}, language = "") {
|
||||
const page = await browser.newPage({viewport: {width: 600, height: 660}}), errors = [];
|
||||
page.on("pageerror", error => errors.push(error.message));
|
||||
t.after(async () => { await page.close(); assert.deepEqual(errors, []); });
|
||||
await page.addInitScript(({table, language}) => {
|
||||
window.ORCA_UI_STRINGS = table;
|
||||
window.ORCA_COLOR_PICKER_LANGUAGE = language;
|
||||
window.messages = [];
|
||||
window.wx = {postMessage: text => messages.push(JSON.parse(text))};
|
||||
}, {table: strings, language});
|
||||
await page.goto(url);
|
||||
const ready = await page.evaluate(() => messages[0]);
|
||||
assert.equal(ready.command, "ready");
|
||||
return {page, ready, send: payload => page.evaluate(value => ColorPickerDialog.handleMessage(value), payload)};
|
||||
}
|
||||
function init(ready, gradient = false, alpha = false) {
|
||||
return {command: "init", page_id: ready.page_id, options: {allow_gradient: gradient, allow_alpha: alpha},
|
||||
selection: {type: "solid", colors: ["#01234567"]},
|
||||
ui: {sidebar_open: true, palette: "basic"},
|
||||
favorites: [{type: "gradient", colors: ["#FF000040", "#0000FF90"]}, {type: "solid", colors: ["#ABCDEF40"]}]};
|
||||
}
|
||||
async function count(page, command) { return page.evaluate(value => messages.filter(message => message.command === value).length, command); }
|
||||
|
||||
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||
test(`Native capabilities gate gradient=${gradient} and alpha=${alpha}`, options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||
assert.equal(await send({...init(ready), options: {allow_gradient: "yes", allow_alpha: alpha}}), false);
|
||||
assert.equal(await send({...init(ready), page_id: "stale"}), false);
|
||||
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||
assert.equal(await page.locator("#modeControls").isVisible(), gradient);
|
||||
assert.equal(await page.locator("#alphaControls").isVisible(), alpha);
|
||||
assert.equal(await page.locator("#userColors button:not(.empty)").count(), gradient ? 2 : 1);
|
||||
await page.locator("#userColors button:not(.empty)").last().click();
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
assert.deepEqual(selection, {type: "solid", colors: [alpha ? "#ABCDEF40" : "#ABCDEFFF"]});
|
||||
assert.equal(await send(init(ready, gradient, alpha)), true);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection, "repeated ready/init retains edits");
|
||||
await page.locator("#colorPreview").click();
|
||||
const favorites = await page.evaluate(() => messages.at(-1).favorites);
|
||||
assert(favorites.some(favorite => favorite.colors[0] === "#ABCDEF40"), "original favorites retain their alpha");
|
||||
assert(favorites.some(favorite => favorite.type === "gradient"), "filtered gradients remain stored");
|
||||
await page.locator("#confirmBtn").click();
|
||||
assert.deepEqual(await page.evaluate(() => messages.at(-1).selection), selection);
|
||||
});
|
||||
}
|
||||
|
||||
test("Visible incomplete editors block confirmation and favorites until recovery", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||
for (const [id, value, valid] of [["rValue", "", "21"], ["oValue", "", "50"], ["hexInput", "GG", "12345680"]]) {
|
||||
await page.locator("#" + id).fill(value);
|
||||
const confirms = await count(page, "confirm"), saves = await count(page, "update_favorites");
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click()); await page.locator("#colorPreview").click();
|
||||
assert.equal(await count(page, "confirm"), confirms); assert.equal(await count(page, "update_favorites"), saves);
|
||||
await page.locator("#" + id).fill(valid); await page.locator("#" + id).press("Enter");
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
}
|
||||
await page.locator("#hValue").fill(""); // HSL is now visible beside RGB, so incomplete HSL must block.
|
||||
const hslConfirms = await count(page, "confirm");
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||
assert.equal(await count(page, "confirm"), hslConfirms);
|
||||
await page.locator("#hValue").fill("100");
|
||||
const confirms = await count(page, "confirm"); await page.locator("#confirmBtn").click();
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
await page.locator("#rValue").fill(""); await page.locator("#rValue").press("Escape");
|
||||
assert.equal(await page.evaluate(() => messages.at(-1).command), "cancel");
|
||||
});
|
||||
|
||||
test("Numeric channel and opacity edits clamp to their field bounds", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send(init(ready, true, true));
|
||||
for (const [id, input, expected] of [["rValue", "999", "255"], ["rValue", "-1", "0"],
|
||||
["hValue", "361", "360"], ["oValue", "101", "100"], ["oValue", "-1", "0"]]) {
|
||||
await page.locator("#" + id).fill(input);
|
||||
assert.equal(await page.locator("#" + id).inputValue(), expected);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.checkValidity()), true);
|
||||
}
|
||||
assert.equal((await page.evaluate(() => ColorPickerDialog.exportSelection())).colors[0].slice(-2), "00");
|
||||
});
|
||||
|
||||
test("Endpoint edits and favorites retain ordered byte alpha", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||
await page.locator(".gradient-thumb[data-index='1']").click();
|
||||
await page.locator('.color-item[title="Red"]').first().click();
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "gradient", colors: ["#FF000001", "#FF000080"]});
|
||||
await page.locator("#hexInput").fill("0000FF80"); await page.locator(".gradient-thumb[data-index='0']").click();
|
||||
assert.equal(await page.locator("#hexInput").inputValue(), "FF000001");
|
||||
await page.locator("#colorPreview").click();
|
||||
assert.deepEqual(await page.evaluate(() => messages.at(-1).favorites[0]), {type: "gradient", colors: ["#FF000001", "#0000FF80"]});
|
||||
});
|
||||
|
||||
test("Read-only favorites remain selectable and late documents cannot initialize", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send({...init(ready), favorites_writable: false});
|
||||
assert.equal(await page.locator("#colorPreview").isDisabled(), true);
|
||||
await page.locator("#userColors button:not(.empty)").first().click();
|
||||
assert.equal(await page.locator("#hexInput").inputValue(), "ABCDEF");
|
||||
await page.reload();
|
||||
assert.equal(await send(init(ready)), false); assert.equal(await page.locator("#confirmBtn").isDisabled(), true);
|
||||
});
|
||||
|
||||
test("Document strings translate palette names and editor errors while retaining RAL codes and numeric labels", options, async t => {
|
||||
const source = readFileSync(resolve(__dirname, "../../../../src/slic3r/GUI/ColorPickerDialog.cpp"), "utf8");
|
||||
const keys = [...source.matchAll(/_u8L\("([^"\n]+)"\)/g)].map(match => match[1]);
|
||||
const strings = Object.fromEntries(keys.map(key => [key, "Translated: " + key]));
|
||||
const {page, ready, send} = await open(t, strings); await send({...init(ready, true, true), options: {allow_gradient: true, allow_alpha: true, max_gradient_colors: 7}, preserve_multi_color: true});
|
||||
assert.equal(await page.title(), strings["Color Picker"]);
|
||||
for (const element of await page.locator("[data-i18n]").all()) assert.equal(await element.textContent(), strings[await element.getAttribute("data-i18n")]);
|
||||
for (const attribute of ["aria-label", "title"]) for (const element of await page.locator(`[data-i18n-${attribute}]`).all())
|
||||
assert.equal((await element.getAttribute(attribute)).split("\n")[0], strings[await element.getAttribute(`data-i18n-${attribute}`)]);
|
||||
assert.equal(await page.locator("#selectionNotice").textContent(), strings["The current multi-color selection is kept until you choose a different color."]);
|
||||
for (const palette of ["basic", "orca", "ral", "gradient"]) {
|
||||
await page.selectOption("#paletteCombo", palette);
|
||||
const names = await page.evaluate(value => ColorPickerPalettes[value].map(entry => entry.name), palette);
|
||||
assert.deepEqual(await page.locator(".color-name").allTextContents(), names.map(name => {
|
||||
const ral = name.match(/^(\d{4}) (.+)$/); return ral ? `${ral[1]} ${strings[ral[2]]}` : /^%\d+$/.test(name) ? name : strings[name];
|
||||
}));
|
||||
}
|
||||
await page.locator("#hexInput").fill("bad");
|
||||
assert.equal(await page.locator("#hexInput").evaluate(element => element.validationMessage), strings["Enter a complete hexadecimal color"]);
|
||||
await page.locator("#hexInput").fill("12345680");
|
||||
for (const [id, invalid, valid, key] of [["rValue", "", "18", "Invalid input"],
|
||||
["oValue", "", "50", "Invalid input"]]) {
|
||||
const confirms = await count(page, "confirm");
|
||||
await page.locator("#" + id).fill(invalid);
|
||||
await page.locator("#confirmBtn").evaluate(element => element.click());
|
||||
await page.locator("#colorPreview").click(); // Saving invokes the same translated validation while OK is disabled.
|
||||
assert.equal(await count(page, "confirm"), confirms);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), strings[key]);
|
||||
await page.locator("#" + id).fill(valid);
|
||||
assert.equal(await page.locator("#" + id).evaluate(element => element.validationMessage), "");
|
||||
await page.locator("#confirmBtn").click();
|
||||
assert.equal(await count(page, "confirm"), confirms + 1);
|
||||
}
|
||||
await page.locator("#hValue").fill("361");
|
||||
await page.locator("#colorPreview").click();
|
||||
assert.equal(await page.locator("#hValue").inputValue(), "360", "out-of-range hue clamps at its boundary");
|
||||
await page.locator(".color-item").first().click();
|
||||
assert.equal(await page.locator("#hValue").evaluate(element => element.validationMessage), "", "a new palette selection clears the previous editor error");
|
||||
});
|
||||
|
||||
test("Document language and escaped labels preserve untranslated English fallback", options, async t => {
|
||||
const cancel = '取消 "Cancel" \\ /\n下一行';
|
||||
const {page, ready, send} = await open(t, {Cancel: cancel}, "zh-CN");
|
||||
await send(init(ready));
|
||||
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
|
||||
assert.equal(await page.locator("#cancelBtn").textContent(), cancel);
|
||||
assert.equal(await page.title(), "Color Picker");
|
||||
assert.equal(await page.locator(".color-name").first().textContent(), "Red");
|
||||
assert.deepEqual(await page.locator("label[for=hValue],label[for=sValue],label[for=lValue],label[for=rValue],label[for=gValue],label[for=bValue]").allTextContents(), ["H", "S", "L", "R", "G", "B"]);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), {type: "solid", colors: ["#012345FF"]});
|
||||
});
|
||||
|
||||
test("Global control colors follow the system while host semantic colors follow the app", options, async t => {
|
||||
const {page, ready, send} = await open(t); await send({...init(ready, true, true), selection: {type: "gradient", colors: ["#FF000001", "#0000FF80"]}});
|
||||
const initial = await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()}));
|
||||
for (const theme of ["light", "dark"]) for (const system of ["light", "dark"]) {
|
||||
await page.emulateMedia({colorScheme: system});
|
||||
await page.evaluate(value => {
|
||||
document.documentElement.dataset.orcaTheme = value;
|
||||
const colors = value === "light" ? {bg: "#ffffff", fg: "#262e30", muted: "#363636", border: "#dbdbdb", accent: "#009688"} : {bg: "#2d2d31", fg: "#efeff0", muted: "#b2b3b5", border: "#36363b", accent: "#00675b"};
|
||||
for (const [key, color] of Object.entries(colors)) document.documentElement.style.setProperty("--orca-" + key, color);
|
||||
document.documentElement.style.setProperty("--orca-accent-fg", "#ffffff");
|
||||
}, theme);
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||
});
|
||||
const colors = await page.evaluate(() => {
|
||||
const body = getComputedStyle(document.body);
|
||||
const tokenColor = token => {
|
||||
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||
};
|
||||
const mode = document.querySelector("label[for=modeSwitch]");
|
||||
return {background: body.backgroundColor, foreground: body.color,
|
||||
modeBackground: getComputedStyle(mode).backgroundColor,
|
||||
accent: getComputedStyle(mode, "::before").backgroundColor,
|
||||
hostPanel: tokenColor("--panel"), hostText: tokenColor("--text")};
|
||||
});
|
||||
assert.equal(colors.background, system === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)", `${theme} app / ${system} system body`);
|
||||
assert.equal(colors.foreground, system === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||
assert.equal(colors.modeBackground, system === "light" ? "rgb(240, 240, 240)" : "rgb(36, 36, 40)");
|
||||
assert.equal(colors.accent, theme === "light" ? "rgb(0, 150, 136)" : "rgb(0, 103, 91)", "shared accent follows the injected app theme");
|
||||
assert.equal(colors.hostPanel, theme === "light" ? "rgb(255, 255, 255)" : "rgb(45, 45, 49)");
|
||||
assert.equal(colors.hostText, theme === "light" ? "rgb(38, 46, 48)" : "rgb(239, 239, 240)");
|
||||
assert.deepEqual(await page.evaluate(() => ({selection: ColorPickerDialog.exportSelection(), spectrum: document.getElementById("spectrum").toDataURL()})), initial);
|
||||
for (const id of ["modeSwitch"]) for (const checked of [false, true]) {
|
||||
if (await page.locator("#" + id).isChecked() !== checked) await page.locator(`label[for=${id}]`).click();
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(document.getAnimations().map(animation => animation.finished.catch(() => {})));
|
||||
});
|
||||
const result = await page.locator(`label[for=${id}]`).evaluate((element, selected) => {
|
||||
const tokenColor = token => {
|
||||
const probe = document.createElement("span"); probe.style.cssText = `position:absolute;visibility:hidden;color:var(${token})`;
|
||||
document.body.appendChild(probe); const color = getComputedStyle(probe).color; probe.remove(); return color;
|
||||
};
|
||||
const labels = element.querySelectorAll(".switch-label");
|
||||
return {selected: getComputedStyle(labels[selected ? 1 : 0]).color,
|
||||
unselected: getComputedStyle(labels[selected ? 0 : 1]).color,
|
||||
accentForeground: tokenColor("--button-fg-light"), mutedForeground: tokenColor("--fg-color-label")};
|
||||
}, checked);
|
||||
assert.equal(result.selected, result.accentForeground, `${theme} app / ${system} system ${id} selected text uses the accent foreground`);
|
||||
assert.equal(result.selected, "rgb(254, 254, 254)", "global selected text stays light in both system schemes");
|
||||
assert.equal(result.unselected, result.mutedForeground, `${theme} app / ${system} system ${id} unselected text stays muted`);
|
||||
assert.equal(result.unselected, system === "light" ? "rgb(54, 54, 54)" : "rgb(178, 179, 181)");
|
||||
const selected = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
assert.equal(selected.type, checked ? "gradient" : "solid");
|
||||
if (checked) assert.deepEqual(selected, initial.selection, "returning to gradient keeps ordered endpoint byte alpha");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Sidebar resizes from a compact viewport and shrinks after every collapse", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await page.setViewportSize({width: 340, height: 380});
|
||||
await send({...init(ready, true, true), ui: {sidebar_open: false, palette: "basic"}});
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
for (const [index, sidebarOpen] of [false, true, false, true, false].entries()) {
|
||||
const width = sidebarOpen ? 510 : 340;
|
||||
await page.waitForFunction(expected => messages.filter(message => message.command === "resize").at(-1)?.width === expected, width);
|
||||
const size = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1));
|
||||
assert.equal(size.height, 380);
|
||||
await page.setViewportSize({width: size.width, height: size.height});
|
||||
await page.waitForTimeout(100);
|
||||
assert(await page.evaluate(() => {
|
||||
const body = document.body.getBoundingClientRect();
|
||||
const picker = document.querySelector(".right-panel").getBoundingClientRect();
|
||||
return body.width === innerWidth && picker.right <= body.right && picker.bottom <= body.bottom;
|
||||
}), "the host viewport contains the full picker");
|
||||
const resizes = await count(page, "resize");
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await count(page, "resize"), resizes, "fitting the host does not cause a resize loop");
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||
if (index < 4) await page.locator("#paletteToggle").click();
|
||||
}
|
||||
});
|
||||
|
||||
test("Floating panel fits capabilities and wrapped translations without a resize feedback loop", options, async t => {
|
||||
const strings = {Color: "A long translated color label", Gradient: "A long translated gradient label", Cancel: "A longer translated cancel action", OK: "A longer translated confirmation action",
|
||||
"The current multi-color selection is kept until you choose a different color.": "A translated explanation that wraps across the compact floating panel and keeps the original multi-color selection until another color is chosen."};
|
||||
for (const gradient of [false, true]) for (const alpha of [false, true]) {
|
||||
const {page, ready, send} = await open(t, strings);
|
||||
await page.setViewportSize({width: 550, height: 520});
|
||||
await send({...init(ready, gradient, alpha), preserve_multi_color: true});
|
||||
await page.waitForFunction(() => messages.some(message => message.command === "resize"));
|
||||
const height = await page.evaluate(() => messages.filter(message => message.command === "resize").at(-1).height);
|
||||
assert(height >= 300 && height <= 900);
|
||||
assert.equal(height, await page.locator("body").evaluate(element => Math.ceil(element.getBoundingClientRect().height)));
|
||||
assert(await page.evaluate(() => messages.findIndex(message => message.command === "initialized") < messages.findIndex(message => message.command === "resize")));
|
||||
await page.setViewportSize({width: 550, height});
|
||||
await page.waitForTimeout(100);
|
||||
const resizes = await count(page, "resize");
|
||||
await page.setViewportSize({width: 550, height: height + 100});
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await count(page, "resize"), resizes, "extra viewport height never changes intrinsic content height");
|
||||
const controls = await page.evaluate(() => {
|
||||
const body = document.body.getBoundingClientRect();
|
||||
return ["confirmBtn", "cancelBtn", ...(document.getElementById("alphaControls").classList.contains("hidden") ? [] : ["oValue"])].map(id => {
|
||||
const rect = document.getElementById(id).getBoundingClientRect();
|
||||
return rect.left >= body.left && rect.right <= body.right && rect.bottom <= body.bottom;
|
||||
});
|
||||
});
|
||||
assert(controls.every(Boolean), "wrapped buttons and opacity fit inside the panel");
|
||||
const edited = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("selectionNotice").textContent += " Additional wrapped text. ".repeat(15);
|
||||
});
|
||||
await page.waitForFunction(previous => messages.filter(message => message.command === "resize").length > previous, resizes);
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), edited, "content resize leaves the selection untouched");
|
||||
// The compact layout now puts saved swatches to the right of both previews.
|
||||
assert(await page.evaluate(() => {
|
||||
const favorites = document.getElementById("userColors").getBoundingClientRect();
|
||||
const preview = document.getElementById("colorPreview").getBoundingClientRect();
|
||||
return favorites.left >= preview.right;
|
||||
}), "favorites do not overlap the preview");
|
||||
await page.locator("#confirmBtn").scrollIntoViewIfNeeded();
|
||||
assert.equal(await page.locator("#confirmBtn").isVisible(), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Native focus hook waits for initialization and preserves active partial edits", options, async t => {
|
||||
const {page, ready, send} = await open(t);
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.notEqual(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||
await send(init(ready, true, true));
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.equal(await page.evaluate(() => document.activeElement.id), "hexInput");
|
||||
await page.locator("#rValue").fill("");
|
||||
const selection = await page.evaluate(() => ColorPickerDialog.exportSelection());
|
||||
await page.evaluate(() => ColorPickerDialog.focusInput());
|
||||
assert.equal(await page.evaluate(() => document.activeElement.id), "rValue");
|
||||
assert.equal(await page.locator("#rValue").inputValue(), "");
|
||||
assert.deepEqual(await page.evaluate(() => ColorPickerDialog.exportSelection()), selection);
|
||||
assert.equal(await count(page, "confirm"), 0);
|
||||
await page.locator("#rValue").press("Escape");
|
||||
assert.equal(await count(page, "cancel"), 1);
|
||||
});
|
||||
@@ -1,464 +0,0 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
if (window.ORCA_COLOR_PICKER_LANGUAGE) document.documentElement.lang = window.ORCA_COLOR_PICKER_LANGUAGE;
|
||||
const T = (key) => window.ORCA_UI_STRINGS?.[key] || key;
|
||||
document.title = T("Color Picker");
|
||||
for (const element of document.querySelectorAll("[data-i18n]")) element.textContent = T(element.dataset.i18n);
|
||||
for (const attribute of ["aria-label", "title", "placeholder"])
|
||||
for (const element of document.querySelectorAll(`[data-i18n-${attribute}]`))
|
||||
element.setAttribute(attribute, T(element.getAttribute(`data-i18n-${attribute}`)));
|
||||
function paletteName(name) {
|
||||
const ral = name.match(/^(\d{4}) (.+)$/);
|
||||
return ral ? `${ral[1]} ${T(ral[2])}` : /^%\d+$/.test(name) ? name : T(name);
|
||||
}
|
||||
|
||||
const FAVORITE_SLOTS = 16;
|
||||
const SPECTRUM_FOLLOWS_SATURATION = true; // true: the spectrum is drawn at the current saturation, false: it always shows full saturation
|
||||
const MULTI_COLOR_GRADIENTS = true; // true: gradients may have more than 2 colors (− / + buttons and the multi-color palette entries), false: exactly 2 colors.
|
||||
// up to the native limit the host sends as options.max_gradient_colors.
|
||||
let maxGradientColors = 2; // Stays 2 until init, and with a host that does not send a limit.
|
||||
|
||||
const model = window.ColorPickerModel;
|
||||
const palettes = window.ColorPickerPalettes;
|
||||
const byId = (id) => document.getElementById(id);
|
||||
const canvas = byId("spectrum"), context = canvas.getContext("2d");
|
||||
const paletteCombo = byId("paletteCombo"), palette = byId("palette"), preview = byId("colorPreview"), confirmBtn = byId("confirmBtn");
|
||||
const modeSwitch = byId("modeSwitch"), satSlider = byId("satSlider"), opacitySlider = byId("opacitySlider");
|
||||
const gradientBar = byId("gradientCtrlBar"), gradientStops = byId("gradientStops"), gradientAdd = byId("gradientAdd"), gradientRemove = byId("gradientRemove");
|
||||
const hostGradientLimit = (options) => options.max_gradient_colors === undefined ? 2 : options.max_gradient_colors;
|
||||
const originalPreview = byId("originalPreview"), palettePanel = byId("palettePanel"), paletteToggle = byId("paletteToggle");
|
||||
const paletteSelect = byId("paletteSelect"), paletteFilter = byId("paletteFilter"), paletteSearchToggle = byId("paletteSearchToggle");
|
||||
const rgbInputs = ["r", "g", "b"].map((name) => byId(name + "Value"));
|
||||
const hslInputs = ["h", "s", "l"].map((name) => byId(name + "Value"));
|
||||
const hexInput = byId("hexInput"), opacityInput = byId("oValue");
|
||||
const pageId = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||||
let state = model.createState(), favorites = [], initialized = false, favoritesWritable = true;
|
||||
// The selection the dialog opened with; the original swatch shows it and restores it on click.
|
||||
let originalSelection = model.exportSelection(state);
|
||||
let lastContentHeight = 0, lastContentWidth = 0, resizePending = false;
|
||||
// Last HSL shown or set. Kept while it still produces the current color: hue 360 and 0 are
|
||||
// both red, and greys, black and white have no hue or saturation in RGB.
|
||||
let hslMemory = [0, 0, 50];
|
||||
function displayHsl(color) {
|
||||
const rgb = model.hslToRgb(...hslMemory);
|
||||
if (!rgb.every((value, index) => value === color[index])) hslMemory = model.rgbToHsl(...color.slice(0, 3));
|
||||
return hslMemory;
|
||||
}
|
||||
// Copy of `values` with one entry replaced.
|
||||
const replaced = (values, index, value) => values.map((current, i) => i === index ? value : current);
|
||||
function setHslChannel(index, value) {
|
||||
hslMemory = replaced(displayHsl(model.activeColor(state)), index, value);
|
||||
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||
}
|
||||
const round1 = (value) => Math.round(value * 10) / 10;
|
||||
|
||||
function emit(command, data = {}) {
|
||||
window.dispatchEvent(new CustomEvent("color-picker-message", {detail: {command, ...data}}));
|
||||
if (window.wx && typeof window.wx.postMessage === "function")
|
||||
window.wx.postMessage(JSON.stringify({command, page_id: pageId, ...data}));
|
||||
}
|
||||
|
||||
function scheduleResize() {
|
||||
if (!initialized || resizePending) return;
|
||||
resizePending = true;
|
||||
requestAnimationFrame(() => {
|
||||
resizePending = false;
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
const height = Math.min(900 , Math.max(300, Math.ceil(rect.height)));
|
||||
const width = Math.min(1200, Math.max(300, Math.ceil(rect.width )));
|
||||
if (height !== lastContentHeight || width !== lastContentWidth) {
|
||||
lastContentHeight = height; lastContentWidth = width;
|
||||
emit("resize", {width, height});
|
||||
}
|
||||
});
|
||||
}
|
||||
new ResizeObserver(scheduleResize).observe(document.body);
|
||||
|
||||
function setSwatch(element, selection) {
|
||||
element.style.setProperty("--swatch-color", model.selectionBackground(selection));
|
||||
}
|
||||
|
||||
// `editing` is the control the user is typing in. Its text is left alone so the caret,
|
||||
// partial input like "05" and fine steps that round-trip through bytes aren't overwritten.
|
||||
function render(resetEditors = false, editing = null) {
|
||||
const color = model.activeColor(state), hsl = displayHsl(color);
|
||||
const show = (input, value) => {
|
||||
if (input === editing) return;
|
||||
if (resetEditors) input.setCustomValidity("");
|
||||
if (resetEditors || input.checkValidity()) input.value = value;
|
||||
};
|
||||
show(hexInput, model.toHex(color).slice(1, state.options.allow_alpha ? 9 : 7));
|
||||
rgbInputs.forEach((input, index) => show(input, color[index]));
|
||||
// Shown in whole numbers; decimals can still be typed, and hslMemory keeps exact values.
|
||||
hslInputs.forEach((input, index) => show(input, Math.round(hsl[index])));
|
||||
// Percentage is a display only until explicitly edited. Never round it back
|
||||
// into the alpha byte on endpoint switches, RGB edits, or confirmation.
|
||||
show(opacityInput, Math.round(color[3] / 255 * 100));
|
||||
if (opacitySlider !== editing) opacitySlider.value = color[3] / 255 * 100;
|
||||
const rgb = color.slice(0, 3).join(",");
|
||||
opacitySlider.style.setProperty("--track-gradient", `linear-gradient(to right, rgba(${rgb},0), rgb(${rgb}))`);
|
||||
opacitySlider.style.setProperty("--thumb-color", `rgba(${rgb},${color[3] / 255})`);
|
||||
if (satSlider !== editing) satSlider.value = hsl[1];
|
||||
satSlider.style.setProperty("--track-gradient", `linear-gradient(to right, hsl(${hsl[0]},0%,${hsl[2]}%), hsl(${hsl[0]},100%,${hsl[2]}%))`);
|
||||
satSlider.style.setProperty("--thumb-color", `hsl(${hsl[0]},${hsl[1]}%,${hsl[2]}%)`);
|
||||
modeSwitch.checked = state.mode === "gradient";
|
||||
const selection = model.exportSelection(state);
|
||||
// Kept in the layout (visibility only) so the grid doesn't shift between Solid and Gradient.
|
||||
gradientBar.classList.toggle("invisible", state.mode !== "gradient");
|
||||
if (state.mode === "gradient") renderGradient(selection);
|
||||
setSwatch(preview, selection);
|
||||
preview.title = `${T("Save current color")}\n${selection.colors.join(" → ")}`;
|
||||
if (SPECTRUM_FOLLOWS_SATURATION && hsl[1] !== spectrumSaturation) { spectrumSaturation = hsl[1]; paintSpectrum(); }
|
||||
updateIndicator(hsl);
|
||||
updateConfirm();
|
||||
}
|
||||
|
||||
// One thumb per gradient color, evenly spaced; rebuilt only when the number of colors changes.
|
||||
function renderGradient(selection) {
|
||||
const count = selection.colors.length;
|
||||
setSwatch(gradientStops.querySelector(".gradient-track"), selection);
|
||||
let thumbs = [...gradientStops.querySelectorAll(".gradient-thumb")];
|
||||
if (thumbs.length !== count) {
|
||||
thumbs.forEach((thumb) => thumb.remove());
|
||||
thumbs = selection.colors.map((_, index) => {
|
||||
const thumb = document.createElement("button");
|
||||
thumb.type = "button"; thumb.className = "gradient-thumb"; thumb.dataset.index = index;
|
||||
thumb.style.setProperty("--stop", index / (count - 1));
|
||||
thumb.setAttribute("aria-label", count === 2 ? T(index ? "Gradient end" : "Gradient start") : `${T("Gradient color")} ${index + 1}`);
|
||||
gradientStops.appendChild(thumb);
|
||||
return thumb;
|
||||
});
|
||||
}
|
||||
thumbs.forEach((thumb, index) => {
|
||||
thumb.style.setProperty("--thumb-color", selection.colors[index]);
|
||||
thumb.setAttribute("aria-pressed", String(state.active === index));
|
||||
});
|
||||
|
||||
const multi = maxGradientColors > 2; // −/+ buttons only exist when more than 2 colors are allowed; − also needs a color to spare.
|
||||
gradientAdd.parentElement.classList.toggle("hidden", !multi);
|
||||
gradientRemove.classList.toggle("hidden", !multi || count <= 2);
|
||||
gradientAdd.classList.toggle("hidden", !multi);
|
||||
// aria-disabled instead of disabled: a disabled button gets no hover, so its tooltip would never show.
|
||||
const full = count >= maxGradientColors;
|
||||
gradientAdd.setAttribute("aria-disabled", String(full));
|
||||
gradientAdd.title = full ? T("Up to %d colors supported.").replace("%d", maxGradientColors) : T("Add color");
|
||||
}
|
||||
|
||||
// Every editor the user can see; alpha only when this caller allows it.
|
||||
function editors() {
|
||||
const inputs = [hexInput, ...rgbInputs, ...hslInputs];
|
||||
if (state.options.allow_alpha) inputs.push(opacityInput);
|
||||
return inputs;
|
||||
}
|
||||
// OK stays disabled while any visible editor holds an invalid or incomplete value.
|
||||
function updateConfirm() {
|
||||
confirmBtn.disabled = !editors().every((input) => input.checkValidity());
|
||||
}
|
||||
|
||||
// Matches the filter against the shown (translated) name, the English name and the hex digits.
|
||||
function paletteMatches(entry, query) {
|
||||
if (!query) return true;
|
||||
const haystack = [paletteName(entry.name), entry.name, entry.hex || "", entry.grad || ""].join(" ").toLowerCase();
|
||||
return query.split(/\s+/).every((word) => haystack.includes(word.replace(/^#/, "")));
|
||||
}
|
||||
|
||||
function populatePalette() {
|
||||
palette.replaceChildren();
|
||||
const query = paletteSearchToggle.getAttribute("aria-pressed") === "true" ? paletteFilter.value.trim().toLowerCase() : "";
|
||||
for (const entry of palettes[paletteCombo.value] || []) {
|
||||
const selection = {type: entry.grad ? "gradient" : "solid", colors: entry.grad ? entry.grad.split("-") : [entry.hex]};
|
||||
if (!model.normalizeSelection(selection, state.options, maxGradientColors) || !paletteMatches(entry, query)) continue;
|
||||
const row = document.createElement("button"); row.type = "button"; row.className = "color-item";
|
||||
const swatch = document.createElement("span"); swatch.className = "color-swatch";
|
||||
const name = document.createElement("span"); name.className = "color-name"; name.textContent = paletteName(entry.name);
|
||||
setSwatch(swatch, model.normalizeSelection(selection, {allow_gradient: true, allow_alpha: true}, maxGradientColors));
|
||||
row.append(swatch, name); row.title = paletteName(entry.name);
|
||||
row.addEventListener("click", () => {
|
||||
// Solid palette colors edit the active endpoint while gradient mode is on.
|
||||
const changed = entry.grad ? model.applySelection(state, selection) : model.setHex(state, entry.hex);
|
||||
if (changed) render(true);
|
||||
});
|
||||
palette.appendChild(row);
|
||||
}
|
||||
byId("paletteEmpty").classList.toggle("hidden", palette.childElementCount > 0);
|
||||
palette.classList.toggle("hidden", palette.childElementCount === 0);
|
||||
}
|
||||
|
||||
// Sidebar state the host keeps in the app config: open/closed and the chosen palette.
|
||||
const uiState = () => ({sidebar_open: palettePanel.classList.contains("open"), palette: paletteCombo.value});
|
||||
function setSidebarOpen(open) {
|
||||
palettePanel.classList.toggle("open", open);
|
||||
paletteToggle.setAttribute("aria-expanded", String(open));
|
||||
}
|
||||
// The search button swaps the combo box for a filter box; closing it clears the filter.
|
||||
function setPaletteSearch(open) {
|
||||
paletteSearchToggle.setAttribute("aria-pressed", String(open));
|
||||
paletteSelect.classList.toggle("hidden", open);
|
||||
paletteFilter.classList.toggle("hidden", !open);
|
||||
if (open) paletteFilter.focus();
|
||||
else paletteFilter.value = "";
|
||||
populatePalette();
|
||||
}
|
||||
|
||||
function drawFavorites() {
|
||||
const grid = byId("userColors"); grid.replaceChildren();
|
||||
// Favorites keep every color, but only those this page can select are shown.
|
||||
const visible = favorites.filter((favorite) => model.normalizeSelection(favorite, {...state.options, allow_alpha: true}, maxGradientColors));
|
||||
for (let index = 0; index < FAVORITE_SLOTS; ++index) {
|
||||
const swatch = document.createElement("button"); swatch.type = "button"; swatch.className = "saved-swatch";
|
||||
const favorite = visible[index];
|
||||
if (favorite) {
|
||||
setSwatch(swatch, favorite); swatch.title = favorite.colors.join(" → ");
|
||||
swatch.addEventListener("click", () => { if (model.applySelection(state, favorite)) render(true); });
|
||||
} else { swatch.classList.add("empty"); swatch.disabled = true; }
|
||||
grid.appendChild(swatch);
|
||||
}
|
||||
}
|
||||
|
||||
function init(payload) {
|
||||
if (!payload || typeof payload !== "object" || (payload.options !== undefined && (!payload.options || typeof payload.options !== "object"))) return false;
|
||||
maxGradientColors = Math.min(MULTI_COLOR_GRADIENTS ? model.MAX_GRADIENT_COLORS : 2, hostGradientLimit(payload.options || {}));
|
||||
// A gradient the host allows but this page does not (switch off) opens as its first color, like
|
||||
// other multi-color selections the picker cannot edit; OK then replaces it, Cancel keeps it.
|
||||
let selection = payload.selection, preserve = payload.preserve_multi_color === true;
|
||||
if (selection?.type === "gradient" && selection.colors?.length > maxGradientColors) {
|
||||
selection = {type: "solid", colors: [selection.colors[0]]};
|
||||
preserve = true;
|
||||
}
|
||||
const next = model.createState({...payload.options, max_gradient_colors: maxGradientColors}, selection);
|
||||
if (!next) return false;
|
||||
byId("selectionNotice").hidden = !preserve;
|
||||
state = next; favorites = model.normalizeFavorites(payload.favorites || []);
|
||||
originalSelection = model.exportSelection(state);
|
||||
setSwatch(originalPreview, originalSelection);
|
||||
originalPreview.title = `${T("Original color")}\n${originalSelection.colors.join(" → ")}`;
|
||||
favoritesWritable = payload.favorites_writable !== false;
|
||||
preview.disabled = !favoritesWritable;
|
||||
byId("modeControls").classList.toggle("hidden", !state.options.allow_gradient);
|
||||
for (const option of paletteCombo.options) option.hidden = option.value === "gradient" && !state.options.allow_gradient;
|
||||
const ui = payload.ui || {}; // Restore the sidebar the way it was left; a palette this caller can't use falls back to basic.
|
||||
const saved = [...paletteCombo.options].find((option) => option.value === ui.palette && !option.hidden);
|
||||
if (saved) paletteCombo.value = saved.value;
|
||||
if (!state.options.allow_gradient && paletteCombo.value === "gradient") paletteCombo.value = "basic";
|
||||
setSidebarOpen(ui.sidebar_open === true);
|
||||
byId("alphaControls").classList.toggle("hidden", !state.options.allow_alpha);
|
||||
hexInput.maxLength = state.options.allow_alpha ? 9 : 7;
|
||||
populatePalette(); drawFavorites(); render(true);
|
||||
initialized = true;
|
||||
byId("pickerEditors").disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (!initialized) return;
|
||||
// Restore initial keyboard focus without replacing an editor's in-progress input.
|
||||
const active = document.activeElement;
|
||||
if (!active || active === document.body || active === document.documentElement)
|
||||
hexInput.focus({preventScroll: true});
|
||||
}
|
||||
|
||||
function handleMessage(payload) {
|
||||
if (!payload || typeof payload !== "object" || payload.page_id !== pageId) return false;
|
||||
if (payload.command === "init") {
|
||||
const options = payload.options;
|
||||
const hostLimit = options && hostGradientLimit(options);
|
||||
if (!options || typeof options.allow_gradient !== "boolean" || typeof options.allow_alpha !== "boolean" ||
|
||||
!Number.isInteger(hostLimit) || hostLimit < 2 || hostLimit > model.MAX_GRADIENT_COLORS ||
|
||||
!model.normalizeSelection(payload.selection, options, hostLimit) || !validFavorites(payload.favorites) ||
|
||||
(payload.favorites_writable !== undefined && typeof payload.favorites_writable !== "boolean") ||
|
||||
(payload.preserve_multi_color !== undefined && typeof payload.preserve_multi_color !== "boolean") ||
|
||||
(payload.ui !== undefined && (!payload.ui || typeof payload.ui !== "object" ||
|
||||
(payload.ui.sidebar_open !== undefined && typeof payload.ui.sidebar_open !== "boolean") ||
|
||||
(payload.ui.palette !== undefined && typeof payload.ui.palette !== "string")))) return false;
|
||||
if (initialized) return true;
|
||||
if (!init(payload)) return false;
|
||||
emit("initialized");
|
||||
scheduleResize();
|
||||
return true;
|
||||
}
|
||||
if (payload.command === "favorites" && initialized && validFavorites(payload.favorites)) {
|
||||
favorites = model.normalizeFavorites(payload.favorites); drawFavorites(); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function validFavorites(values) {
|
||||
return Array.isArray(values) && values.length <= 24 && values.every((value) => model.normalizeSelection(value, {allow_gradient: true, allow_alpha: true}, model.MAX_GRADIENT_COLORS));
|
||||
}
|
||||
|
||||
// Saturation the spectrum is currently painted at (always 100 when the option is off).
|
||||
let spectrumSaturation = 100;
|
||||
function drawSpectrum() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.round(rect.width)); canvas.height = Math.max(1, Math.round(rect.height));
|
||||
paintSpectrum();
|
||||
}
|
||||
function paintSpectrum() {
|
||||
const hue = context.createLinearGradient(0, 0, canvas.width, 0);
|
||||
for (let i = 0; i <= 6; ++i) hue.addColorStop(i / 6, `hsl(${i * 60},100%,50%)`);
|
||||
context.fillStyle = hue; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const shade = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||
shade.addColorStop(0, "#FFFFFF"); shade.addColorStop(0.5, "#FFFFFF00"); shade.addColorStop(0.5, "#00000000"); shade.addColorStop(1, "#000000");
|
||||
context.fillStyle = shade; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
// HSL desaturation blends each color toward the grey of the same lightness, and on this
|
||||
// canvas that grey only depends on the row (white at the top, black at the bottom).
|
||||
if (spectrumSaturation < 100) {
|
||||
const grey = context.createLinearGradient(0, 0, 0, canvas.height);
|
||||
grey.addColorStop(0, "#FFFFFF"); grey.addColorStop(1, "#000000");
|
||||
context.globalAlpha = 1 - spectrumSaturation / 100;
|
||||
context.fillStyle = grey; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const indicator = byId("colorIndicator");
|
||||
function updateIndicator(hsl) {
|
||||
indicator.style.left = hsl[0] / 360 * (canvas.width - 1) + "px";
|
||||
indicator.style.top = (1 - hsl[2] / 100) * (canvas.height - 1) + "px";
|
||||
}
|
||||
|
||||
// Spectrum position maps straight to hue (x, 0..360) and lightness (y, 100..0); saturation is
|
||||
// the third dimension and comes from the slider below. hslMemory keeps these exact values,
|
||||
// including hue 360 at the right edge and the hue of the white and black corners.
|
||||
function sampleSpectrum(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const fraction = (client, start, size, pixels) =>
|
||||
Math.max(0, Math.min(pixels - 1, Math.floor((client - start) * pixels / size))) / Math.max(1, pixels - 1);
|
||||
const saturation = displayHsl(model.activeColor(state))[1];
|
||||
hslMemory = [round1(fraction(event.clientX, rect.left, rect.width, canvas.width) * 360), saturation,
|
||||
round1((1 - fraction(event.clientY, rect.top, rect.height, canvas.height)) * 100)];
|
||||
model.setRgb(state, model.hslToRgb(...hslMemory));
|
||||
// A pick replaces every editor's value, so clear any invalid state left from typing.
|
||||
render(true);
|
||||
}
|
||||
|
||||
const clamp = (control, value) => Math.min(Number(control.max), Math.max(Number(control.min), value));
|
||||
function bindValue(control, apply) {
|
||||
// Number fields are required, so an empty field is invalid; a range input always has a value.
|
||||
if (control.type === "number") control.required = true;
|
||||
const commit = (value) => { apply(value); render(false, control); };
|
||||
control.addEventListener("input", () => {
|
||||
control.setCustomValidity("");
|
||||
// Out-of-range numbers snap to the field's min/max instead of becoming invalid.
|
||||
if (control.value !== "") {
|
||||
const clamped = clamp(control, Number(control.value));
|
||||
if (clamped !== Number(control.value)) control.value = clamped;
|
||||
}
|
||||
if (control.checkValidity()) commit(Number(control.value));
|
||||
});
|
||||
control.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const step = control.type === "range" ? 5 : 1;
|
||||
control.value = clamp(control, (Number(control.value) || 0) + (event.deltaY > 0 ? -step : step));
|
||||
control.setCustomValidity("");
|
||||
commit(Number(control.value));
|
||||
}, {passive: false});
|
||||
// Normalize the text (e.g. "05" → "5") once the user leaves the field.
|
||||
control.addEventListener("blur", () => { if (control.checkValidity()) render(); });
|
||||
}
|
||||
|
||||
rgbInputs.forEach((input, index) => bindValue(input, (value) => model.setRgb(state, replaced(model.activeColor(state).slice(0, 3), index, value))));
|
||||
hslInputs.forEach((input, index) => bindValue(input, (value) => setHslChannel(index, value)));
|
||||
bindValue(satSlider, (value) => setHslChannel(1, value));
|
||||
bindValue(opacityInput, (value) => model.setAlphaPercent(state, value));
|
||||
bindValue(opacitySlider, (value) => model.setAlphaPercent(state, value));
|
||||
// The opacity slider takes the gradient bar's row while the opacity field or the slider is in
|
||||
// use: it opens when the field is clicked or focused, and closes on a click or focus elsewhere.
|
||||
const thirdRow = byId("thirdRow"), alphaControls = byId("alphaControls");
|
||||
const inOpacityControls = (target) => target instanceof Element && target.closest("#alphaControls, #opacitySliderBar");
|
||||
const setOpacitySliderOpen = (open) => thirdRow.classList.toggle("opacity-open", open);
|
||||
for (const type of ["focusin", "click"]) alphaControls.addEventListener(type, () => setOpacitySliderOpen(true));
|
||||
for (const type of ["pointerdown", "focusin"])
|
||||
document.addEventListener(type, (event) => { if (!inOpacityControls(event.target)) setOpacitySliderOpen(false); });
|
||||
hexInput.addEventListener("input", () => {
|
||||
const text = hexInput.value;
|
||||
const valid = model.setHex(state, text.startsWith("#") ? text : "#" + text);
|
||||
hexInput.setCustomValidity(valid ? "" : T("Enter a complete hexadecimal color"));
|
||||
// render() leaves the typed text alone, so alpha isn't appended after the sixth digit.
|
||||
if (valid) render(false, hexInput);
|
||||
});
|
||||
hexInput.addEventListener("blur", () => { if (hexInput.checkValidity()) render(); });
|
||||
modeSwitch.addEventListener("change", () => {
|
||||
|
||||
setOpacitySliderOpen(false); // Close the opacity slider before the row changes, so it doesn't linger over the gradient bar.
|
||||
model.setMode(state, modeSwitch.checked ? "gradient" : "solid");
|
||||
render(true);
|
||||
});
|
||||
// Click on the track selects the nearest thumb. Keyboard users tab to a thumb and press Enter or Space.
|
||||
gradientStops.addEventListener("click", (event) => {
|
||||
if (state.mode !== "gradient") return;
|
||||
// A thumb is read from its index: a keyboard-triggered click has no pointer position.
|
||||
const thumb = event.target.closest(".gradient-thumb"), rect = gradientStops.getBoundingClientRect();
|
||||
const last = state.gradient.length - 1;
|
||||
// Thumbs sit at 10px + (width - 20px) * i / last, matching the CSS.
|
||||
const fraction = Math.max(0, Math.min(1, (event.clientX - rect.left - 10) / Math.max(1, rect.width - 20)));
|
||||
const index = thumb ? Number(thumb.dataset.index) : Math.round(fraction * last);
|
||||
if (index !== state.active) { state.active = index; render(true); }
|
||||
});
|
||||
gradientAdd.addEventListener("click", () => { if (gradientAdd.getAttribute("aria-disabled") !== "true" && model.addGradientColor(state)) render(true); });
|
||||
gradientRemove.addEventListener("click", () => { if (model.removeGradientColor(state)) render(true); });
|
||||
originalPreview.addEventListener("click", () => { if (model.applySelection(state, originalSelection)) render(true); });
|
||||
paletteToggle.addEventListener("click", () => {
|
||||
setSidebarOpen(!palettePanel.classList.contains("open"));
|
||||
if (initialized) emit("update_ui", uiState());
|
||||
});
|
||||
paletteCombo.addEventListener("change", () => {
|
||||
populatePalette();
|
||||
if (initialized) emit("update_ui", uiState());
|
||||
});
|
||||
paletteSearchToggle.addEventListener("click", () => setPaletteSearch(paletteSearchToggle.getAttribute("aria-pressed") !== "true"));
|
||||
paletteFilter.addEventListener("input", populatePalette);
|
||||
// Pointer capture ends by itself on pointerup/pointercancel, so it doubles as the drag state.
|
||||
canvas.addEventListener("pointerdown", (event) => { canvas.setPointerCapture(event.pointerId); sampleSpectrum(event); });
|
||||
canvas.addEventListener("pointermove", (event) => { if (canvas.hasPointerCapture(event.pointerId)) sampleSpectrum(event); });
|
||||
// The spectrum takes the panel's free height, which also changes with the notice, alpha and
|
||||
// gradient controls, so redraw on any size change rather than on window resize only.
|
||||
new ResizeObserver(() => { drawSpectrum(); updateIndicator(displayHsl(model.activeColor(state))); }).observe(canvas.parentElement);
|
||||
// Bubble phase on document runs after each editor's own handler has set its validity.
|
||||
document.addEventListener("input", updateConfirm);
|
||||
// Hex keeps its own message from its input handler; number fields get a translated one.
|
||||
function validateEditors() {
|
||||
const inputs = editors();
|
||||
for (const input of inputs.filter((input) => input !== hexInput)) {
|
||||
input.setCustomValidity("");
|
||||
const {valid, rangeUnderflow, rangeOverflow} = input.validity;
|
||||
if (!valid) input.setCustomValidity(T(rangeUnderflow || rangeOverflow ? "Value is out of range." : "Invalid input"));
|
||||
}
|
||||
return inputs.every((input) => input.reportValidity());
|
||||
}
|
||||
function confirm() {
|
||||
if (initialized && validateEditors()) emit("confirm", {selection: model.exportSelection(state)});
|
||||
}
|
||||
preview.addEventListener("click", () => {
|
||||
if (!initialized || !favoritesWritable || !validateEditors()) return;
|
||||
const selection = model.exportSelection(state);
|
||||
const key = JSON.stringify(selection);
|
||||
favorites = [selection, ...favorites.filter((favorite) => JSON.stringify(favorite) !== key)].slice(0, FAVORITE_SLOTS);
|
||||
drawFavorites(); emit("update_favorites", {favorites});
|
||||
});
|
||||
confirmBtn.addEventListener("click", confirm);
|
||||
byId("cancelBtn").addEventListener("click", () => emit("cancel"));
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Escape in the filter box closes the search first; a second Escape cancels the dialog.
|
||||
if (event.key === "Escape" && event.target === paletteFilter) { event.preventDefault(); setPaletteSearch(false); paletteSearchToggle.focus(); }
|
||||
else if (event.key === "Escape") { event.preventDefault(); emit("cancel"); }
|
||||
else if (event.key === "Enter" && event.target instanceof HTMLInputElement && ["text", "number"].includes(event.target.type)) {
|
||||
event.preventDefault(); confirm();
|
||||
}
|
||||
});
|
||||
|
||||
window.ColorPickerDialog = {
|
||||
init, handleMessage, focusInput, exportSelection: () => model.exportSelection(state),
|
||||
setFavorites: (values) => { favorites = model.normalizeFavorites(values); drawFavorites(); }
|
||||
};
|
||||
// The host passes the saved sidebar state before the page loads (the same values come again in
|
||||
// init), so the first paint already matches the window size the host opened with.
|
||||
const earlyUi = window.ORCA_COLOR_PICKER_UI;
|
||||
if (earlyUi && typeof earlyUi === "object") {
|
||||
if (typeof earlyUi.palette === "string" && [...paletteCombo.options].some((option) => option.value === earlyUi.palette))
|
||||
paletteCombo.value = earlyUi.palette;
|
||||
if (typeof earlyUi.sidebar_open === "boolean") setSidebarOpen(earlyUi.sidebar_open);
|
||||
}
|
||||
setSwatch(originalPreview, originalSelection);
|
||||
drawSpectrum(); populatePalette(); drawFavorites(); render(true); emit("ready");
|
||||
})();
|
||||
@@ -1,103 +0,0 @@
|
||||
"use strict";
|
||||
const {test} = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const model = require("./colorpicker-model.js");
|
||||
|
||||
test("complete RGB/RGBA hex accepts mixed case and rejects partial or unbounded input", () => {
|
||||
assert.deepEqual(model.parseHex("#Ab12cD80"), [171,18,205,128]);
|
||||
for (const hex of ["", "123456", "#123", "#1234567", "#123456789", "#12345Z", "#123456GG", " #123456", null]) assert.equal(model.parseHex(hex), null);
|
||||
for (let byte = 0; byte <= 255; ++byte) {
|
||||
const hex = model.toHex([byte, byte, byte, byte]);
|
||||
assert.deepEqual(model.parseHex(hex), [byte,byte,byte,byte]);
|
||||
}
|
||||
});
|
||||
|
||||
test("HSL conversion preserves primary channels and hue wraps at 360", () => {
|
||||
for (const rgb of [[255,0,0], [0,255,0], [0,0,255], [128,128,128], [37,129,240]]) assert.deepEqual(model.hslToRgb(...model.rgbToHsl(...rgb)), rgb);
|
||||
assert.deepEqual(model.hslToRgb(360,100,50), [255,0,0]);
|
||||
assert.deepEqual(model.hslToRgb(240,100,50), [0,0,255]);
|
||||
});
|
||||
|
||||
test("capabilities default off and reject gradient through every model entry", () => {
|
||||
const gradient = {type: "gradient", colors: ["#FF000080", "#0000FF20"]};
|
||||
const state = model.createState();
|
||||
assert.deepEqual(state.options, {allow_gradient: false, allow_alpha: false});
|
||||
assert.equal(model.createState({}, gradient), null);
|
||||
assert.equal(model.applySelection(state, gradient), false);
|
||||
assert.equal(model.setMode(state, "gradient"), false);
|
||||
assert.equal(model.setAlphaPercent(state, 20), false);
|
||||
assert.equal(model.createState({allow_alpha: true}, gradient), null);
|
||||
const opaque = model.createState({allow_gradient: true}, gradient);
|
||||
assert.deepEqual(model.exportSelection(opaque), {type: "gradient", colors: ["#FF0000FF", "#0000FFFF"]});
|
||||
});
|
||||
|
||||
test("gradient endpoint edits keep order and preserve unedited byte alpha", () => {
|
||||
const initial = {type: "gradient", colors: ["#FF000001", "#0000FF80"]};
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true}, initial);
|
||||
state.active = 0; model.setRgb(state, [0,255,0]);
|
||||
state.active = 1; model.setHex(state, "#112233");
|
||||
assert.deepEqual(model.exportSelection(state), {type: "gradient", colors: ["#00FF0001", "#11223380"]});
|
||||
model.setAlphaPercent(state, 0);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#00FF0001", "#11223300"]);
|
||||
model.setAlphaPercent(state, 100);
|
||||
assert.equal(model.exportSelection(state).colors[1], "#112233FF");
|
||||
assert.deepEqual(initial.colors, ["#FF000001", "#0000FF80"]);
|
||||
});
|
||||
|
||||
test("favorites retain alpha and gradient even when the selection disables them", () => {
|
||||
const favorite = {type: "solid", colors: ["#ABCDEF80"]};
|
||||
const gradient = {type: "gradient", colors: ["#FFFFFF00", "#00000010"]};
|
||||
const favorites = model.normalizeFavorites([favorite, gradient]);
|
||||
const state = model.createState();
|
||||
assert.equal(model.applySelection(state, favorites[0]), true);
|
||||
assert.deepEqual(model.exportSelection(state), {type: "solid", colors: ["#ABCDEFFF"]});
|
||||
assert.deepEqual(favorites, [favorite, gradient]);
|
||||
assert.equal(model.applySelection(state, favorites[1]), false);
|
||||
assert.equal(model.normalizeFavorites(Array(25).fill(favorite)).length, 24);
|
||||
});
|
||||
|
||||
test("invalid cardinality and channel edits cannot change a selection", () => {
|
||||
const state = model.createState({allow_alpha: true});
|
||||
const before = model.exportSelection(state);
|
||||
for (const value of [{type: "solid", colors: []}, {type: "solid", colors: ["#112233", "#445566"]}, {type: "gradient", colors: ["#112233"]}, {type: "other", colors: ["#112233"]}]) assert.equal(model.applySelection(state, value), false);
|
||||
assert.equal(model.setHex(state, "#GG2233"), false);
|
||||
for (const value of [-1, 101, NaN, Infinity]) assert.equal(model.setAlphaPercent(state, value), false);
|
||||
assert.equal(model.setRgb(state, [256,0,0]), false);
|
||||
assert.deepEqual(model.exportSelection(state), before);
|
||||
});
|
||||
|
||||
test("multi-color gradients stay at 2 colors unless the caller raises the limit", () => {
|
||||
const five = {type: "gradient", colors: ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"]};
|
||||
assert.equal(model.normalizeSelection(five, {allow_gradient: true}), null);
|
||||
assert.equal(model.createState({allow_gradient: true}, five), null);
|
||||
const state = model.createState({allow_gradient: true, max_gradient_colors: 7}, five);
|
||||
assert.equal(state.gradient.length, 5);
|
||||
assert.equal(model.normalizeSelection({...five, colors: Array(8).fill("#000000")}, {allow_gradient: true}, 7), null);
|
||||
assert.equal(model.createState({allow_gradient: true, max_gradient_colors: 99}).maxGradientColors, model.MAX_GRADIENT_COLORS);
|
||||
assert.equal(model.normalizeFavorites([five]).length, 1, "favorites keep every color");
|
||||
});
|
||||
|
||||
test("adding and removing gradient colors keeps order, selection and the 2..max limits", () => {
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true, max_gradient_colors: 4}, {type: "gradient", colors: ["#FF000080", "#0000FFFF"]});
|
||||
assert.equal(model.removeGradientColor(state), false, "never fewer than 2");
|
||||
assert.equal(model.addGradientColor(state), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080", "#0000FFFF"], "selected color turned 75° in hue, alpha kept");
|
||||
assert.equal(model.NEW_GRADIENT_HUE_SHIFT, 75);
|
||||
assert.equal(state.active, 1);
|
||||
state.active = 2; assert.equal(model.addGradientColor(state), true);
|
||||
assert.equal(state.active, 3); assert.equal(state.gradient.length, 4);
|
||||
assert.equal(model.addGradientColor(state), false, "limit reached");
|
||||
state.active = 1; assert.equal(model.removeGradientColor(state), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors.slice(0, 2), ["#FF000080", "#0000FFFF"]);
|
||||
assert.equal(state.active, 1);
|
||||
assert.equal(model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]}), true);
|
||||
state.active = 1; model.addGradientColor(state); model.addGradientColor(state);
|
||||
model.applySelection(state, {type: "gradient", colors: ["#111111", "#222222"]});
|
||||
assert.equal(state.active, 1, "a shorter gradient clamps the selected color");
|
||||
});
|
||||
|
||||
test("a new gradient starts at the solid color and its 75° hue neighbour", () => {
|
||||
const state = model.createState({allow_gradient: true, allow_alpha: true}, {type: "solid", colors: ["#FF000080"]});
|
||||
assert.equal(model.setMode(state, "gradient"), true);
|
||||
assert.deepEqual(model.exportSelection(state).colors, ["#FF000080", "#BFFF0080"]);
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-orca-page="color-picker">
|
||||
<head><title data-i18n="Color Picker">Color Picker</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../include/global.css">
|
||||
<link rel="stylesheet" href="../css/theme.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<p id="selectionNotice" hidden data-i18n="The current multi-color selection is kept until you choose a different color.">The current multi-color selection is kept until you choose a different color.</p>
|
||||
<fieldset id="pickerEditors" disabled>
|
||||
<div class="main-wrapper">
|
||||
|
||||
<div class="left-panel" id="palettePanel">
|
||||
<div class="palette thin-scroll" id="palette" tabindex="-1"></div>
|
||||
<p class="palette-empty hidden" id="paletteEmpty" data-i18n="No matching colors">No matching colors</p>
|
||||
<div class="palette-toolbar">
|
||||
<div class="palette-select ComboBox" id="paletteSelect">
|
||||
<div class="arrow-icon"></div>
|
||||
<select id="paletteCombo" tabindex="-1">
|
||||
<option value="basic" data-i18n="Basic Colors">Basic Colors</option>
|
||||
<option value="orca" data-i18n="Orca Palette">Orca Palette</option>
|
||||
<option value="ral" data-i18n="RAL Classic" >RAL Classic</option>
|
||||
<option value="gradient" hidden data-i18n="Gradients" >Gradients</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="search" id="paletteFilter" class="palette-filter hidden" autocomplete="off" spellcheck="false"
|
||||
placeholder="Filter colors" data-i18n-placeholder="Filter colors" aria-label="Filter colors" data-i18n-aria-label="Filter colors">
|
||||
<button type="button" id="paletteSearchToggle" class="palette-search-button" aria-pressed="false" title="Search colors" data-i18n-title="Search colors">
|
||||
<div class="icon-search icon16"></div>
|
||||
<div class="icon-close icon16"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="spectrum-row">
|
||||
<div class="saturation-slider">
|
||||
<input class="valueSlider" type="range" id="satSlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Saturation" data-i18n-aria-label="Saturation">
|
||||
</div>
|
||||
<div class="spectrum-container">
|
||||
<canvas id="spectrum"></canvas>
|
||||
<div id="colorIndicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-grid">
|
||||
<div class="editor-column">
|
||||
<div id="modeControls" class="hidden">
|
||||
<input class="switch-check" type="checkbox" id="modeSwitch">
|
||||
<label class="switch-slider" for="modeSwitch">
|
||||
<div class="switch-label" data-i18n="Solid">Solid</div>
|
||||
<div class="switch-label" data-i18n="Gradient">Gradient</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="hex-row">
|
||||
<div class="inline-field field-hex">
|
||||
<label for="hexInput">#</label>
|
||||
<input type="text" id="hexInput" required value="808080" maxlength="8" autocomplete="off" spellcheck="false" aria-label="Hex color" data-i18n-aria-label="Hex color">
|
||||
</div>
|
||||
<div class="inline-field value-field hidden" id="alphaControls" title="Opacity percent">
|
||||
<label for="oValue">
|
||||
<svg viewBox="0 0 12 12" width="12" height="12">
|
||||
<path d="M0,1v10c0,.55.45,1,1,1h10c.55,0,1-.45,1-1V1c0-.55-.45-1-1-1H1C.45,0,0,.45,0,1ZM6,6v5H1v-5h5V1h5v5h-5Z" style="fill:#999"/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="number" id="oValue" min="0" max="100" aria-label="Opacity percent" data-i18n-aria-label="Opacity percent">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Third row: gradient endpoints, replaced by the opacity slider while the opacity field is in use. -->
|
||||
<div id="thirdRow" class="third-row">
|
||||
<!-- Gradient colors: evenly spaced thumbs (built by colorpicker.js); a click selects the nearest. -->
|
||||
<div id="gradientCtrlBar" class="invisible" role="group" aria-label="Gradient" data-i18n-aria-label="Gradient">
|
||||
<div id="gradientStops" class="gradient-stops">
|
||||
<div class="gradient-track"></div>
|
||||
</div>
|
||||
<div class="gradient-step-buttons hidden">
|
||||
<button type="button" id="gradientRemove" class="gradient-step-button hidden" title="Remove color" data-i18n-title="Remove color">
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button type="button" id="gradientAdd" class="gradient-step-button hidden" title="Add color" data-i18n-title="Add color">
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"><rect x="0" y="4" width="10" height="2" fill="currentColor"/><rect x="4" y="0" width="2" height="10" fill="currentColor"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opacitySliderBar" hidden>
|
||||
<input class="valueSlider" type="range" id="opacitySlider" min="0" max="100" step="0.1" tabindex="-1" aria-label="Opacity" data-i18n-aria-label="Opacity">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-column">
|
||||
<div class="inline-field value-field" title="Hue">
|
||||
<label for="hValue">H</label>
|
||||
<input type="number" id="hValue" min="0" max="360" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Saturation">
|
||||
<label for="sValue">S</label>
|
||||
<input type="number" id="sValue" min="0" max="100" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Lightness">
|
||||
<label for="lValue">L</label>
|
||||
<input type="number" id="lValue" min="0" max="100" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-column">
|
||||
<div class="inline-field value-field" title="Red">
|
||||
<label for="rValue">R</label>
|
||||
<input type="number" id="rValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Green">
|
||||
<label for="gValue">G</label>
|
||||
<input type="number" id="gValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
<div class="inline-field value-field" title="Blue">
|
||||
<label for="bValue">B</label>
|
||||
<input type="number" id="bValue" min="0" max="255" step="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<button id="originalPreview" type="button" class="color-preview original-preview" title="Original color" data-i18n-title="Original color">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#ff6f00;"/>
|
||||
<g style="fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;">
|
||||
<polyline points="5.5 5.5 5.5 9.5 9.5 9.5" />
|
||||
<path d="M5.84,9.5c.82-2.33,3.05-4,5.66-4,3.31,0,6,2.69,6,6s-2.69,6-6,6c-2.95,0-5.41-2.13-5.91-4.94"/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="colorPreview" class="color-preview" title="Save current color" data-i18n-title="Save current color">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle cx="11.5" cy="11.5" r="11.5" style="fill:#009688"/>
|
||||
<path d="M11.85,5.11l1.43,4.39h4.61c.35,0,.5.45.22.66l-3.73,2.71,1.43,4.39c.11.34-.28.62-.56.41l-3.73-2.71-3.73,2.71c-.29.21-.67-.07-.56-.41l1.43-4.39-3.73-2.71c-.29-.21-.14-.66.22-.66h4.61s1.43-4.39,1.43-4.39c.11-.34.59-.34.7,0Z" style="fill:none; stroke:#fff; stroke-linecap:round; stroke-linejoin:round;"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="saved-grid" id="userColors"></div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button type="button" id="paletteToggle" class="icon-button" aria-expanded="false" aria-controls="palettePanel" title="Palettes" data-i18n-title="Palettes">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><rect x="1.5" y="2.5" width="13" height="11" rx="2" fill="none" stroke="currentColor"/><path d="M5.5 2.5v11" stroke="currentColor"/></svg>
|
||||
</button>
|
||||
<div class="stretch-spacer"></div>
|
||||
<button id="confirmBtn" class="ButtonTypeChoice ButtonStyleConfirm" data-i18n="OK">OK</button>
|
||||
<button id="cancelBtn" class="ButtonTypeChoice ButtonStyleRegular" data-i18n="Cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<script src="palettes.js"></script><script src="colorpicker-model.js"></script><script src="colorpicker.js"></script></body></html>
|
||||
@@ -1,459 +0,0 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
const cssColors = [
|
||||
{hex: "#FF0000", name: "Red"},
|
||||
{hex: "#DC143C", name: "Crimson"},
|
||||
{hex: "#FF4500", name: "Orange red"},
|
||||
{hex: "#FF6347", name: "Tomato"},
|
||||
{hex: "#FFA500", name: "Orange"},
|
||||
{hex: "#FF8C00", name: "Dark orange"},
|
||||
{hex: "#FFD700", name: "Gold"},
|
||||
{hex: "#DEB887", name: "Burly wood"},
|
||||
{hex: "#FFE4C4", name: "Bisque"},
|
||||
{hex: "#FFE4E1", name: "Misty rose"},
|
||||
{hex: "#FFFF00", name: "Yellow"},
|
||||
{hex: "#FFFFE0", name: "Light yellow"},
|
||||
{hex: "#FFFACD", name: "Lemon chiffon"},
|
||||
{hex: "#F5F5DC", name: "Beige"},
|
||||
{hex: "#FAFAD2", name: "Light golden rod yellow"},
|
||||
{hex: "#EEE8AA", name: "Pale golden rod"},
|
||||
{hex: "#F0E68C", name: "Khaki"},
|
||||
{hex: "#BDB76B", name: "Dark khaki"},
|
||||
{hex: "#FFDAB9", name: "Peach puff"},
|
||||
{hex: "#FFE4B5", name: "Moccasin"},
|
||||
{hex: "#FFEFD5", name: "Papaya whip"},
|
||||
{hex: "#FFEBCD", name: "Blanched almond"},
|
||||
{hex: "#FAEBD7", name: "Antique white"},
|
||||
{hex: "#FFDEAD", name: "Navajo white"},
|
||||
{hex: "#F5DEB3", name: "Wheat"},
|
||||
{hex: "#DAA520", name: "Golden rod"},
|
||||
{hex: "#D2691E", name: "Chocolate"},
|
||||
{hex: "#CD853F", name: "Peru"},
|
||||
{hex: "#B22222", name: "Fire brick"},
|
||||
{hex: "#A0522D", name: "Sienna"},
|
||||
{hex: "#8B4513", name: "Saddle brown"},
|
||||
{hex: "#A52A2A", name: "Brown"},
|
||||
{hex: "#BC8F8F", name: "Rosy brown"},
|
||||
{hex: "#FA8072", name: "Salmon"},
|
||||
{hex: "#FFA07A", name: "Light salmon"},
|
||||
{hex: "#E9967A", name: "Dark salmon"},
|
||||
{hex: "#F4A460", name: "Sandy brown"},
|
||||
{hex: "#FF7F50", name: "Coral"},
|
||||
{hex: "#F08080", name: "Light coral"},
|
||||
{hex: "#CD5C5C", name: "Indian red"},
|
||||
{hex: "#8B0000", name: "Dark red"},
|
||||
{hex: "#800000", name: "Maroon"},
|
||||
{hex: "#ADFF2F", name: "Green yellow"},
|
||||
{hex: "#9ACD32", name: "Yellow green"},
|
||||
{hex: "#7FFF00", name: "Chartreuse"},
|
||||
{hex: "#7CFC00", name: "Lawn green"},
|
||||
{hex: "#32CD32", name: "Lime green"},
|
||||
{hex: "#00FF7F", name: "Spring green"},
|
||||
{hex: "#00FA9A", name: "Medium spring green"},
|
||||
{hex: "#00FF00", name: "Lime"},
|
||||
{hex: "#008000", name: "Green"},
|
||||
{hex: "#228B22", name: "Forest green"},
|
||||
{hex: "#006400", name: "Dark green"},
|
||||
{hex: "#6B8E23", name: "Olive drab"},
|
||||
{hex: "#808000", name: "Olive"},
|
||||
{hex: "#556B2F", name: "Dark olive green"},
|
||||
{hex: "#98FB98", name: "Pale green"},
|
||||
{hex: "#90EE90", name: "Light green"},
|
||||
{hex: "#3CB371", name: "Medium sea green"},
|
||||
{hex: "#2E8B57", name: "Sea green"},
|
||||
{hex: "#8FBC8F", name: "Dark sea green"},
|
||||
{hex: "#5F9EA0", name: "Cadet blue"},
|
||||
{hex: "#66CDAA", name: "Medium aqua marine"},
|
||||
{hex: "#40E0D0", name: "Turquoise"},
|
||||
{hex: "#48D1CC", name: "Medium turquoise"},
|
||||
{hex: "#00CED1", name: "Dark turquoise"},
|
||||
{hex: "#AFEEEE", name: "Pale turquoise"},
|
||||
{hex: "#00FFFF", name: "Aqua"},
|
||||
{hex: "#00FFFF", name: "Cyan"},
|
||||
{hex: "#E0FFFF", name: "Light cyan"},
|
||||
{hex: "#F5FFFA", name: "Mint cream"},
|
||||
{hex: "#F0FFF0", name: "Honey dew"},
|
||||
{hex: "#7FFFD4", name: "Aquamarine"},
|
||||
{hex: "#20B2AA", name: "Light sea green"},
|
||||
{hex: "#008B8B", name: "Dark cyan"},
|
||||
{hex: "#008080", name: "Teal"},
|
||||
{hex: "#2F4F4F", name: "Dark slate gray"},
|
||||
{hex: "#778899", name: "Light slate gray"},
|
||||
{hex: "#708090", name: "Slate gray"},
|
||||
{hex: "#00BFFF", name: "Deep sky blue"},
|
||||
{hex: "#1E90FF", name: "Dodger Blue"},
|
||||
{hex: "#87CEEB", name: "Sky blue"},
|
||||
{hex: "#87CEFA", name: "Light sky blue"},
|
||||
{hex: "#ADD8E6", name: "Light blue"},
|
||||
{hex: "#B0E0E6", name: "Powder blue"},
|
||||
{hex: "#B0C4DE", name: "Light steel blue"},
|
||||
{hex: "#4682B4", name: "Steel blue"},
|
||||
{hex: "#6495ED", name: "Cornflower blue"},
|
||||
{hex: "#4169E1", name: "Royal blue"},
|
||||
{hex: "#0000FF", name: "Blue"},
|
||||
{hex: "#0000CD", name: "Medium blue"},
|
||||
{hex: "#191970", name: "Midnight blue"},
|
||||
{hex: "#00008B", name: "Dark blue"},
|
||||
{hex: "#000080", name: "Navy"},
|
||||
{hex: "#6A5ACD", name: "Slate blue"},
|
||||
{hex: "#483D8B", name: "Dark slate blue"},
|
||||
{hex: "#7B68EE", name: "Medium slate blue"},
|
||||
{hex: "#8A2BE2", name: "Blue violet"},
|
||||
{hex: "#4B0082", name: "Indigo"},
|
||||
{hex: "#9400D3", name: "Dark violet"},
|
||||
{hex: "#9932CC", name: "Dark orchid"},
|
||||
{hex: "#BA55D3", name: "Medium orchid"},
|
||||
{hex: "#DA70D6", name: "Orchid"},
|
||||
{hex: "#EE82EE", name: "Violet"},
|
||||
{hex: "#9370DB", name: "Medium purple"},
|
||||
{hex: "#800080", name: "Purple"},
|
||||
{hex: "#663399", name: "Rebecca purple"},
|
||||
{hex: "#C71585", name: "Medium violet red"},
|
||||
{hex: "#DB7093", name: "PaleV violet red"},
|
||||
{hex: "#FF1493", name: "Deep pink"},
|
||||
{hex: "#FF69B4", name: "Hot pink"},
|
||||
{hex: "#FFB6C1", name: "Light pink"},
|
||||
{hex: "#FFC0CB", name: "Pink"},
|
||||
{hex: "#DDA0DD", name: "Plum"},
|
||||
{hex: "#D8BFD8", name: "Thistle"},
|
||||
{hex: "#FF00FF", name: "Magenta"},
|
||||
{hex: "#FF00FF", name: "Fuchsia"},
|
||||
{hex: "#8B008B", name: "Dark magenta"},
|
||||
|
||||
{hex: "#FAF0E6", name: "Linen"},
|
||||
{hex: "#FDF5E6", name: "Old lace"},
|
||||
{hex: "#FFF5EE", name: "Sea shell"},
|
||||
{hex: "#FFFFF0", name: "Ivory"},
|
||||
{hex: "#FFFAF0", name: "Floral white"},
|
||||
{hex: "#FFF0F5", name: "Lavender blush"},
|
||||
{hex: "#F8F8FF", name: "Ghost white"},
|
||||
{hex: "#F0F8FF", name: "Alice blue"},
|
||||
{hex: "#E6E6FA", name: "Lavender"},
|
||||
{hex: "#F5F5F5", name: "White smoke"},
|
||||
{hex: "#DCDCDC", name: "Gainsboro"},
|
||||
];
|
||||
|
||||
const palettes = {
|
||||
basic: [
|
||||
{hex: "#FF0000", name: "Red"},
|
||||
{hex: "#FF8000", name: "Orange"},
|
||||
{hex: "#FFFF00", name: "Yellow"},
|
||||
{hex: "#80FF00", name: "Lime"},
|
||||
{hex: "#00FF00", name: "Green"},
|
||||
{hex: "#00FFFF", name: "Cyan"},
|
||||
{hex: "#0000FF", name: "Blue"},
|
||||
{hex: "#8000FF", name: "Purple"},
|
||||
{hex: "#FF00FF", name: "Magenta"},
|
||||
{hex: "#FF8080", name: "Pink"},
|
||||
{hex: "#804000", name: "Brown"},
|
||||
{hex: "#000000", name: "Black"},
|
||||
{hex: "#191919", name: "%90"},
|
||||
{hex: "#333333", name: "%80"},
|
||||
{hex: "#4c4c4c", name: "%70"},
|
||||
{hex: "#666666", name: "%60"},
|
||||
{hex: "#808080", name: "%50"},
|
||||
{hex: "#999999", name: "%40"},
|
||||
{hex: "#b3b3b3", name: "%30"},
|
||||
{hex: "#cccccc", name: "%20"},
|
||||
{hex: "#e6e6e6", name: "%10"},
|
||||
{hex: "#FFFFFF", name: "White"},
|
||||
...cssColors
|
||||
],
|
||||
orca: [
|
||||
{hex: "#ED1C24", name: "Coral fire"},
|
||||
{hex: "#C41E3A", name: "Sea anemone"},
|
||||
{hex: "#800000", name: "Deep abyss"},
|
||||
{hex: "#B22222", name: "Lobster red"},
|
||||
{hex: "#A0522D", name: "Sandy clay"},
|
||||
{hex: "#8B4513", name: "Driftwood"},
|
||||
{hex: "#D2691E", name: "Sea buckthorn"},
|
||||
{hex: "#F26722", name: "Sunset reef"},
|
||||
{hex: "#FF8C00", name: "Tiger coral"},
|
||||
{hex: "#FA8173", name: "Pink coral"},
|
||||
{hex: "#FFA07A", name: "Seashell glow"},
|
||||
{hex: "#F4E2C1", name: "Sand"},
|
||||
{hex: "#FFD700", name: "Golden kelp"},
|
||||
{hex: "#F7B763", name: "Sunlit dune"},
|
||||
{hex: "#FFAA33", name: "Amber nautilus"},
|
||||
{hex: "#FFEB31", name: "Sea lemon"},
|
||||
{hex: "#D4ED91", name: "Pale starfish"},
|
||||
{hex: "#00FF7F", name: "Spring tide"},
|
||||
{hex: "#32CD32", name: "Lime algae"},
|
||||
{hex: "#A4C41E", name: "Sea grass"},
|
||||
{hex: "#7CFC00", name: "Neon plankton"},
|
||||
{hex: "#228B22", name: "Kelp forest"},
|
||||
{hex: "#008000", name: "Emerald wave"},
|
||||
{hex: "#345B2F", name: "Whale shadow"},
|
||||
{hex: "#2E8B57", name: "Sea turtle"},
|
||||
{hex: "#00C1AE", name: "Lagoon"},
|
||||
{hex: "#20B2AA", name: "Tropical current"},
|
||||
{hex: "#115877", name: "Midnight trench"},
|
||||
{hex: "#008B8B", name: "Dark kelp"},
|
||||
{hex: "#00FFFF", name: "Crystal surf"},
|
||||
{hex: "#40E0D0", name: "Turquoise bay"},
|
||||
{hex: "#2EBDEF", name: "Sky over reef"},
|
||||
{hex: "#87CEEB", name: "Shallow sky"},
|
||||
{hex: "#1E90FF", name: "Electric bel"},
|
||||
{hex: "#4169E1", name: "Dolphin"},
|
||||
{hex: "#0000CD", name: "Ocean depths"},
|
||||
{hex: "#191970", name: "Whale"},
|
||||
{hex: "#4B0082", name: "Indigo abyss"},
|
||||
{hex: "#7841CE", name: "Sea urchin"},
|
||||
{hex: "#9370DB", name: "Lavender coral"},
|
||||
{hex: "#800080", name: "Violet tide"},
|
||||
{hex: "#8B008B", name: "Deep sea orchid"},
|
||||
{hex: "#FF00FF", name: "Neon jelly"},
|
||||
{hex: "#ED1E79", name: "Fuchsia coral"},
|
||||
{hex: "#FF69B4", name: "Pink jellyfish"},
|
||||
{hex: "#FFFFFF", name: "Foam"},
|
||||
{hex: "#D3D3D3", name: "Seashell white"},
|
||||
{hex: "#808080", name: "Pebble gray"},
|
||||
{hex: "#696969", name: "Storm cloud"},
|
||||
{hex: "#000000", name: "Ink black"}
|
||||
],
|
||||
ral: [
|
||||
{hex: "#716F52", name: "1000 Green beige"},
|
||||
{hex: "#9B8E79", name: "1001 Beige"},
|
||||
{hex: "#D4BB87", name: "1002 Sand yellow"},
|
||||
{hex: "#F4A460", name: "1003 Signal yellow"},
|
||||
{hex: "#EFBF04", name: "1004 Golden yellow"},
|
||||
{hex: "#D7A500", name: "1005 Honey yellow"},
|
||||
{hex: "#F2D185", name: "1006 Maize yellow"},
|
||||
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
|
||||
{hex: "#8B7D6B", name: "1011 Brown beige"},
|
||||
{hex: "#F5DB50", name: "1012 Lemon yellow"},
|
||||
{hex: "#F3E5AB", name: "1013 Oyster white"},
|
||||
{hex: "#F8E4BC", name: "1014 Ivory"},
|
||||
{hex: "#FAE6D3", name: "1015 Light ivory"},
|
||||
{hex: "#E4B922", name: "1018 Zinc yellow"},
|
||||
{hex: "#A19D94", name: "1019 Grey beige"},
|
||||
{hex: "#B9975B", name: "1020 Olive yellow"},
|
||||
{hex: "#B8860B", name: "1021 Rape yellow"},
|
||||
{hex: "#FF8C00", name: "1023 Traffic yellow"},
|
||||
{hex: "#CC7722", name: "1024 Ochre yellow"},
|
||||
{hex: "#FDBA37", name: "1026 Traffic orange yellow"},
|
||||
{hex: "#F3A55C", name: "1027 Curry"},
|
||||
{hex: "#E87C33", name: "1028 Melon yellow"},
|
||||
{hex: "#F2A641", name: "1032 Broom yellow"},
|
||||
{hex: "#E09F39", name: "1033 Dahlia yellow"},
|
||||
{hex: "#F0B76E", name: "1034 Pastel yellow"},
|
||||
{hex: "#D1BCA0", name: "1035 Pearl beige"},
|
||||
{hex: "#D4AF37", name: "1036 Pearl gold"},
|
||||
{hex: "#F7C64F", name: "1037 Sun yellow"},
|
||||
{hex: "#F28C38", name: "2000 Yellow orange"},
|
||||
{hex: "#E55B2B", name: "2001 Deep orange"},
|
||||
{hex: "#E34234", name: "2002 Vermilion"},
|
||||
{hex: "#F27B33", name: "2003 Pearl orange"},
|
||||
{hex: "#F37322", name: "2004 Pure orange"},
|
||||
{hex: "#F75C03", name: "2005 Luminous orange"},
|
||||
{hex: "#F46C1F", name: "2007 Luminous bright orange"},
|
||||
{hex: "#E14423", name: "2008 Bright red orange"},
|
||||
{hex: "#F28500", name: "2009 Traffic orange"},
|
||||
{hex: "#F46A0A", name: "2010 Signal orange"},
|
||||
{hex: "#E0490C", name: "2011 Deep orange"},
|
||||
{hex: "#F46F42", name: "2012 Salmon orange"},
|
||||
{hex: "#E6761C", name: "2013 Pearl orange"},
|
||||
{hex: "#E8A18B", name: "2015 Pearl pink"},
|
||||
{hex: "#B8612C", name: "2016 Copper orange"},
|
||||
{hex: "#8B4513", name: "2017 Signal brown"},
|
||||
{hex: "#C14A0F", name: "2018 Fir orange"},
|
||||
{hex: "#BC2E1F", name: "2020 Signal red"},
|
||||
{hex: "#D52713", name: "2021 Luminous red"},
|
||||
{hex: "#9F261E", name: "2022 Carmine red"},
|
||||
{hex: "#D53832", name: "2023 Pearl carmine red"},
|
||||
{hex: "#C2341C", name: "2024 Pure red"},
|
||||
{hex: "#B32D1C", name: "2025 Luminous deep red"},
|
||||
{hex: "#A62320", name: "2027 Currant red"},
|
||||
{hex: "#B9301E", name: "2028 Pearl ruby red"},
|
||||
{hex: "#A91D2E", name: "3000 Fire red"},
|
||||
{hex: "#A1262A", name: "3001 Signal red"},
|
||||
{hex: "#8F2626", name: "3002 Carmine red"},
|
||||
{hex: "#9B2428", name: "3003 Ruby red"},
|
||||
{hex: "#872241", name: "3004 Purple red"},
|
||||
{hex: "#722F37", name: "3005 Wine red"},
|
||||
{hex: "#3D1818", name: "3007 Black red"},
|
||||
{hex: "#8A3324", name: "3009 Oxide red"},
|
||||
{hex: "#A22C29", name: "3011 Brown red"},
|
||||
{hex: "#B22D20", name: "3012 Pine red"},
|
||||
{hex: "#D23741", name: "3013 Tomato red"},
|
||||
{hex: "#A8282E", name: "3014 Antique pink"},
|
||||
{hex: "#C8283C", name: "3015 Light pink"},
|
||||
{hex: "#D23F39", name: "3016 Coral red"},
|
||||
{hex: "#D1425E", name: "3017 Rose"},
|
||||
{hex: "#C4232F", name: "3018 Strawberry red"},
|
||||
{hex: "#C8102E", name: "3020 Traffic red"},
|
||||
{hex: "#E36A5A", name: "3022 Salmon pink"},
|
||||
{hex: "#E63E62", name: "3024 Luminous rose"},
|
||||
{hex: "#E02A27", name: "3026 Luminous bright red"},
|
||||
{hex: "#B8315F", name: "3027 Raspberry red"},
|
||||
{hex: "#C61C23", name: "3028 Pure red"},
|
||||
{hex: "#AD2932", name: "3031 Orient red"},
|
||||
{hex: "#D32F2F", name: "3032 Pearl ruby"},
|
||||
{hex: "#D13C6F", name: "3033 Pearl pink"},
|
||||
{hex: "#7B2B5F", name: "4001 Red purple"},
|
||||
{hex: "#912E8C", name: "4002 Red violet"},
|
||||
{hex: "#9C366F", name: "4003 Heather violet"},
|
||||
{hex: "#6A1F49", name: "4004 Bordeaux violet"},
|
||||
{hex: "#4B2663", name: "4005 Blue violet"},
|
||||
{hex: "#5C2D3C", name: "4006 Mahogany"},
|
||||
{hex: "#5E3559", name: "4007 Grey purple"},
|
||||
{hex: "#9C3568", name: "4008 Signal violet"},
|
||||
{hex: "#5E6E3A", name: "4009 Fir green"},
|
||||
{hex: "#5B5B8A", name: "4010 Sabine grey"},
|
||||
{hex: "#8F3A70", name: "4011 Pearl violet"},
|
||||
{hex: "#5E3252", name: "4012 Pearl blackberry"},
|
||||
{hex: "#1B4F72", name: "5001 Green blue"},
|
||||
{hex: "#18669E", name: "5002 Ultramarine blue"},
|
||||
{hex: "#0F4C8A", name: "5003 Sapphire blue"},
|
||||
{hex: "#0D324A", name: "5004 Black blue"},
|
||||
{hex: "#1B5583", name: "5005 Signal blue"},
|
||||
{hex: "#2C5F9D", name: "5007 Brilliant blue"},
|
||||
{hex: "#3B5AA9", name: "5008 Grey blue"},
|
||||
{hex: "#0077A3", name: "5009 Azure blue"},
|
||||
{hex: "#0A558C", name: "5010 Gentian blue"},
|
||||
{hex: "#2B547E", name: "5011 Steel blue"},
|
||||
{hex: "#0066A1", name: "5012 Light blue"},
|
||||
{hex: "#124780", name: "5013 Cobalt blue"},
|
||||
{hex: "#1F4E79", name: "5014 Pigeon blue"},
|
||||
{hex: "#0384A7", name: "5015 Sky blue"},
|
||||
{hex: "#006A9E", name: "5017 Traffic blue"},
|
||||
{hex: "#005F87", name: "5018 Turquoise blue"},
|
||||
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
|
||||
{hex: "#04749F", name: "5020 Ocean blue"},
|
||||
{hex: "#009AAA", name: "5021 Water blue"},
|
||||
{hex: "#1A3D5F", name: "5022 Night blue"},
|
||||
{hex: "#047AA6", name: "5023 Distant blue"},
|
||||
{hex: "#5B9BB8", name: "5024 Pastel blue"},
|
||||
{hex: "#3C6A98", name: "5025 Pearl gentian blue"},
|
||||
{hex: "#1C3F5F", name: "5026 Pearl midnight blue"},
|
||||
{hex: "#36673A", name: "6000 Patina green"},
|
||||
{hex: "#0E5C34", name: "6001 Emerald green"},
|
||||
{hex: "#437034", name: "6002 Leaf green"},
|
||||
{hex: "#3F3F2A", name: "6003 Olive green"},
|
||||
{hex: "#2B4E52", name: "6004 Blue green"},
|
||||
{hex: "#3B5F3F", name: "6005 Moss green"},
|
||||
{hex: "#4A563C", name: "6006 Grey olive"},
|
||||
{hex: "#2B4638", name: "6007 Pine green"},
|
||||
{hex: "#4A4139", name: "6008 Pine brown"},
|
||||
{hex: "#3B3A2E", name: "6009 Fir green"},
|
||||
{hex: "#3E7A5F", name: "6010 Grass green"},
|
||||
{hex: "#5E716A", name: "6011 Reseda green"},
|
||||
{hex: "#3A4E48", name: "6012 Brown green"},
|
||||
{hex: "#6B8E23", name: "6013 Reed green"},
|
||||
{hex: "#6B8E23", name: "6014 Yellow green"},
|
||||
{hex: "#1E3E2F", name: "6015 Black green"},
|
||||
{hex: "#1E4D4B", name: "6016 Turquoise green"},
|
||||
{hex: "#4C7C47", name: "6017 May green"},
|
||||
{hex: "#9CBF60", name: "6018 Yellow green"},
|
||||
{hex: "#2E5A3A", name: "6019 Pine green"},
|
||||
{hex: "#2E5A3A", name: "6020 Chrome oxide green"},
|
||||
{hex: "#9DC0A5", name: "6021 Pale green"},
|
||||
{hex: "#4A563C", name: "6022 Brown green"},
|
||||
{hex: "#009E60", name: "6024 Traffic green"},
|
||||
{hex: "#3F7044", name: "6025 Fern green"},
|
||||
{hex: "#4F7942", name: "6026 Opal green"},
|
||||
{hex: "#3B5F3F", name: "6027 Pine green"},
|
||||
{hex: "#3B5F3F", name: "6028 Pine green"},
|
||||
{hex: "#00A550", name: "6029 Mint green"},
|
||||
{hex: "#3E5A40", name: "6031 Bronze green"},
|
||||
{hex: "#3D7E5A", name: "6032 Pearl green"},
|
||||
{hex: "#007D6E", name: "6033 Sax turquoise"},
|
||||
{hex: "#3D7E5A", name: "6034 Pearl turquoise"},
|
||||
{hex: "#3D7E5A", name: "6035 Pearl green"},
|
||||
{hex: "#3D7E5A", name: "6036 Pearl jade"},
|
||||
{hex: "#00A550", name: "6037 Pure green"},
|
||||
{hex: "#00A550", name: "6038 Luminous green"},
|
||||
{hex: "#00A550", name: "6039 Mint green"},
|
||||
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
|
||||
{hex: "#A8A9AD", name: "7001 Silver grey"},
|
||||
{hex: "#6F7368", name: "7002 Olivestone"},
|
||||
{hex: "#8F9080", name: "7003 Moss grey"},
|
||||
{hex: "#8E9291", name: "7004 Signal grey"},
|
||||
{hex: "#8B8788", name: "7005 Mouse grey"},
|
||||
{hex: "#9A8373", name: "7006 Beige grey"},
|
||||
{hex: "#828C6A", name: "7008 Khaki grey"},
|
||||
{hex: "#6F7368", name: "7009 Green grey"},
|
||||
{hex: "#5F6563", name: "7010 Tarpaulin grey"},
|
||||
{hex: "#4A5656", name: "7011 Iron grey"},
|
||||
{hex: "#4D5656", name: "7012 Basalt grey"},
|
||||
{hex: "#635147", name: "7013 Brown grey"},
|
||||
{hex: "#6B6B83", name: "7015 Slate grey"},
|
||||
{hex: "#32373D", name: "7016 Anthracite grey"},
|
||||
{hex: "#8B8B83", name: "7021 Light grey"},
|
||||
{hex: "#716347", name: "7022 Umber grey"},
|
||||
{hex: "#76787A", name: "7023 Concrete grey"},
|
||||
{hex: "#5E6565", name: "7024 Graphite grey"},
|
||||
{hex: "#5E6565", name: "7026 Granite grey"},
|
||||
{hex: "#8F9494", name: "7030 Stone grey"},
|
||||
{hex: "#4A5656", name: "7031 Iron grey"},
|
||||
{hex: "#B2BFC0", name: "7032 Pebble grey"},
|
||||
{hex: "#9B9B9B", name: "7033 Cement grey"},
|
||||
{hex: "#8F9D9B", name: "7034 Yellow grey"},
|
||||
{hex: "#CBD0CC", name: "7035 Light grey"},
|
||||
{hex: "#6D7A80", name: "7036 Platinum grey"},
|
||||
{hex: "#8A9191", name: "7037 Dusty grey"},
|
||||
{hex: "#7E8181", name: "7038 Agate grey"},
|
||||
{hex: "#6E7D7E", name: "7039 Quartz grey"},
|
||||
{hex: "#8A9191", name: "7040 Window grey"},
|
||||
{hex: "#8A9191", name: "7042 Traffic grey A"},
|
||||
{hex: "#6E7D7E", name: "7043 Traffic grey B"},
|
||||
{hex: "#9B9B9B", name: "7044 Silk grey"},
|
||||
{hex: "#A1A7B0", name: "7045 Telegrey 1"},
|
||||
{hex: "#7E848C", name: "7046 Telegrey 2"},
|
||||
{hex: "#7E848C", name: "7047 Telegrey 4"},
|
||||
{hex: "#6D7A80", name: "7048 Pearl mouse grey"},
|
||||
{hex: "#786D5F", name: "8000 Green brown"},
|
||||
{hex: "#8B7355", name: "8001 Ochre brown"},
|
||||
{hex: "#8B6F47", name: "8003 Clay brown"},
|
||||
{hex: "#8B6F47", name: "8004 Copper brown"},
|
||||
{hex: "#5C4033", name: "8007 Chocolate brown"},
|
||||
{hex: "#6E5C4A", name: "8008 Olive brown"},
|
||||
{hex: "#8B4513", name: "8011 Nut brown"},
|
||||
{hex: "#8B4513", name: "8012 Red brown"},
|
||||
{hex: "#5C3317", name: "8014 Sepia brown"},
|
||||
{hex: "#854636", name: "8015 Chestnut brown"},
|
||||
{hex: "#8B4513", name: "8016 Mahogany brown"},
|
||||
{hex: "#5C3317", name: "8017 Chocolate brown"},
|
||||
{hex: "#786D5F", name: "8019 Grey brown"},
|
||||
{hex: "#854636", name: "8022 Chestnut brown"},
|
||||
{hex: "#8B6F47", name: "8023 Pearl copper"},
|
||||
{hex: "#8B7355", name: "8024 Beige brown"},
|
||||
{hex: "#8B6F47", name: "8025 Pearl mocha brown"},
|
||||
{hex: "#8B6F47", name: "8027 Clay brown"},
|
||||
{hex: "#8B6F47", name: "8028 Earth brown"},
|
||||
{hex: "#8B6F47", name: "8029 Pearl brown"},
|
||||
{hex: "#FAF0E6", name: "9001 Cream"},
|
||||
{hex: "#EDECE6", name: "9002 Grey white"},
|
||||
{hex: "#FFFFFF", name: "9003 Signal white"},
|
||||
{hex: "#000000", name: "9004 Signal black"},
|
||||
{hex: "#1D1D1D", name: "9005 Jet black"},
|
||||
{hex: "#C0C0C0", name: "9006 White aluminium"},
|
||||
{hex: "#8C7853", name: "9007 Grey aluminium"},
|
||||
{hex: "#FFFFFF", name: "9010 Pure white"},
|
||||
{hex: "#1D1D1D", name: "9011 Graphite black"},
|
||||
{hex: "#FFFFFF", name: "9016 Traffic white"},
|
||||
{hex: "#000000", name: "9017 Traffic black"},
|
||||
{hex: "#FAF0E6", name: "9018 Papyrus white"},
|
||||
{hex: "#B2BFC0", name: "9022 Pearl light grey"}
|
||||
],
|
||||
gradient: [
|
||||
{grad: "#FFF47B-#CD9800", name: "Metal: Gold"},
|
||||
{grad: "#373737-#F5F4F4", name: "Metal: Silver"},
|
||||
{grad: "#C39292-#C73725", name: "Metal: Copper"},
|
||||
{grad: "#DED699-#C99620", name: "Metal: Brass"},
|
||||
{grad: "#6E4316-#B0742E", name: "Metal: Bronze"},
|
||||
// More than two colors: only listed when the page enables multi-color gradients.
|
||||
{grad: "#FF0000-#FFFF00-#00FF00-#00FFFF-#0000FF-#FF00FF-#FF0000", name: "Nature: Rainbow"},
|
||||
|
||||
{grad: "#D8FEE7-#FFF5BD", name: "Nature: Pearl"},
|
||||
{grad: "#1E5AA8-#3D8BD9-#7DB9F0-#C9E6FF", name: "Nature: Clear Sky"},
|
||||
{grad: "#2B1B5A-#7A2E7E-#D9466E-#F57C3A-#FFC857", name: "Nature: Sunset"},
|
||||
{grad: "#3B2416-#5A3A24-#45291A-#6B4630-#3B2416", name: "Wood: Dark"},
|
||||
{grad: "#8B5A2B-#A8733F-#946235-#B5844F-#8B5A2B", name: "Wood: Medium"},
|
||||
{grad: "#D9B98C-#E8CFA6-#D3AE7C-#EBD5B0-#D9B98C", name: "Wood: Light"},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
window.ColorPickerPalettes = palettes;
|
||||
})();
|
||||
@@ -1,534 +0,0 @@
|
||||
:root {
|
||||
--thumb-size : 20px;
|
||||
--field-height: 28px;
|
||||
--number-width: 52px;
|
||||
--preview-size: 52px;
|
||||
--checkerboard: conic-gradient(var(--checker-light) 25%, var(--checker-dark) 0 50%, var(--checker-light) 0 75%, var(--checker-dark) 0);
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
/* Report the full content width even before the native host expands its viewport.
|
||||
WebKit's fit-content can clip the sidebar and retain the expanded width on close. */
|
||||
width: max-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--orca-font, system-ui, sans-serif);
|
||||
background: var(--bg-color);
|
||||
color: var(--fg-color-text);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#selectionNotice {
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
margin-bottom: 8px;
|
||||
color: var(--fg-color-label);
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
/* ---------- Palette sidebar ---------- */
|
||||
.left-panel {
|
||||
flex: 0 0 160px;
|
||||
width: 160px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.left-panel.open {
|
||||
display: flex;
|
||||
padding-right: 0px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#paletteCombo {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Combo box or filter box, with the search / close button on the right. */
|
||||
.palette-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
height: 25px;
|
||||
margin-right:5px;
|
||||
}
|
||||
.palette-toolbar > .palette-select { flex: 1 }
|
||||
.palette-filter {
|
||||
flex: 1;
|
||||
height: 25px;
|
||||
padding: 0 6px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
color: var(--fg-color-text);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--main-color);
|
||||
outline: none;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.palette-filter::-webkit-search-cancel-button { -webkit-appearance: none }
|
||||
.palette-filter:focus-visible{
|
||||
background-color: var(--focus-bg-box);
|
||||
}
|
||||
|
||||
.palette-search-button {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: var(--button-bg-normal);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 0;
|
||||
}
|
||||
.palette-search-button:hover {
|
||||
background-color: var(--button-bg-hover);
|
||||
}
|
||||
|
||||
.palette-search-button .icon-search {
|
||||
--icon-url: var(--icon-search);
|
||||
}
|
||||
.palette-search-button .icon-close {
|
||||
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g style="fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round"><line x1="3.5" y1="2.5" x2="13.5" y2="12.5"/><line x1="3.5" y1="12.5" x2="13.5" y2="2.5"/></g></svg>');;
|
||||
}
|
||||
.palette-search-button .icon-close,
|
||||
.palette-search-button[aria-pressed="true"] .icon-search { display: none }
|
||||
.palette-search-button[aria-pressed="true"] .icon-close { display: block }
|
||||
.palette-empty {
|
||||
flex: 1;
|
||||
padding: 8px 2px;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color-label);
|
||||
}
|
||||
|
||||
.palette {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 1px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1px 2px 1px 0;
|
||||
border:1px solid transparent;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color-text);
|
||||
}
|
||||
|
||||
.color-item:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
flex: 0 0 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
margin-right: 5px;
|
||||
margin: 0 5px 0 1px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.color-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ---------- Picker ---------- */
|
||||
.right-panel {
|
||||
flex: 0 0 330px;
|
||||
/* WebKit uses width, rather than flex-basis, for the parent's intrinsic width. */
|
||||
width: 330px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spectrum-row {
|
||||
--spectrum-height: 150px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: var(--spectrum-height);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.spectrum-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#spectrum {
|
||||
position: absolute; /* Absolute so the canvas's pixel size never feeds back into the flex layout */
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#pickerEditors:disabled #spectrum { pointer-events: none }
|
||||
|
||||
#colorIndicator {
|
||||
position: absolute;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.saturation-slider {
|
||||
position: relative;
|
||||
flex: 0 0 var(--thumb-size);
|
||||
}
|
||||
|
||||
#satSlider {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
flex: none;
|
||||
width: var(--spectrum-height);
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
}
|
||||
|
||||
.valueSlider {
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.valueSlider::-webkit-slider-runnable-track {
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--track-gradient);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.valueSlider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
background: var(--thumb-color);
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
/* Left column: mode switch, hex + alpha, gradient endpoints. Right: H S L and R G B columns. */
|
||||
.editor-grid {
|
||||
--editor-grid-gap: 5px;
|
||||
display: flex;
|
||||
gap: var(--editor-grid-gap);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.editor-column:first-of-type {flex:1}
|
||||
.editor-column {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, var(--field-height));
|
||||
gap: var(--editor-grid-gap);
|
||||
}
|
||||
|
||||
.hex-row {
|
||||
display: flex;
|
||||
gap: var(--editor-grid-gap);
|
||||
}
|
||||
|
||||
.switch-check {
|
||||
display: none !important;
|
||||
}
|
||||
.switch-label {
|
||||
font-size: 13px;
|
||||
padding: 2px 10px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.switch-slider {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-radius: 20px;
|
||||
background: var(--bg-color-alt);
|
||||
border: 2px solid var(--bg-color-alt);
|
||||
height:26px;
|
||||
}
|
||||
.switch-slider:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
left: 0%;
|
||||
border-radius:20px;
|
||||
background: var(--main-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.switch-check:checked + .switch-slider:before {
|
||||
left: 50%;
|
||||
}
|
||||
.switch-check:checked + .switch-slider div:first-child,
|
||||
.switch-check + .switch-slider div:last-child{
|
||||
color: var(--fg-color-label);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.switch-check:checked + .switch-slider div:last-child,
|
||||
.switch-check + .switch-slider div:first-child{
|
||||
color: var(--button-fg-light);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* H S L / R G B / Hex / Alpha input boxes */
|
||||
.inline-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
.inline-field > label {
|
||||
width: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--fg-color-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.inline-field > input {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0 2px 0 5px;
|
||||
color: var(--fg-color-text);
|
||||
font-size: 15px;
|
||||
line-height: var(--field-height);
|
||||
appearance: textfield; /* No spinner buttons; Up/Down keys and the wheel still step by 1. */
|
||||
}
|
||||
.inline-field:focus-within {
|
||||
border-color: var(--main-color);
|
||||
background: var(--focus-bg-box);
|
||||
}
|
||||
.inline-field:has(input:invalid) {
|
||||
border-color: var(--control-invalid);
|
||||
}
|
||||
.inline-field > input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-field > input[type="number"]::-webkit-inner-spin-button,
|
||||
.inline-field > input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none }
|
||||
|
||||
.value-field { width: var(--number-width)}
|
||||
.field-hex { flex: 1 }
|
||||
#alphaControls {
|
||||
width: calc(var(--number-width) + 2px)
|
||||
}
|
||||
#alphaControls > input {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
/* Third row holds the gradient bar and the opacity slider in the same cell. The slider only
|
||||
shows while the opacity field or the slider itself is in use, and hides the gradient bar. */
|
||||
.third-row { position: relative }
|
||||
.third-row > * {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.third-row.opacity-open > #opacitySliderBar { display: flex }
|
||||
.third-row.opacity-open > #gradientCtrlBar { display: none }
|
||||
|
||||
#opacitySlider { width: 100%; }
|
||||
#opacitySlider::-webkit-slider-runnable-track {
|
||||
background: var(--track-gradient), var(--checkerboard); /* Transparent end shows the checkerboard through the color. */
|
||||
background-size: auto, 8px 8px;
|
||||
}
|
||||
|
||||
/* Gradient colors: thumbs are evenly spaced and fixed; a click anywhere selects the nearest. */
|
||||
#gradientCtrlBar {
|
||||
display: flex;
|
||||
}
|
||||
.gradient-stops {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
/* Kept 10px away from the gradient bar so a click near its right end can't hit − or +. */
|
||||
.gradient-step-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 10px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.gradient-step-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border-radius: 4px;
|
||||
background: var(--button-bg-normal);
|
||||
color: var(--icon-color);
|
||||
}
|
||||
.gradient-step-button svg { display: block; flex: none }
|
||||
.gradient-step-button:hover:not([aria-disabled="true"]) { background: var(--button-bg-hover) }
|
||||
.gradient-step-button[aria-disabled="true"] { opacity: 0.4; cursor: default }
|
||||
.gradient-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 50%;
|
||||
height: var(--thumb-size);
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||
background-size: auto, 8px 8px;
|
||||
}
|
||||
.gradient-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
border: 1px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 1px var(--color-marker-shadow);
|
||||
background: var(--thumb-color);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
/* Thumb i of n sits at 10px + (100% - 20px) * i / (n - 1), set inline as --stop. */
|
||||
.gradient-thumb { left: calc(10px + (100% - 20px) * var(--stop, 0)) }
|
||||
.gradient-thumb[aria-pressed="true"] {
|
||||
width: var(--thumb-size);
|
||||
height: var(--thumb-size);
|
||||
border: 2px solid var(--color-marker-outline);
|
||||
box-shadow: 0 0 0 2px var(--color-marker-shadow);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gradient-thumb:hover {
|
||||
box-shadow: 0 0 0 3px var(--color-marker-shadow);
|
||||
}
|
||||
|
||||
/* ---------- Original / current / favorites ---------- */
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: var(--preview-size);
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--preview-size);
|
||||
height: var(--preview-size);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.color-preview svg {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.color-preview:hover svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.saved-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 24px));
|
||||
grid-template-rows: repeat(2, minmax(0, 24px));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.saved-swatch {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saved-swatch.empty {
|
||||
background: var(--bg-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Each layer is separate so transparent RGBA shows the checkerboard beneath it. */
|
||||
.color-swatch, .saved-swatch, .color-preview {
|
||||
background-image: var(--swatch-color, linear-gradient(transparent, transparent)), var(--checkerboard);
|
||||
background-size: auto, 12px 12px;
|
||||
}
|
||||
.color-item { width: 100%; background: transparent; text-align: left; font-family: inherit; }
|
||||
|
||||
/* ---------- Actions ---------- */
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.dialog-actions button { margin: 0 }
|
||||
#confirmBtn[disabled] { opacity: 0.5 }
|
||||
.stretch-spacer { flex: 1 }
|
||||
|
||||
.icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
background: var(--button-bg-normal);
|
||||
color: var(--fg-color-text);
|
||||
}
|
||||
.icon-button:hover {
|
||||
background: var(--button-bg-hover);
|
||||
}
|
||||
.icon-button[aria-expanded="true"] {
|
||||
background: var(--main-color);
|
||||
color: var(--button-fg-light);
|
||||
}
|
||||
.icon-button[aria-expanded="true"]:hover {
|
||||
background: var(--main-color-hover);
|
||||
}
|
||||
|
||||
.hidden { display: none !important }
|
||||
.invisible { visibility: hidden }
|
||||
|
||||
.color-item:focus-visible, .saved-swatch:focus-visible,
|
||||
select:focus-visible, button:focus-visible { outline: 2px solid var(--main-color); outline-offset: -1px; }
|
||||
@@ -18,7 +18,7 @@
|
||||
<script src="../js/plugin-config-ui.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<script src="./plugin-sort.js"></script>
|
||||
<script src="../js/fuzzy-search.js"></script>
|
||||
<script src="../../js/fuzzy-search.js"></script>
|
||||
<script src="./plugin-search.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
|
||||
@@ -55,13 +55,6 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #efefef;
|
||||
--speed-tile-border: #d8d8d8;
|
||||
|
||||
/* Transparency and control markers are UI surfaces, never color data. */
|
||||
--checker-light: #eeeeee;
|
||||
--checker-dark: #aaaaaa;
|
||||
--color-marker-outline: #ffffff;
|
||||
--color-marker-shadow: rgba(0, 0, 0, 0.7);
|
||||
--control-invalid: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
:root[data-orca-theme="dark"] {
|
||||
@@ -100,8 +93,6 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #34343b;
|
||||
--speed-tile-border: #50505a;
|
||||
--checker-light: #68686f;
|
||||
--checker-dark: #414148;
|
||||
}
|
||||
|
||||
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
||||
|
||||
@@ -10,11 +10,6 @@ function OnInit()
|
||||
// Orca: fallback timeout in case the C++ -> JS signal fails (e.g., WebKit issues).
|
||||
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
|
||||
setTimeout("JumpToTarget()",180*1000);
|
||||
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="userguide_loading_page_ready";
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
}
|
||||
|
||||
function HandleStudio( pVal )
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -4607,16 +4607,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
maps.erase(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament - never synthesize a
|
||||
// preset for it. Kept here only to keep index alignment with ams_infos; the sync
|
||||
// drops it so no filament slot is created or retained for an empty tray.
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else if (use_map) {
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
@@ -4625,6 +4615,12 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
} else if (is_placeholder) {
|
||||
// Orca: push placeholders to keep index alignment with ams_infos
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -4988,10 +4984,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
result_multi_colors.push_back(
|
||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||
: std::vector<std::string>{ams_filament_colors[i]});
|
||||
} else if (i < ams_infos.size() && ams_infos[i].is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament, so drop it instead of
|
||||
// keeping or filling a slot for it. The device AMS panel still shows the empty tray.
|
||||
continue;
|
||||
} else if (i < exist_presets.size()) {
|
||||
// Empty tray or beyond tray count: keep existing filament
|
||||
result_colors.push_back(exist_colors[i]);
|
||||
@@ -5016,8 +5008,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
filament_color_type->values = result_color_types;
|
||||
this->filament_presets = result_presets;
|
||||
ams_multi_color_filment = result_multi_colors;
|
||||
filament_map->values.resize(result_presets.size(), 1);
|
||||
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
filament_map->values.resize(total, 1);
|
||||
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
} else {
|
||||
// BBL: existing wholesale replace
|
||||
filament_color->values = ams_filament_colors;
|
||||
|
||||
@@ -129,8 +129,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Shortcuts.hpp
|
||||
GUI/SpeedDialDialog.cpp
|
||||
GUI/SpeedDialDialog.hpp
|
||||
GUI/ColorPickerDialog.cpp
|
||||
GUI/ColorPickerDialog.hpp
|
||||
GUI/ActionRegistry.cpp
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/NativeCommands.cpp
|
||||
@@ -689,6 +687,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/host/PluginHostUi.hpp
|
||||
plugin/host/PluginPages.cpp
|
||||
plugin/host/PluginPages.hpp
|
||||
plugin/host/PluginResourceTextures.cpp
|
||||
plugin/host/PluginResourceTextures.hpp
|
||||
plugin/CloudPluginService.cpp
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
@@ -712,6 +712,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
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
#include "AMSMaterialsSetting.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <cstdint>
|
||||
#include "ExtrusionCalibration.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -41,9 +36,11 @@
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
#include "slic3r/GUI/Widgets/StaticBox.hpp"
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
@@ -1966,6 +1963,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));
|
||||
@@ -1989,34 +1991,46 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
wxGetApp().UpdateDarkUIWin(this);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
|
||||
{
|
||||
const bool was_shown = IsShown();
|
||||
std::optional<wxColour> picker_color;
|
||||
{
|
||||
const ColorRGBA initial(m_def_col.Red(), m_def_col.Green(), m_def_col.Blue(), static_cast<unsigned char>(255));
|
||||
// Snapshot the custom control's anchor before releasing the transient popup grab.
|
||||
ColorPickerDialog dialog(GetParent(), initial, {}, false, m_custom_cp);
|
||||
Dismiss();
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
picker_color = wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f)));
|
||||
}
|
||||
} // Destroy the modal before restoring the first-level popup and its focus/grab.
|
||||
if (!picker_color) {
|
||||
if (was_shown) Popup();
|
||||
return;
|
||||
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]));
|
||||
}
|
||||
auto clr_dialog = new wxColourDialog(nullptr, m_clrData);
|
||||
wxColour picker_color;
|
||||
|
||||
set_def_colour(*picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
const std::uint32_t packed = (std::uint32_t(picker_color->Red()) << 24) |
|
||||
(std::uint32_t(picker_color->Green()) << 16) |
|
||||
(std::uint32_t(picker_color->Blue()) << 8) | 255u;
|
||||
evt.SetInt(static_cast<int>(packed));
|
||||
wxPostEvent(GetParent(), evt);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
|
||||
@@ -85,6 +85,7 @@ public:
|
||||
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;
|
||||
wxColour m_def_col;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <variant>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
|
||||
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
|
||||
// and never offers more, so the picker, favorites and project colors all share one limit.
|
||||
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
|
||||
|
||||
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
|
||||
using ColorGradient = std::vector<ColorRGBA>;
|
||||
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
|
||||
|
||||
struct ColorPickerOptions {
|
||||
bool allow_gradient = false;
|
||||
bool allow_alpha = false;
|
||||
};
|
||||
|
||||
// Project storage preserves direction; FilamentColor's set is only for the
|
||||
// official palette and cannot represent ordered custom gradients.
|
||||
struct FilamentColorPickerValue {
|
||||
std::vector<std::string> colors;
|
||||
bool gradient = false;
|
||||
std::string primary;
|
||||
};
|
||||
|
||||
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
|
||||
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
|
||||
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
|
||||
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
|
||||
|
||||
class ColorPickerDialog : public WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
|
||||
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
|
||||
~ColorPickerDialog() override;
|
||||
|
||||
const std::optional<ColorSelection>& selection() const { return m_selection; }
|
||||
|
||||
private:
|
||||
void add_user_scripts() override;
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
void send_initial_state();
|
||||
void finish(int return_code);
|
||||
void position_panel();
|
||||
void move_to_anchor();
|
||||
void resize_to_content(int height);
|
||||
void focus_webview();
|
||||
void repaint_webview();
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
void on_sys_color_changed() override;
|
||||
|
||||
ColorSelection m_initial;
|
||||
ColorPickerOptions m_options;
|
||||
std::optional<ColorSelection> m_selection;
|
||||
std::vector<ColorSelection> m_favorites;
|
||||
std::string m_page_id;
|
||||
bool m_preserve_multi_color = false;
|
||||
bool m_init_sent = false;
|
||||
bool m_page_ready = false;
|
||||
bool m_closing = false;
|
||||
bool m_favorites_writable = false;
|
||||
bool m_sidebar_open = false;
|
||||
std::string m_palette;
|
||||
bool m_initial_positioned = false;
|
||||
wxRect m_anchor_rect;
|
||||
wxRect m_work_area;
|
||||
int m_content_height = 360;
|
||||
int m_content_width = 330;
|
||||
bool m_positioning = false;
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -163,8 +163,8 @@ public:
|
||||
~devPrinterUtil() = delete;
|
||||
|
||||
public:
|
||||
static bool IsVirtualSlot(int ams_id);
|
||||
static bool IsVirtualSlot(const std::string& ams_id);
|
||||
static bool IsVirtualSlot(int ams_id) { return (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID);}
|
||||
static bool IsVirtualSlot(const std::string& ams_id) { return (ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR); }
|
||||
};
|
||||
|
||||
namespace GUI
|
||||
@@ -198,6 +198,3 @@ template<> struct std::hash<NozzleDef>
|
||||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
||||
};
|
||||
};
|
||||
|
||||
// key(extruder_id) -> { key1(nozzle type info), val1( number of the nozzle type)}
|
||||
using ExtruderNozzleInfos = std::unordered_map<int, std::unordered_map<NozzleDef, int>>;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
@@ -34,36 +33,6 @@
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(int ams_id)
|
||||
{
|
||||
// When the app or preset bundle is unavailable we cannot know the vendor's
|
||||
// extruder count, so fall back deterministically to the fixed Bambu pair
|
||||
// {255, 254} rather than guessing from an unknown generic layout.
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor())
|
||||
return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID;
|
||||
|
||||
const int extruder_count = GUI::wxGetApp().preset_bundle->get_printer_extruder_count();
|
||||
return ams_id >= VIRTUAL_TRAY_MAIN_ID - extruder_count + 1 && ams_id <= VIRTUAL_TRAY_MAIN_ID;
|
||||
}
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id)
|
||||
{
|
||||
// Strict: only a fully numeric string is a valid slot id. std::stoi alone
|
||||
// would accept trailing garbage (e.g. "255abc" -> 255).
|
||||
if (ams_id.empty())
|
||||
return false;
|
||||
for (char c : ams_id) {
|
||||
if (c < '0' || c > '9')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return IsVirtualSlot(std::stoi(ams_id));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; }
|
||||
|
||||
wxColour DevAmsTray::decode_color(const std::string &color)
|
||||
@@ -231,22 +200,18 @@ wxString DevAms::GetDisplayName() const
|
||||
|
||||
int DevAms::GetSlotCount() const
|
||||
{
|
||||
// N3S is a 1-slot Bambu contract, so report 1 regardless of whether the
|
||||
// preset bundle is momentarily unavailable (m_trays.size() can be 0).
|
||||
if (GetAmsType() == N3S)
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
else if (ams_type == N3S)
|
||||
{
|
||||
return 1;
|
||||
|
||||
if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int>(m_trays.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
|
||||
@@ -337,21 +302,13 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const
|
||||
{
|
||||
return it->second->GetExtruderId();
|
||||
}
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (!is_bbl_vendor && GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
if (tray.id == ams_id)
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR)
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
|
||||
{
|
||||
return MAIN_EXTRUDER_ID;
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
|
||||
assert(false && __FUNCTION__);
|
||||
@@ -368,22 +325,9 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id)
|
||||
std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
{
|
||||
std::map<int, DevAmsSlotId> tray_id_map;
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
} else if (GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
try {
|
||||
const int tray_id = std::stoi(tray.id);
|
||||
tray_id_map[tray_id] = DevAmsSlotId{tray_id, 0};
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid virtual tray id: " << tray.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
|
||||
int generic_tray_index = 0;
|
||||
for (auto& [ams_id, ams_item] : GetAmsList()) {
|
||||
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
|
||||
if (ams_item && slot_item) {
|
||||
@@ -391,11 +335,9 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
int ams_id_int = stoi(ams_id);
|
||||
int slot_id_int = stoi(slot_id);
|
||||
int tray_index = -1;
|
||||
if (!is_bbl_vendor) {
|
||||
tray_index = generic_tray_index + slot_id_int;
|
||||
} else if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
tray_index = ams_id_int;
|
||||
} else if (ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
} else if(ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
tray_index = 24 + slot_id_int;
|
||||
} else {
|
||||
tray_index = (ams_id_int * 4 + slot_id_int);
|
||||
@@ -406,8 +348,6 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor)
|
||||
generic_tray_index += static_cast<int>(ams_item->GetTrays().size());
|
||||
}
|
||||
|
||||
return tray_id_map;
|
||||
@@ -516,13 +456,6 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
ams_id_set.insert(it->first);
|
||||
}
|
||||
|
||||
// Bambu trays index their exist bit as ams_id*4+tray_id; a generic (non-Bambu) unit
|
||||
// reports its own lane count, so its trays carry a cumulative lane index instead.
|
||||
// Same index DevFilaSystem::GetTrayIndexMap() builds and the agent sets bits with.
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
int generic_tray_base = 0;
|
||||
|
||||
for (auto it = j_ams.begin(); it != j_ams.end(); it++)
|
||||
{
|
||||
if (!it->contains("id")) continue;
|
||||
@@ -862,18 +795,7 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
|
||||
if (type_id < 4)
|
||||
{
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A generic unit is not four slots wide, so its trays carry a
|
||||
// cumulative lane index across units instead of ams_id*4+tray_id.
|
||||
const int tray_bit = generic_tray_base + tray_id_int;
|
||||
curr_tray->is_exists = tray_bit >= 0 && tray_bit < 32 &&
|
||||
(obj->tray_exist_bits & (1UL << tray_bit)) != 0;
|
||||
}
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
}
|
||||
else if (type_id == DevAms::AMS_LITE_MIXED)
|
||||
{
|
||||
@@ -929,9 +851,6 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor && it->contains("tray") && (*it)["tray"].is_array()) {
|
||||
generic_tray_base += static_cast<int>((*it)["tray"].size());
|
||||
}
|
||||
}
|
||||
// remove not in amsList
|
||||
for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++)
|
||||
@@ -994,8 +913,6 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
ams->m_exist = true;
|
||||
ams->m_binded_extruder_set = {ext_id};
|
||||
ams->m_binded_switcher_pos.reset();
|
||||
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
||||
@@ -1078,22 +995,16 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// Keep one virtual slot per extruder. The legacy two-extruder mapping uses
|
||||
// 255 for extruder 0 and 254 for extruder 1; continue that sequence for
|
||||
// additional extruders (253, 252, ...).
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
std::map<int, DevAmsTray> external_slots;
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
continue;
|
||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
||||
if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext;
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
@@ -1102,11 +1013,8 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||
tray.remain = e.value("remain_percent", -1);
|
||||
external_slots.insert_or_assign(ext, std::move(tray));
|
||||
obj->vt_slot.push_back(tray);
|
||||
}
|
||||
|
||||
for (auto& entry : external_slots)
|
||||
obj->vt_slot.push_back(std::move(entry.second));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ public:
|
||||
DevAmsTray* GetAmsTray(const std::string& ams_id, const std::string& tray_id) const;
|
||||
void CollectAmsColors(std::vector<wxColour>& ams_colors) const;
|
||||
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes virtual external-spool trays,
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes the two virtual (external-spool) trays,
|
||||
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
|
||||
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
||||
|
||||
@@ -433,4 +433,4 @@ struct DevFilamentDryingPreset
|
||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||
};
|
||||
|
||||
}// namespace Slic3r
|
||||
}// namespace Slic3r
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include <string>
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
@@ -28,7 +27,6 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -178,52 +176,34 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
||||
int DevNozzle::GetLogicExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
assert(0);
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
// For some reason, BBL's logical extruder ID is inverted:
|
||||
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_EXTRUDER_ID)
|
||||
// Likely because logical id reads from left to right (left = 0, right = 1)
|
||||
// For generic N extruders, this inversion does not apply.
|
||||
if (IsOnRack()) return INVALID_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
int DevNozzle::GetExtruderId() const
|
||||
{
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
return m_nozzle_id;
|
||||
return MAIN_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
bool DevNozzle::AtLeftExtruder() const
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
@@ -807,12 +806,8 @@ bool MachineObject::is_extrusion_cali_finished()
|
||||
DevAmsTray *MachineObject::get_curr_tray()
|
||||
{
|
||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
|
||||
for (auto& tray : vt_slot) {
|
||||
if (tray.id == cur_ams_id)
|
||||
return &tray;
|
||||
}
|
||||
return nullptr;
|
||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
||||
return &vt_slot[0];
|
||||
}
|
||||
|
||||
DevAms* curr_ams = get_curr_Ams();
|
||||
@@ -1278,24 +1273,19 @@ int MachineObject::get_bed_temperature_limit()
|
||||
|
||||
bool MachineObject::is_filament_installed()
|
||||
{
|
||||
// if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// // right//or single
|
||||
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// /*left*/
|
||||
// if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
// auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
for (auto& ext : m_extder_system->m_extders) {
|
||||
if (ext.m_ext_has_filament)
|
||||
if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// right//or single
|
||||
auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*left*/
|
||||
if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2637,10 +2627,13 @@ void MachineObject::reset()
|
||||
json empty_j;
|
||||
print_json.diff2all_base_reset(empty_j);
|
||||
|
||||
for (auto& tray : vt_slot)
|
||||
tray.reset();
|
||||
if (vt_slot.size() > 1)
|
||||
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
|
||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
||||
vt_slot[i].reset();
|
||||
|
||||
if (i == 1) {
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -4121,28 +4114,25 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
|
||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
||||
try {
|
||||
const auto tray_json = it.value().get<json>();
|
||||
if (!tray_json.is_object() || !tray_json.contains("id") || !tray_json["id"].is_string())
|
||||
continue;
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
|
||||
const auto tray_id_str = tray_json["id"].get<std::string>();
|
||||
int tray_id = 0;
|
||||
const auto result = std::from_chars(tray_id_str.data(), tray_id_str.data() + tray_id_str.size(), tray_id);
|
||||
if (result.ec != std::errc{} || result.ptr != tray_id_str.data() + tray_id_str.size()
|
||||
|| tray_id < 0 || tray_id > VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
auto vslot = parse_vt_tray(tray_json);
|
||||
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
|
||||
while (vt_slot.size() <= index) {
|
||||
const int placeholder_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(vt_slot.size());
|
||||
vt_slot.emplace_back(std::to_string(placeholder_id));
|
||||
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 0);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[0] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
vt_slot[index] = vslot;
|
||||
}
|
||||
catch (...) {
|
||||
continue;
|
||||
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 1);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[1] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5280,17 +5270,16 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
|
||||
bool MachineObject::contains_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
{
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
|
||||
if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
} else {
|
||||
for (const auto& tray : vt_slot) {
|
||||
if (tray.id == ams_id) return true;
|
||||
if (tray.id == ams_id) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return !is_bbl_vendor && m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
@@ -5300,19 +5289,13 @@ DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string
|
||||
return DevAmsTray(tray_id);
|
||||
}
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) return *tray;
|
||||
if (tray) { return *tray;};
|
||||
}
|
||||
else if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
else {
|
||||
for (const auto &tray : vt_slot) {
|
||||
if (tray.id == ams_id) return tray;
|
||||
}
|
||||
|
||||
if (!is_bbl_vendor) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) return *tray;
|
||||
if (tray.id == ams_id) { return tray; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+166
-75
@@ -2,7 +2,6 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Field.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Plater.hpp"
|
||||
@@ -46,11 +45,7 @@
|
||||
#include <wx/arrstr.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/brush.h>
|
||||
#include <wx/pen.h>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/dc.h>
|
||||
@@ -2623,41 +2618,92 @@ void PluginConfigField::msw_rescale()
|
||||
|
||||
void ColourPicker::BUILD()
|
||||
{
|
||||
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
|
||||
// field the same height as other parameter buttons before drawing its swatch.
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
|
||||
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
|
||||
|
||||
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
|
||||
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
|
||||
window = button;
|
||||
button->SetBitmapMargins(0, 0);
|
||||
// Validate the color
|
||||
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk()) {
|
||||
clr = wxTransparentColour;
|
||||
}
|
||||
|
||||
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
|
||||
if (parent_is_custom_ctrl && m_opt.height < 0)
|
||||
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
|
||||
button->SetFont(wxGetApp().normal_font());
|
||||
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
set_value(boost::any(clr_str));
|
||||
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
|
||||
apply_user_color(wxTransparentColour);
|
||||
event.Skip();
|
||||
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
|
||||
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
|
||||
convert_to_picker_widget(temp);
|
||||
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
|
||||
|
||||
// // recast as a wxWindow to fit the calling convention
|
||||
window = dynamic_cast<wxWindow*>(temp);
|
||||
|
||||
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
|
||||
#ifdef __WXMSW__
|
||||
temp,
|
||||
#endif
|
||||
this](wxCommandEvent e) {
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(temp, temp->GetColour());
|
||||
#endif
|
||||
on_change_field();
|
||||
}), temp->GetId());
|
||||
|
||||
// ORCA reset value to default on right click. previously no way to switch back on windows
|
||||
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
|
||||
#ifdef __WXMSW__
|
||||
temp->SetColour(wxTransparentColour);
|
||||
draw_bmp_btn(temp, wxTransparentColour);
|
||||
#else
|
||||
set_undef_value(temp);
|
||||
#endif
|
||||
on_change_field();
|
||||
e.Skip();
|
||||
});
|
||||
// Native layouts may assign the final field size after construction.
|
||||
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
|
||||
draw_bmp_btn();
|
||||
event.Skip();
|
||||
});
|
||||
button->SetToolTip(get_tooltip_text(clr_str));
|
||||
|
||||
temp->SetToolTip(get_tooltip_text(clr_str));
|
||||
}
|
||||
|
||||
void ColourPicker::draw_bmp_btn()
|
||||
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
|
||||
{
|
||||
if (!window) return;
|
||||
auto* btn = static_cast<wxButton*>(window);
|
||||
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
|
||||
field->SetColour(wxTransparentColour);
|
||||
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
|
||||
wxImage image(btn->GetBitmap().GetSize());
|
||||
image.InitAlpha();
|
||||
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return;
|
||||
#ifdef __WXMSW__
|
||||
wxGCDC dc2(dc);
|
||||
#else
|
||||
wxDC &dc2(dc);
|
||||
#endif
|
||||
dc2.SetPen(wxPen("#F1754E", 1));
|
||||
|
||||
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
|
||||
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
|
||||
|
||||
dc.SelectObject(wxNullBitmap);
|
||||
btn->SetBitmapLabel(bmp);
|
||||
}
|
||||
|
||||
// ORCA match style with button on windows
|
||||
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
{
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
|
||||
btn->SetBackgroundColour(*wxWHITE);
|
||||
wxGetApp().UpdateDarkUI(btn);
|
||||
|
||||
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
|
||||
wxSize btn_sz = btn->GetSize();
|
||||
wxImage image(btn_sz);
|
||||
@@ -2666,11 +2712,11 @@ void ColourPicker::draw_bmp_btn()
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return bmp;
|
||||
wxGCDC dc2(dc);
|
||||
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
|
||||
|
||||
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
|
||||
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
|
||||
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
|
||||
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
|
||||
|
||||
int padding = btn->FromDIP(5);
|
||||
dc2.SetPen(*wxTRANSPARENT_PEN);
|
||||
@@ -2689,72 +2735,117 @@ void ColourPicker::draw_bmp_btn()
|
||||
return bmp;
|
||||
};
|
||||
|
||||
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
|
||||
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
|
||||
}
|
||||
|
||||
void ColourPicker::set_value(const std::string& value, bool change_event)
|
||||
void ColourPicker::set_value(const boost::any& value, bool change_event)
|
||||
{
|
||||
set_value(boost::any(wxString::FromUTF8(value)), change_event);
|
||||
}
|
||||
m_disable_change_event = !change_event;
|
||||
const wxString clr_str(boost::any_cast<wxString>(value));
|
||||
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
|
||||
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
|
||||
{
|
||||
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
|
||||
const wxString clr_str = boost::any_cast<wxString>(value);
|
||||
const wxColour parsed(clr_str);
|
||||
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
|
||||
draw_bmp_btn();
|
||||
#ifdef __WXMSW__
|
||||
const wxColour parsed_clr(clr_str);
|
||||
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
|
||||
field->SetColour(clr);
|
||||
draw_bmp_btn(field, clr);
|
||||
#else
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk())
|
||||
set_undef_value(field);
|
||||
else
|
||||
field->SetColour(clr);
|
||||
#endif
|
||||
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
|
||||
boost::any& ColourPicker::get_value()
|
||||
{
|
||||
m_value = m_colour == wxTransparentColour ? std::string() :
|
||||
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
|
||||
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();
|
||||
save_colors_to_config();
|
||||
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
|
||||
if (colour == wxTransparentColour)
|
||||
m_value = std::string("");
|
||||
else {
|
||||
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
|
||||
}
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void ColourPicker::msw_rescale()
|
||||
{
|
||||
Field::msw_rescale();
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
|
||||
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
|
||||
if (m_opt.height >= 0)
|
||||
size.SetHeight(m_opt.height * m_em_unit);
|
||||
else if (parent_is_custom_ctrl && opt_height > 0)
|
||||
size.SetHeight(lround(opt_height * m_em_unit));
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
|
||||
if (parent_is_custom_ctrl) window->SetSize(size);
|
||||
else window->SetMinSize(size);
|
||||
draw_bmp_btn();
|
||||
if (parent_is_custom_ctrl)
|
||||
field->SetSize(size);
|
||||
else
|
||||
field->SetMinSize(size);
|
||||
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(field, field->GetColour());
|
||||
#else
|
||||
if (field->GetColour() == wxTransparentColour)
|
||||
set_undef_value(field);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void ColourPicker::sys_color_changed()
|
||||
{
|
||||
if (window) wxGetApp().UpdateDarkUI(window, true);
|
||||
draw_bmp_btn();
|
||||
#ifdef _WIN32
|
||||
if (wxWindow* win = this->getWindow())
|
||||
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
|
||||
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
|
||||
draw_bmp_btn(picker, picker->GetColour());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
|
||||
{
|
||||
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
|
||||
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
|
||||
ColorPickerDialog dialog(m_parent, color, {}, false, window);
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
|
||||
void ColourPicker::on_button_click(wxCommandEvent &event) {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
}
|
||||
}
|
||||
m_picker_widget->OnButtonClick(event);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
|
||||
{
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
|
||||
if (m_picker_widget) {
|
||||
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
m_clrData = m_picker_widget->GetColourData();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::save_colors_to_config() {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors;
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void PointCtrl::BUILD()
|
||||
|
||||
+24
-10
@@ -22,10 +22,10 @@
|
||||
#include <boost/any.hpp>
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/bmpcbox.h>
|
||||
#include <optional>
|
||||
#include <wx/clrpicker.h>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Config.hpp"
|
||||
@@ -610,27 +610,41 @@ private:
|
||||
};
|
||||
|
||||
class ColourPicker : public Field {
|
||||
using Field::Field;
|
||||
|
||||
void set_undef_value(wxColourPickerCtrl* field);
|
||||
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
|
||||
public:
|
||||
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
|
||||
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
|
||||
~ColourPicker() {}
|
||||
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
void set_value(const std::string& value, bool change_event = false);
|
||||
|
||||
void set_value(const std::string& value, bool change_event = false) {
|
||||
m_disable_change_event = !change_event;
|
||||
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
void set_value(const boost::any& value, bool change_event = false) override;
|
||||
boost::any& get_value() override;
|
||||
void msw_rescale() override;
|
||||
void sys_color_changed() override;
|
||||
void enable() override { if (window) window->Enable(); }
|
||||
void disable() override{ if (window) window->Disable(); }
|
||||
|
||||
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
|
||||
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
|
||||
wxWindow* getWindow() override { return window; }
|
||||
|
||||
private:
|
||||
// An absent result is cancellation; transparent color is the undefined field.
|
||||
void apply_user_color(const std::optional<wxColour>& color);
|
||||
void draw_bmp_btn();
|
||||
void on_button_click(wxCommandEvent& event);
|
||||
wxColour m_colour = wxTransparentColour;
|
||||
void convert_to_picker_widget(wxColourPickerCtrl *widget);
|
||||
void on_button_click(wxCommandEvent &WXUNUSED(ev));
|
||||
void save_colors_to_config();
|
||||
private:
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
wxColourData* m_clrData{nullptr};
|
||||
wxColourPickerWidget* m_picker_widget{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
class PointCtrl : public Field {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
@@ -277,6 +278,19 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
|
||||
return bdc.bitmap;
|
||||
}
|
||||
|
||||
// Sort colors by HSV values (primarily by hue, then saturation, then value)
|
||||
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
|
||||
if (colors.size() < 2) return;
|
||||
std::sort(colors.begin(), colors.end(),
|
||||
[](const wxColour& a, const wxColour& b) {
|
||||
ColourHSV ha = wxColourToHSV(a);
|
||||
ColourHSV hb = wxColourToHSV(b);
|
||||
if (ha.h != hb.h) return ha.h < hb.h;
|
||||
if (ha.s != hb.s) return ha.s < hb.s;
|
||||
return ha.v < hb.v;
|
||||
});
|
||||
}
|
||||
|
||||
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
|
||||
{
|
||||
// Check if color is transparent
|
||||
@@ -416,16 +430,24 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
|
||||
{
|
||||
if (colors.empty()) return wxNullBitmap;
|
||||
|
||||
if (force_gradient && colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(colors, size);
|
||||
// Make a copy to sort without modifying original
|
||||
std::vector<wxColour> sorted_colors = colors;
|
||||
|
||||
// Sort colors by HSV when there are 2 or more colors
|
||||
if (sorted_colors.size() >= 2) {
|
||||
sort_colors_by_hsv(sorted_colors);
|
||||
}
|
||||
|
||||
switch (colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(colors, size);
|
||||
default: return create_gradient_filament_bitmap(colors, size);
|
||||
if (force_gradient && sorted_colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
}
|
||||
|
||||
switch (sorted_colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(sorted_colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
|
||||
default: return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -83,8 +79,7 @@ void FilamentPickerDialog::StartFlashing()
|
||||
m_flash_timer->Start(50);
|
||||
}
|
||||
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color)
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
|
||||
: DPIDialog(parent ? parent : wxGetApp().mainframe,
|
||||
wxID_ANY,
|
||||
_L("Select Filament"),
|
||||
@@ -97,7 +92,6 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
|
||||
m_color_query = new FilamentColorCodeQuery();
|
||||
m_is_data_loaded = LoadFilamentData(fila_id);
|
||||
m_cur_filament_color = fila_color;
|
||||
m_ordered_color = std::move(ordered_color);
|
||||
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
|
||||
m_cur_color_name = new wxString(color_name);
|
||||
|
||||
@@ -447,9 +441,6 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
|
||||
|
||||
// Bind click
|
||||
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
|
||||
m_selection_changed = true;
|
||||
m_ordered_color.reset();
|
||||
m_custom_selection.reset();
|
||||
m_cur_filament_color = color_code->GetFilaColor();
|
||||
UpdatePreview(*color_code);
|
||||
UpdateButtonStates(btn);
|
||||
@@ -520,27 +511,24 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
|
||||
Layout();
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
|
||||
{
|
||||
std::vector<wxColour> wx_colors = {custom_color};
|
||||
|
||||
// Update preview bitmap
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
|
||||
|
||||
if (bmp.IsOk()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
|
||||
m_color_demo->SetBitmap(bmp);
|
||||
m_color_demo->SetBackgroundColour(wx_colors.front());
|
||||
m_color_demo->SetBackgroundColour(custom_color);
|
||||
m_color_demo->Refresh();
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
|
||||
}
|
||||
|
||||
// Update preview labels for custom color
|
||||
wxString label;
|
||||
for (const auto& color : wx_colors) {
|
||||
if (!label.empty()) label += " / ";
|
||||
label += color.GetAsString(wxC2S_HTML_SYNTAX);
|
||||
}
|
||||
m_label_preview_color->SetLabel(label);
|
||||
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
|
||||
m_label_preview_idx->SetLabel("");
|
||||
Layout();
|
||||
}
|
||||
@@ -569,6 +557,16 @@ void FilamentPickerDialog::CreateMoreInfoButton()
|
||||
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
|
||||
}
|
||||
|
||||
wxColourData FilamentPickerDialog::GetSingleColorData()
|
||||
{
|
||||
wxColourData data;
|
||||
data.SetChooseFull(true);
|
||||
if (m_cur_filament_color.ColorCount() > 0) {
|
||||
data.SetColour(*m_cur_filament_color.m_colors.begin());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::BindEvents()
|
||||
{
|
||||
// Bind mouse events for window dragging
|
||||
@@ -590,37 +588,27 @@ void FilamentPickerDialog::BindEvents()
|
||||
// Pause click detection while color picker is open
|
||||
StopClickDetection();
|
||||
|
||||
FilamentColorPickerValue current;
|
||||
if (m_ordered_color) {
|
||||
current = *m_ordered_color;
|
||||
} else {
|
||||
for (const auto& color : m_cur_filament_color.m_colors)
|
||||
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
|
||||
}
|
||||
const auto initial = filament_color_picker_initial(current);
|
||||
std::optional<ColorSelection> selected;
|
||||
{
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
selected = dialog.selection();
|
||||
}
|
||||
StartClickDetection();
|
||||
if (!filament_color_picker_changed(initial, selected))
|
||||
return;
|
||||
wxColourData original_data = GetSingleColorData();
|
||||
wxColourData result = show_sys_picker_dialog(this, original_data);
|
||||
|
||||
m_custom_selection = selected;
|
||||
m_ordered_color = filament_color_picker_result(*selected);
|
||||
m_selection_changed = true;
|
||||
std::vector<wxColour> colors;
|
||||
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());
|
||||
// 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);
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include <optional>
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "EncodedFilament.hpp"
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/event.h>
|
||||
@@ -26,13 +25,10 @@ namespace Slic3r { namespace GUI {
|
||||
class FilamentPickerDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
|
||||
virtual ~FilamentPickerDialog();
|
||||
|
||||
// Public interface methods
|
||||
bool HasSelectionChanged() const { return m_selection_changed; }
|
||||
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
|
||||
bool IsDataLoaded() const { return m_is_data_loaded; }
|
||||
wxColour GetSelectedColour() const;
|
||||
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
|
||||
@@ -72,7 +68,7 @@ private:
|
||||
|
||||
// UI update methods
|
||||
void UpdatePreview(const FilamentColorCode& filament);
|
||||
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
|
||||
void UpdateCustomColorPreview(const wxColour& custom_color);
|
||||
void UpdateButtonStates(wxBitmapButton* selected_btn);
|
||||
|
||||
// Shaped window methods
|
||||
@@ -81,6 +77,7 @@ private:
|
||||
|
||||
// Data loading
|
||||
bool LoadFilamentData(const wxString& fila_id);
|
||||
wxColourData GetSingleColorData();
|
||||
|
||||
// Flash effect
|
||||
void StartFlashing();
|
||||
@@ -101,9 +98,6 @@ private:
|
||||
FilamentColorCodes* m_cur_color_codes{nullptr};
|
||||
wxBitmapButton* m_cur_selected_btn{nullptr};
|
||||
FilamentColor m_cur_filament_color;
|
||||
std::optional<FilamentColorPickerValue> m_ordered_color;
|
||||
std::optional<ColorSelection> m_custom_selection;
|
||||
bool m_selection_changed = false;
|
||||
|
||||
// Shaped window members
|
||||
wxBitmap m_shape_bmp;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -365,6 +365,8 @@ private:
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
std::string m_cloud_handoff_state;
|
||||
// The OrcaCloud path the open requests target, remembered so a login re-seed keeps the same page.
|
||||
std::string m_cloud_open_path;
|
||||
uint64_t m_cloud_handoff_gen { 0 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
@@ -466,6 +468,9 @@ public:
|
||||
#if wxUSE_WEBVIEW_EDGE
|
||||
void init_webview_runtime();
|
||||
#endif
|
||||
// Switches to the Home tab and opens the OrcaCloud pane beside the homepage on `next`, a
|
||||
// same-site OrcaCloud path (empty = the page's default). Work is deferred internally.
|
||||
void open_orca_cloud(const std::string& next = std::string());
|
||||
static unsigned get_colour_approx_luma(const wxColour& colour);
|
||||
static bool dark_mode();
|
||||
const wxColour get_label_default_clr_system();
|
||||
@@ -572,7 +577,7 @@ public:
|
||||
// called from the pane when the page asks for a session; it opens the desktop login dialog if
|
||||
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
wxString cloud_webview_handoff(const std::string& next = std::string());
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
void mint_cloud_web_session(const std::string& state, uint64_t gen);
|
||||
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "GLGizmoUtils.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -72,6 +73,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
@@ -97,6 +99,48 @@ namespace {
|
||||
// bigger jump than the whole useful region.
|
||||
constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic);
|
||||
|
||||
// The scrollbar of the panel's scrolling areas (the panel body and the texture library). ImGui's stock
|
||||
// scrollbar is a wide, square-cornered slab in a tinted track - against the flat panel it reads as a raw
|
||||
// widget bolted onto the edge - so it is slimmed to a rounded thumb over an invisible track. The child
|
||||
// background goes transparent with it, so the area reads as part of the panel.
|
||||
void push_slim_scrollbar_style(float width, bool dark)
|
||||
{
|
||||
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, width);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * width);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
}
|
||||
|
||||
void pop_slim_scrollbar_style()
|
||||
{
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
|
||||
std::string ellipsize(std::string s, float max_w, float font_size = 0.f)
|
||||
{
|
||||
const auto text_width = [&](const std::string &text) {
|
||||
return font_size > 0.f
|
||||
? ImGui::GetFont()->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.f, text.c_str()).x
|
||||
: ImGui::CalcTextSize(text.c_str()).x;
|
||||
};
|
||||
if (text_width(s) <= max_w)
|
||||
return s;
|
||||
while (!s.empty() && text_width(s + "...") > max_w) {
|
||||
unsigned char c;
|
||||
do { // a whole code point at a time
|
||||
c = static_cast<unsigned char>(s.back());
|
||||
s.pop_back();
|
||||
} while (!s.empty() && (c & 0xC0) == 0x80);
|
||||
}
|
||||
return s + "...";
|
||||
}
|
||||
|
||||
// Uploads decoded 8-bit grayscale height data as an RGBA GPU texture for display in the panel.
|
||||
// Shared by the per-layer thumbnail and the picker's library entries, which differ only in where
|
||||
// their pixels came from.
|
||||
@@ -2919,8 +2963,9 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
|
||||
}
|
||||
else if (cmd == int(Command::PickTexture)) {
|
||||
// The same texture library the layer card opens; the panel renders it this frame.
|
||||
m_picker_slot = m_active_layer_slot;
|
||||
m_picker_open_request = true;
|
||||
m_picker_slot = m_active_layer_slot;
|
||||
m_picker_open_request = true;
|
||||
m_picker_reveal_selected = true;
|
||||
} else if (cmd == int(Command::Unwrap)) {
|
||||
// The solve runs only on this - painting, the seam angle and seam marking never trigger it.
|
||||
m_uv_unwrap_pending = true;
|
||||
@@ -4102,7 +4147,7 @@ void GLGizmoTextureDisplacement::set_layer_texture(TextureDisplacementLayer &lay
|
||||
// the PNG the first time the layer is previewed or baked.
|
||||
layer.image_data = tex->image_data;
|
||||
// The user's own textures are listed most recently used first.
|
||||
if (entry.is_user)
|
||||
if (entry.source == TextureSource::User)
|
||||
touch_user_texture(entry.path);
|
||||
|
||||
rebuild_preview();
|
||||
@@ -4129,6 +4174,40 @@ void GLGizmoTextureDisplacement::import_custom_texture(TextureDisplacementLayer
|
||||
set_layer_texture(layer, *entry);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::ease_wheel_scroll(float &target, float &applied)
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
const float scroll_now = ImGui::GetScrollY();
|
||||
const float scroll_max = ImGui::GetScrollMaxY();
|
||||
|
||||
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
|
||||
// content shrinking under a clamped offset, a SetScroll*() call - has to re-seed the target, or the
|
||||
// easing below would immediately drag the view back to where it last animated to.
|
||||
if (applied < 0.f || std::abs(scroll_now - applied) > 0.5f)
|
||||
target = scroll_now;
|
||||
|
||||
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
|
||||
target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
|
||||
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
|
||||
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
|
||||
target = std::floor(std::clamp(target, 0.f, scroll_max));
|
||||
|
||||
const float delta = target - scroll_now;
|
||||
if (std::abs(delta) >= 1.f) {
|
||||
// Exponential ease, formulated against the frame time so the glide takes the same wall time
|
||||
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
|
||||
// away, so land on the target outright once the remainder is that small.
|
||||
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
|
||||
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
|
||||
const float next = std::floor(scroll_now + step);
|
||||
ImGui::SetScrollY(next);
|
||||
applied = next;
|
||||
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
|
||||
} else {
|
||||
applied = scroll_now;
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -4145,17 +4224,55 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
ImGui::OpenPopup(popup_id);
|
||||
}
|
||||
|
||||
const float tile = std::round(m_imgui->scaled(3.2f));
|
||||
const float spacing = std::round(m_imgui->scaled(0.4f));
|
||||
const float grid_w = 4.f * tile + 3.f * spacing;
|
||||
const float gap = m_imgui->scaled(0.5f);
|
||||
const float tile = std::round(m_imgui->scaled(3.2f));
|
||||
const float spacing = std::round(m_imgui->scaled(0.4f));
|
||||
const float grid_w = 4.f * tile + 3.f * spacing;
|
||||
const float gap = m_imgui->scaled(0.5f);
|
||||
const float scrollbar_w = m_imgui->scaled(0.5f);
|
||||
const float ring_w = std::max(2.f, std::round(m_imgui->scaled(0.1f))); // the current texture's ring
|
||||
const float frame_h = ImGui::GetFrameHeight();
|
||||
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
// A fixed size rather than a fit-to-content one. The width is the four-column grid, a gutter and the
|
||||
// slim scrollbar beside it, plus the window padding, so the columns always fit and no horizontal
|
||||
// scrollbar is needed; it derives from scaled values, so it tracks the DPI like the rest.
|
||||
const float fixed_w = grid_w + spacing + scrollbar_w + 2.f * style.WindowPadding.x;
|
||||
|
||||
// BeginPopup() ORs in ImGuiWindowFlags_AlwaysAutoResize (deps_src/imgui/imgui.cpp:8371) and no flag
|
||||
// turns it back off. A SetNextWindowSize() overrides it for each axis it sets (imgui.cpp:5983-5996),
|
||||
// so an explicit size is the only way to fix the popup's size; the grid child below then scrolls.
|
||||
//
|
||||
// Top-aligned with the panel, which keeps it clear of the gizmo toolbar above a docked panel, and as
|
||||
// tall as the panel or 70% of the window, whichever is taller, while keeping a gap inside the window.
|
||||
// The height follows the window and the panel, never the content, so it does not jump as packs load or
|
||||
// sections collapse.
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
const float vp_top = viewport->Pos.y + gap;
|
||||
const float vp_bottom = viewport->Pos.y + viewport->Size.y - gap;
|
||||
const float height = std::max(std::min(std::max(0.70f * viewport->Size.y, panel_max.y - panel_min.y), vp_bottom - vp_top),
|
||||
2.f * frame_h);
|
||||
const float popup_y = std::max(vp_top, std::min(panel_min.y, vp_bottom - height));
|
||||
// Beside the panel rather than over it, so the layer being retextured stays in view: to its left when
|
||||
// there is room (the docked panel sits against the right edge of the canvas), otherwise to its right.
|
||||
const float popup_w = grid_w + 2.f * ImGui::GetStyle().WindowPadding.x;
|
||||
const bool left = panel_min.x - gap - popup_w >= 0.f;
|
||||
ImGui::SetNextWindowPos(ImVec2(left ? panel_min.x - gap : panel_max.x + gap, panel_min.y + m_imgui->scaled(4.f)),
|
||||
ImGuiCond_Appearing, ImVec2(left ? 1.f : 0.f, 0.f));
|
||||
if (!ImGui::BeginPopup(popup_id))
|
||||
const float width = viewport->Size.x > 0.f ? std::min(fixed_w, viewport->Size.x) : fixed_w;
|
||||
const bool left = panel_min.x - gap - width >= 0.f;
|
||||
const float popup_x = left ? panel_min.x - gap - width : panel_max.x + gap;
|
||||
// Cond_Always, not Appearing: a viewport resize while the popup is open must move and resize it too.
|
||||
ImGui::SetNextWindowPos(ImVec2(popup_x, popup_y), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
|
||||
// Drawn as the panel's own sheet: its opaque background and rounding, no border. A popup is filled with
|
||||
// PopupBg, which the panel's toolbar style leaves at ImGui's translucent near-black - a dark window
|
||||
// beside a light panel, with whatever it covers showing through. Only Begin reads these, so they are
|
||||
// popped at once and the tooltips shown from inside keep their own look.
|
||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, style.WindowRounding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.f);
|
||||
// The window itself never scrolls; the grid child below does.
|
||||
const bool open = ImGui::BeginPopup(popup_id, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
if (!open)
|
||||
return;
|
||||
if (layer == nullptr) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
@@ -4171,16 +4288,49 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
|
||||
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
|
||||
|
||||
m_imgui->push_bold_font();
|
||||
m_imgui->text(_L("Choose a texture"));
|
||||
m_imgui->pop_bold_font();
|
||||
// The header stays put while the grid scrolls under it: the title says what the popup is for, and the
|
||||
// close button is there for anyone who does not know that a click outside closes it.
|
||||
bool close_clicked = false;
|
||||
{
|
||||
ImDrawList *hdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 row = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + 0.5f * (frame_h - ImGui::GetTextLineHeight())));
|
||||
m_imgui->bold_text(_u8L("Choose a texture"));
|
||||
const ImVec2 btn(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x - frame_h, row.y);
|
||||
ImGui::SetCursorScreenPos(btn);
|
||||
close_clicked = ImGui::InvisibleButton("##close", ImVec2(frame_h, frame_h));
|
||||
const bool hot = ImGui::IsItemHovered();
|
||||
const ImVec2 c(btn.x + 0.5f * frame_h, btn.y + 0.5f * frame_h);
|
||||
const float k = std::round(0.18f * frame_h);
|
||||
const float line_w = std::max(1.f, std::round(m_imgui->scaled(0.08f)));
|
||||
const ImU32 x_col = ImGui::GetColorU32(ImGuiCol_Text, hot ? 1.f : 0.6f);
|
||||
if (hot)
|
||||
hdl->AddCircleFilled(c, 0.5f * frame_h, ImGui::GetColorU32(ImGuiCol_Text, 0.08f));
|
||||
hdl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), x_col, line_w);
|
||||
hdl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), x_col, line_w);
|
||||
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + frame_h + style.ItemSpacing.y));
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
const std::vector<TextureLibraryEntry> &library = texture_library();
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
const ImU32 selected_col = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA);
|
||||
std::optional<TextureLibraryEntry> chosen;
|
||||
std::optional<TextureLibraryEntry> to_remove;
|
||||
bool import_clicked = false;
|
||||
// A reinstalled pack can drop new bytes at a path already cached on this gizmo, and get_library_texture()
|
||||
// caches a failure as well as a success, so a moved generation makes every cached entry suspect.
|
||||
if (const uint64_t generation = texture_library_generation(); generation != m_library_generation) {
|
||||
m_library_textures.clear();
|
||||
m_library_generation = generation;
|
||||
}
|
||||
|
||||
// The grid scrolls as the panel body does: the same slim scrollbar, and the wheel eased by
|
||||
// ease_wheel_scroll() instead of ImGui's five-line jumps (hence NoScrollWithMouse).
|
||||
push_slim_scrollbar_style(scrollbar_w, wxGetApp().dark_mode());
|
||||
ImGui::BeginChild("##texture_grid", ImVec2(0.f, 0.f), false, ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ease_wheel_scroll(m_picker_scroll_target, m_picker_scroll_applied);
|
||||
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList(); // the child's, so tiles clip to the grid
|
||||
std::optional<TextureLibraryEntry> chosen;
|
||||
std::optional<TextureLibraryEntry> to_remove;
|
||||
bool import_clicked = false;
|
||||
bool download_clicked = false;
|
||||
|
||||
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
|
||||
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the
|
||||
@@ -4196,16 +4346,15 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
ImGui::Dummy(ImVec2(grid_w, float(rows) * tile + float(rows - 1) * spacing));
|
||||
}
|
||||
};
|
||||
// One group's tiles: shipped textures alphabetically, or the user's own most recently used first (the
|
||||
// order texture_library() keeps), those with a remove button on hover. Returns how many it drew.
|
||||
const auto grid = [&](const ImVec2 &origin, bool user) {
|
||||
// One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
|
||||
// name, the user's own most recently used first - the order texture_library() produced); a user tile
|
||||
// carries a remove button on hover. Returns how many it drew.
|
||||
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
|
||||
int n = 0;
|
||||
for (const TextureLibraryEntry &entry : library) {
|
||||
if (entry.is_user != user)
|
||||
continue;
|
||||
for (const TextureLibraryEntry *entry : entries) {
|
||||
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
|
||||
ImGui::PushID(entry.path.c_str());
|
||||
const LibraryTexture *tex = get_library_texture(entry.path);
|
||||
ImGui::PushID(entry->path.c_str());
|
||||
const LibraryTexture *tex = get_library_texture(entry->path);
|
||||
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
|
||||
bool clicked = false;
|
||||
if (thumb != nullptr) {
|
||||
@@ -4223,15 +4372,27 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
} else {
|
||||
clicked = ImGui::Button("?", ImVec2(tile, tile));
|
||||
}
|
||||
if (user)
|
||||
if (removable)
|
||||
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
|
||||
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
|
||||
if (entry.path == layer->path)
|
||||
dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f);
|
||||
if (ImGui::IsItemHovered())
|
||||
panel_tooltip(entry.name);
|
||||
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
|
||||
const bool current = entry->path == layer->path;
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
// The layer's current texture carries an accent ring, and the tile under the pointer a thinner,
|
||||
// fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
|
||||
if (current || hovered) {
|
||||
const float w = current ? ring_w : std::round(0.5f * ring_w);
|
||||
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
|
||||
ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w);
|
||||
}
|
||||
// Centred, as far as the scroll range allows, so the textures around it are in view too.
|
||||
if (current && m_picker_reveal_selected)
|
||||
ImGui::SetScrollFromPosY(0.5f * (a.y + b.y) - ImGui::GetWindowPos().y, 0.5f);
|
||||
if (hovered)
|
||||
panel_tooltip(entry->name);
|
||||
|
||||
if (user && ImGui::IsMouseHoveringRect(a, b)) {
|
||||
// Clipped by the grid's clip rect, so a tile scrolled out of the grid or under its scrollbar
|
||||
// does not offer its remove button. IsMouseHoveringRect() clips by default.
|
||||
if (removable && ImGui::IsMouseHoveringRect(a, b)) {
|
||||
const float r = std::round(tile * 0.17f);
|
||||
const ImVec2 c(b.x - r - 2.f, a.y + r + 2.f);
|
||||
ImGui::SetCursorScreenPos(ImVec2(c.x - r, c.y - r));
|
||||
@@ -4244,27 +4405,162 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
if (hot)
|
||||
panel_tooltip(_u8L("Remove from My textures"));
|
||||
if (remove_clicked)
|
||||
to_remove = entry;
|
||||
to_remove = *entry;
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (clicked)
|
||||
chosen = entry;
|
||||
chosen = *entry;
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
|
||||
// Every section collapses; the state is remembered for the app run. Pack keys use pack_name, a
|
||||
// capability name unique per type across loaded packages and stable across reinstalls. This function-local
|
||||
// static survives popup closes, never touches app_config, and resets on restart.
|
||||
static std::map<std::string, bool> s_section_collapsed; // true = collapsed; absent = expanded
|
||||
|
||||
const auto split_pack_title = [](const std::string &title) -> std::pair<std::string, std::string> {
|
||||
size_t split = std::string::npos;
|
||||
for (size_t i = 0; i + 3 < title.size(); ++i)
|
||||
if (title[i] == ' ' && (title[i + 1] == 'b' || title[i + 1] == 'B') &&
|
||||
(title[i + 2] == 'y' || title[i + 2] == 'Y') && title[i + 3] == ' ')
|
||||
split = i;
|
||||
if (split == std::string::npos)
|
||||
return { title, {} };
|
||||
|
||||
std::string name = title.substr(0, split);
|
||||
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back())))
|
||||
name.pop_back();
|
||||
std::string attribution = title.substr(split);
|
||||
size_t first = 0;
|
||||
while (first < attribution.size() && std::isspace(static_cast<unsigned char>(attribution[first])))
|
||||
++first;
|
||||
attribution.erase(0, first);
|
||||
if (name.empty() || attribution.empty())
|
||||
return { title, {} };
|
||||
return { name, attribution };
|
||||
};
|
||||
|
||||
// A section heading, one row as wide as the grid: a click anywhere on it collapses or expands the
|
||||
// section. Child headings indent their contents while keeping their counts right-aligned. Attribution
|
||||
// lines use a smaller disabled style; the grid follows closer than the next heading does.
|
||||
// Returns true when the section is open, so the caller knows whether to draw its contents.
|
||||
const auto section_header = [&](const std::string &key, const std::string &title, size_t count, float indent,
|
||||
const std::string &attribution, const std::string &full_title) {
|
||||
bool &collapsed = s_section_collapsed[key]; // default-inserts false -> expanded
|
||||
const ImVec2 row = ImGui::GetCursorScreenPos();
|
||||
const bool two_lines = !attribution.empty();
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const float small_font_size = ImGui::GetFontSize() * 0.8f;
|
||||
const float small_line_h = line_h * 0.8f;
|
||||
const float row_h = two_lines ? std::max(frame_h, line_h + small_line_h + spacing) : frame_h;
|
||||
const ImVec2 row_max(row.x + grid_w, row.y + row_h);
|
||||
if (ImGui::InvisibleButton(key.c_str(), ImVec2(grid_w, row_h)))
|
||||
collapsed = !collapsed;
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
if (hovered)
|
||||
dl->AddRectFilled(row, row_max, ImGui::GetColorU32(ImGuiCol_Text, 0.05f), style.FrameRounding);
|
||||
|
||||
const float inset = std::round(0.25f * frame_h);
|
||||
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
|
||||
const float cx = row.x + inset + indent + 0.5f * arrow;
|
||||
const float cy = row.y + 0.5f * row_h;
|
||||
const float hs = 0.3f * arrow;
|
||||
const ImU32 accent = ImGui::GetColorU32(orca);
|
||||
if (collapsed)
|
||||
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx + 0.5f * hs, cy), ImVec2(cx - 0.5f * hs, cy + hs), accent);
|
||||
else
|
||||
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs), ImVec2(cx, cy + 0.5f * hs), accent);
|
||||
|
||||
const std::string n = std::to_string(count);
|
||||
const float n_w = ImGui::CalcTextSize(n.c_str()).x;
|
||||
const float tx = row.x + inset + indent + arrow + spacing;
|
||||
const float max_text_w = std::max(0.f, row_max.x - inset - n_w - spacing - tx);
|
||||
const std::string shown = ellipsize(title, max_text_w);
|
||||
const std::string shown_attribution = two_lines ? ellipsize(attribution, max_text_w, small_font_size) : std::string{};
|
||||
const float title_y = two_lines ? row.y + 0.5f * (row_h - line_h - small_line_h)
|
||||
: row.y + 0.5f * (frame_h - line_h);
|
||||
const float count_y = row.y + 0.5f * (row_h - line_h);
|
||||
dl->AddText(ImVec2(tx, title_y), ImGui::GetColorU32(ImGuiCol_Text), shown.c_str());
|
||||
if (two_lines)
|
||||
dl->AddText(ImGui::GetFont(), small_font_size, ImVec2(tx, title_y + line_h),
|
||||
ImGui::GetColorU32(ImGuiCol_TextDisabled), shown_attribution.c_str());
|
||||
dl->AddText(ImVec2(row_max.x - inset - n_w, count_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), n.c_str());
|
||||
if (hovered && (shown != title || shown_attribution != attribution))
|
||||
panel_tooltip(full_title); // a long plugin-supplied name, cut by the fixed width
|
||||
ImGui::SetCursorScreenPos(ImVec2(row.x, row_max.y + spacing));
|
||||
return !collapsed;
|
||||
};
|
||||
|
||||
// Built-in is first, plugin packs are grouped under Texture libraries, and My
|
||||
// textures stays last so its import tile and help line describe that group. Pack names identify both
|
||||
// their child section and state.
|
||||
struct Section
|
||||
{
|
||||
std::string key;
|
||||
std::string title;
|
||||
std::vector<const TextureLibraryEntry *> entries;
|
||||
};
|
||||
std::vector<Section> sections;
|
||||
std::map<std::string, size_t> pack_section; // pack_name -> index in `sections`
|
||||
sections.push_back({ "builtin:", _u8L("Built-in"), {} });
|
||||
for (const TextureLibraryEntry &entry : library) {
|
||||
if (entry.source == TextureSource::Shipped) {
|
||||
sections.front().entries.push_back(&entry); // the section exists even when the library is empty
|
||||
continue;
|
||||
}
|
||||
if (entry.source == TextureSource::User)
|
||||
continue; // added below, so My textures stays last
|
||||
if (entry.source != TextureSource::Plugin)
|
||||
continue;
|
||||
const auto [it, inserted] = pack_section.emplace(entry.pack_name, sections.size());
|
||||
if (inserted)
|
||||
sections.push_back({ "pack:" + entry.pack_name, entry.pack_name, {} });
|
||||
sections[it->second].entries.push_back(&entry);
|
||||
}
|
||||
sections.push_back({ "user:", _u8L("My textures"), {} });
|
||||
for (const TextureLibraryEntry &entry : library)
|
||||
if (entry.source == TextureSource::User)
|
||||
sections.back().entries.push_back(&entry);
|
||||
|
||||
const float child_indent = std::round(m_imgui->scaled(0.8f));
|
||||
const Section &built_in = sections.front();
|
||||
if (section_header(built_in.key, built_in.title, built_in.entries.size(), 0.f, {}, built_in.title)) {
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const int count = grid(origin, false);
|
||||
const int count = grid(origin, built_in.entries, false);
|
||||
end_grid(origin, count);
|
||||
if (count == 0)
|
||||
ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
|
||||
}
|
||||
ImGui::TextDisabled("%s", _u8L("My textures").c_str());
|
||||
{
|
||||
|
||||
size_t subscribed_count = 0;
|
||||
for (size_t i = 1; i + 1 < sections.size(); ++i)
|
||||
subscribed_count += sections[i].entries.size();
|
||||
|
||||
const std::string subscribed_title = _u8L("Texture libraries");
|
||||
if (section_header("subscribed:", subscribed_title, subscribed_count, 0.f, {}, subscribed_title)) {
|
||||
for (size_t i = 1; i + 1 < sections.size(); ++i) {
|
||||
const Section &pack = sections[i];
|
||||
const auto [name, attribution] = split_pack_title(pack.title);
|
||||
if (!section_header(pack.key, name, pack.entries.size(), child_indent, attribution, pack.title))
|
||||
continue;
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const int count = grid(origin, pack.entries, false);
|
||||
end_grid(origin, count);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0.f, spacing));
|
||||
const std::string download_label = _u8L("Explore more texture libraries");
|
||||
if (ImGui::Button(download_label.c_str(), ImVec2(grid_w, frame_h)))
|
||||
download_clicked = true;
|
||||
if (ImGui::IsItemHovered())
|
||||
panel_tooltip(_u8L("Opens OrcaCloud in the Home page to browse for plugins."));
|
||||
}
|
||||
|
||||
const Section &user = sections.back();
|
||||
if (section_header(user.key, user.title, user.entries.size(), 0.f, {}, user.title)) {
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const int count = grid(origin, true);
|
||||
const int count = grid(origin, user.entries, true);
|
||||
ImGui::SetCursorScreenPos(tile_pos(origin, count));
|
||||
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
|
||||
import_clicked = true;
|
||||
@@ -4273,17 +4569,25 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
"bakes from and saved to your personal texture folder, kept separate from the "
|
||||
"textures shipped with OrcaSlicer."));
|
||||
end_grid(origin, count + 1);
|
||||
// Belongs to this section: it describes the import tile and the remove-on-hover tiles, both of
|
||||
// which live here, so it is hidden with them rather than left describing controls off-screen.
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
|
||||
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
|
||||
m_picker_reveal_selected = false;
|
||||
ImGui::EndChild();
|
||||
pop_slim_scrollbar_style();
|
||||
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
|
||||
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (chosen || import_clicked)
|
||||
if (chosen || import_clicked || close_clicked || download_clicked)
|
||||
ImGui::CloseCurrentPopup();
|
||||
m_tooltip_anchor_min = prev_anchor_min;
|
||||
m_tooltip_anchor_max = prev_anchor_max;
|
||||
ImGui::EndPopup();
|
||||
// The OrcaCloud plugin hub filtered to resource textures; the pane's session handoff redirects here.
|
||||
if (download_clicked)
|
||||
wxGetApp().open_orca_cloud("/app/plugins/plugin-hub?types=resource-textures");
|
||||
|
||||
// Only once the loop over the library is done: importing, picking (which reorders the recently used)
|
||||
// and removing all change the library the loop was iterating.
|
||||
@@ -6586,19 +6890,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->text(text);
|
||||
m_imgui->pop_bold_font();
|
||||
};
|
||||
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
|
||||
const auto ellipsize = [](std::string s, float max_w) {
|
||||
if (ImGui::CalcTextSize(s.c_str()).x <= max_w)
|
||||
return s;
|
||||
while (!s.empty() && ImGui::CalcTextSize((s + "...").c_str()).x > max_w) {
|
||||
unsigned char c;
|
||||
do { // a whole code point at a time
|
||||
c = static_cast<unsigned char>(s.back());
|
||||
s.pop_back();
|
||||
} while (!s.empty() && (c & 0xC0) == 0x80);
|
||||
}
|
||||
return s + "...";
|
||||
};
|
||||
// Label column + a slider filling the rest of the row, value printed inside the track.
|
||||
// `right_inset` keeps a layer card's inner padding.
|
||||
const auto slider_label = [&](const wxString &label) {
|
||||
@@ -6791,52 +7082,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
style.WindowPadding.y - style.ItemSpacing.y);
|
||||
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
|
||||
|
||||
// ImGui's stock scrollbar is a wide, square-cornered slab in a tinted track - against this flat panel
|
||||
// it reads as a raw widget bolted onto the edge. Slim it to a rounded thumb over an invisible track.
|
||||
const float scrollbar_w = m_imgui->scaled(0.5f);
|
||||
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
// NoScrollWithMouse: the wheel is handled below so the scroll can be eased instead of teleporting
|
||||
// five text lines per notch, which on blocks this tall lost the reader's place.
|
||||
push_slim_scrollbar_style(m_imgui->scaled(0.5f), dark);
|
||||
// NoScrollWithMouse: ease_wheel_scroll() eases the scroll instead of teleporting five text lines per
|
||||
// notch, which on blocks this tall lost the reader's place.
|
||||
ImGui::BeginChild("##td_body", ImVec2(panel_w, body_h), false, ImGuiWindowFlags_NoScrollWithMouse);
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
const float scroll_now = ImGui::GetScrollY();
|
||||
const float scroll_max = ImGui::GetScrollMaxY();
|
||||
|
||||
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
|
||||
// content shrinking under a clamped offset - has to re-seed the target, or the easing below would
|
||||
// immediately drag the view back to where it last animated to.
|
||||
if (m_panel_scroll_applied < 0.f || std::abs(scroll_now - m_panel_scroll_applied) > 0.5f)
|
||||
m_panel_scroll_target = scroll_now;
|
||||
|
||||
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
|
||||
m_panel_scroll_target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
|
||||
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
|
||||
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
|
||||
m_panel_scroll_target = std::floor(std::clamp(m_panel_scroll_target, 0.f, scroll_max));
|
||||
|
||||
const float delta = m_panel_scroll_target - scroll_now;
|
||||
if (std::abs(delta) >= 1.f) {
|
||||
// Exponential ease, formulated against the frame time so the glide takes the same wall time
|
||||
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
|
||||
// away, so land on the target outright once the remainder is that small.
|
||||
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
|
||||
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
|
||||
const float next = std::floor(scroll_now + step);
|
||||
ImGui::SetScrollY(next);
|
||||
m_panel_scroll_applied = next;
|
||||
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
|
||||
} else {
|
||||
m_panel_scroll_applied = scroll_now;
|
||||
}
|
||||
}
|
||||
ease_wheel_scroll(m_panel_scroll_target, m_panel_scroll_applied);
|
||||
|
||||
// Everything that changes the layer list is deferred until the list is no longer being drawn:
|
||||
// removing or reordering shifts mv->texture_displacement_layers under the pointers the loop holds,
|
||||
@@ -7081,8 +7331,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
// Opens the texture library for a layer, making it the active one too (after the loop).
|
||||
const auto open_picker = [&](int slot) {
|
||||
m_picker_slot = slot;
|
||||
m_picker_open_request = true;
|
||||
m_picker_slot = slot;
|
||||
m_picker_open_request = true;
|
||||
m_picker_reveal_selected = true;
|
||||
};
|
||||
// The six-dot drag icon, drawn into `list` over [x, x + w] x [y, y + h].
|
||||
const auto draw_drag_icon = [&](ImDrawList *list, float x, float y, float w, float h, ImU32 tint) {
|
||||
@@ -8077,8 +8328,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// height of everything above, whether or not it is scrolled.
|
||||
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(2);
|
||||
pop_slim_scrollbar_style();
|
||||
|
||||
// The layer-list changes deferred from the loop above.
|
||||
if (slot_to_remove >= 0)
|
||||
|
||||
@@ -428,6 +428,10 @@ private:
|
||||
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
|
||||
int m_picker_slot = -1;
|
||||
bool m_picker_open_request = false;
|
||||
// Set when the popup opens, so the next layout scrolls the layer's current texture into view.
|
||||
bool m_picker_reveal_selected = false;
|
||||
float m_picker_scroll_target = 0.f; // see ease_wheel_scroll()
|
||||
float m_picker_scroll_applied = -1.f;
|
||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
||||
|
||||
@@ -1062,6 +1066,8 @@ private:
|
||||
|
||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||
std::map<std::string, LibraryTexture> m_library_textures;
|
||||
// Generation of the library the above were cached against; a change clears them.
|
||||
uint64_t m_library_generation = 0;
|
||||
|
||||
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
|
||||
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
|
||||
@@ -1103,12 +1109,15 @@ private:
|
||||
// The config key behind m_undocked.
|
||||
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
|
||||
|
||||
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
||||
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
||||
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
|
||||
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
|
||||
// Smooth scrolling for a child window, called inside it: the panel body (everything between the header
|
||||
// and the pinned Bake footer) and the texture library's grid. ImGui jumps a fixed number of lines per
|
||||
// wheel notch, which on tall layer cards reads as a hard jolt rather than a scroll. The child sets
|
||||
// ImGuiWindowFlags_NoScrollWithMouse, and the wheel moves `target` instead; the real scroll is eased
|
||||
// toward it over the following frames. `applied` is what the easing wrote last frame, <0 until the
|
||||
// first one.
|
||||
void ease_wheel_scroll(float &target, float &applied);
|
||||
float m_panel_scroll_target = 0.f;
|
||||
float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
|
||||
float m_panel_scroll_applied = -1.f;
|
||||
// Last frame's body content height and footer height. The body is a child window that has to be
|
||||
// given its height before its content is laid out, so it is sized from the previous frame: as tall
|
||||
// as its content, capped so the footer still fits above the bottom of the canvas.
|
||||
|
||||
@@ -505,10 +505,12 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt)
|
||||
io.MouseDown[0] = evt.LeftIsDown();
|
||||
io.MouseDown[1] = evt.RightIsDown();
|
||||
io.MouseDown[2] = evt.MiddleIsDown();
|
||||
float wheel_delta = static_cast<float>(evt.GetWheelDelta());
|
||||
if (wheel_delta != 0.0f) {
|
||||
// Only a vertical wheel event with a direction is a wheel step. A macOS trackpad reports sideways motion
|
||||
// as separate horizontal events, and its wheel events often carry a rotation of 0: wx truncates the
|
||||
// sub-pixel deltas to whole ones and passes on events with no delta at all. Read as vertical steps,
|
||||
// either kind scrolls against the gesture, and scrolling up bounces back and forth.
|
||||
if (evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL && evt.GetWheelRotation() != 0)
|
||||
io.MouseWheel = evt.GetWheelRotation() > 0 ? 1.f : -1.f;
|
||||
}
|
||||
unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0);
|
||||
m_mouse_buttons = buttons;
|
||||
|
||||
|
||||
@@ -6071,8 +6071,6 @@ void Sidebar::on_bed_type_change(BedType bed_type)
|
||||
* - Map key encoding:
|
||||
* - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary)
|
||||
* - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary)
|
||||
* - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane
|
||||
* count across units instead of ams_id * 4 + slot_id
|
||||
* - The 0x10000 flag indicates the main/right extruder
|
||||
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
|
||||
*
|
||||
@@ -6138,26 +6136,13 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
return std::string();
|
||||
};
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
auto list = obj->GetFilaSystem()->GetAmsList();
|
||||
for (auto ams : list) {
|
||||
int ams_id = std::stoi(ams.first);
|
||||
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
|
||||
// Bambu units are four slots wide; a generic unit reports its own lane count and its trays
|
||||
// carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units
|
||||
// numerically to keep this the same index the parser and the agent use.
|
||||
int generic_tray_base = 0;
|
||||
if (!is_bbl_vendor) {
|
||||
for (const auto& other : list) {
|
||||
if (std::stoi(other.first) < ams_id)
|
||||
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
|
||||
}
|
||||
}
|
||||
for (auto tray : ams.second->GetTrays()) {
|
||||
int slot_id = std::stoi(tray.first);
|
||||
const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id;
|
||||
filament_ams_list.emplace(extruder + tray_index,
|
||||
filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id),
|
||||
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
#include "slic3r/GUI/CalibrationWizardPage.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
@@ -36,12 +34,14 @@
|
||||
#include <wx/image.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/statbox.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/wupdlock.h>
|
||||
#include <wx/menu.h>
|
||||
#include <wx/odcombo.h>
|
||||
@@ -69,7 +69,6 @@
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ParamsDialog.hpp"
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
@@ -85,24 +84,6 @@
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static FilamentColorPickerValue filament_color_picker_value(const DynamicPrintConfig& config, std::size_t index)
|
||||
{
|
||||
FilamentColorPickerValue value;
|
||||
if (const auto* primary = config.opt<ConfigOptionStrings>("filament_colour"); primary && index < primary->values.size())
|
||||
value.primary = primary->values[index];
|
||||
if (const auto* multi = config.opt<ConfigOptionStrings>("filament_multi_colour"); multi && index < multi->values.size()) {
|
||||
std::istringstream stream(multi->values[index]);
|
||||
for (std::string color; stream >> color;)
|
||||
value.colors.push_back(std::move(color));
|
||||
}
|
||||
if (value.colors.empty() && !value.primary.empty())
|
||||
value.colors.push_back(value.primary);
|
||||
if (const auto* types = config.opt<ConfigOptionStrings>("filament_colour_type"); types && index < types->values.size())
|
||||
value.gradient = value.colors.size() > 1 && types->values[index] == "0";
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
#define BORDER_W 10
|
||||
|
||||
// ---------------------------------
|
||||
@@ -930,37 +911,31 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset
|
||||
FilamentColor fila_color = get_cur_color_info();
|
||||
|
||||
// Show filament picker dialog
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type,
|
||||
filament_color_picker_value(m_preset_bundle->project_config, m_filament_idx));
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type);
|
||||
|
||||
if (!dialog.IsDataLoaded()) {
|
||||
// If FilamentPicker fails, fallback to default color picker
|
||||
show_default_color_picker();
|
||||
} else if (dialog.ShowModal() == wxID_OK && dialog.HasSelectionChanged()) {
|
||||
if (const auto& custom = dialog.GetCustomColorSelection()) {
|
||||
const auto value = filament_color_picker_result(*custom);
|
||||
sync_colour_config(value.colors, value.gradient);
|
||||
} else if (dialog.ShowModal() == wxID_OK) {
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else {
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
}
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1116,8 +1091,30 @@ bool PlaterPresetComboBox::switch_to_tab()
|
||||
|
||||
void PlaterPresetComboBox::change_extruder_color()
|
||||
{
|
||||
// The Linux sidebar menu edits the same ordered project color as its swatch.
|
||||
show_default_color_picker();
|
||||
// get current color
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour clr(colors->values[m_filament_idx]);
|
||||
if (!clr.IsOk())
|
||||
clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
auto data = new wxColourData();
|
||||
data->SetChooseFull(1);
|
||||
data->SetColour(clr);
|
||||
|
||||
wxColourDialog dialog(this, data);
|
||||
dialog.CenterOnParent();
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
{
|
||||
colors->values[m_filament_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
|
||||
DynamicPrintConfig cfg_new = *cfg;
|
||||
cfg_new.set_key_value("filament_colour", colors);
|
||||
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new);
|
||||
this->update();
|
||||
wxGetApp().plater()->on_config_change(cfg_new);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaterPresetComboBox::show_add_menu()
|
||||
@@ -1611,12 +1608,19 @@ FilamentColor PlaterPresetComboBox::get_cur_color_info()
|
||||
|
||||
void PlaterPresetComboBox::show_default_color_picker()
|
||||
{
|
||||
const auto value = filament_color_picker_value(wxGetApp().preset_bundle->project_config, m_filament_idx);
|
||||
const auto initial = filament_color_picker_initial(value);
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(value), clr_picker);
|
||||
if (dialog.ShowModal() == wxID_OK && filament_color_picker_changed(initial, dialog.selection())) {
|
||||
const auto selected = filament_color_picker_result(*dialog.selection());
|
||||
sync_colour_config(selected.colors, selected.gradient);
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour current_clr(colors->values[m_filament_idx]);
|
||||
if (!current_clr.IsOk())
|
||||
current_clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
m_clrData.SetColour(current_clr);
|
||||
|
||||
wxColourData data = show_sys_picker_dialog(this, m_clrData);
|
||||
if (m_clrData.GetColour() != data.GetColour()) {
|
||||
std::vector<std::string> color = {data.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
m_clrData.SetColour(data.GetColour());
|
||||
sync_colour_config(color, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/clrpicker.h>
|
||||
@@ -200,6 +201,7 @@ public:
|
||||
|
||||
// BBS
|
||||
wxButton* clr_picker { nullptr };
|
||||
wxColourData m_clrData;
|
||||
|
||||
wxColor get_color() { return m_color; }
|
||||
|
||||
|
||||
+248
-824
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
#ifndef slic3r_StatusPanel_hpp_
|
||||
#define slic3r_StatusPanel_hpp_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
#include "MonitorPage.hpp"
|
||||
@@ -68,7 +66,6 @@
|
||||
#include "StagedBuild.hpp"
|
||||
|
||||
class StepIndicator;
|
||||
class wxChoice;
|
||||
|
||||
#define COMMAND_TIMEOUT 5
|
||||
|
||||
@@ -143,36 +140,28 @@ class ExtruderImage : public wxWindow
|
||||
ScalableBitmap *m_left_extruder_active_empty;
|
||||
ScalableBitmap *m_left_extruder_unactive_filled;
|
||||
ScalableBitmap *m_left_extruder_unactive_empty;
|
||||
|
||||
ScalableBitmap *m_right_extruder_active_filled;
|
||||
ScalableBitmap *m_right_extruder_active_empty;
|
||||
ScalableBitmap *m_right_extruder_unactive_filled;
|
||||
ScalableBitmap *m_right_extruder_unactive_empty;
|
||||
|
||||
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
|
||||
|
||||
|
||||
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_single_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
std::vector<ExtruderState> m_multi_extruder_states;
|
||||
bool m_generic_nozzle_display{false};
|
||||
|
||||
public:
|
||||
void update(int nozzle_num, int nozzle_id);
|
||||
void update(ExtruderState single_state);
|
||||
void update(ExtruderState right_state, ExtruderState left_state);
|
||||
|
||||
void update(ExtruderState state, int idx);
|
||||
|
||||
void msw_rescale();
|
||||
void updateGenericSize();
|
||||
void setExtruderCount(int nozzle_num);
|
||||
void setGenericNozzleDisplay(bool enabled);
|
||||
void setExtruderUsed(std::string loc);
|
||||
void setExtruderUsed(int nozzle_idx);
|
||||
void paintEvent(wxPaintEvent &evt);
|
||||
|
||||
void render(wxDC &dc);
|
||||
@@ -506,8 +495,6 @@ protected:
|
||||
std::vector<ExtruderImage *> m_extruderImage;
|
||||
|
||||
SwitchBoard * m_nozzle_btn_panel;
|
||||
wxChoice* m_generic_nozzle_selector{nullptr};
|
||||
int m_generic_nozzle_selector_count{0};
|
||||
|
||||
wxStaticText * m_text_tasklist_caption;
|
||||
|
||||
@@ -520,23 +507,10 @@ protected:
|
||||
wxBoxSizer * m_misc_ctrl_sizer;
|
||||
StaticBox* m_fan_panel;
|
||||
StaticLine * m_line_nozzle;
|
||||
wxWindowID m_nozzle_temp_control_id{wxID_ANY};
|
||||
wxWindow* m_temp_nozzle_parent{nullptr};
|
||||
wxBoxSizer* m_temp_nozzle_sizer{nullptr};
|
||||
size_t m_temp_nozzle_active_count{0};
|
||||
TempInput* m_tempCtrl_nozzle{nullptr};
|
||||
TempInput* m_tempCtrl_nozzle;
|
||||
int m_temp_nozzle_timeout{ 0 };
|
||||
TempInput* m_tempCtrl_nozzle_deputy{nullptr};
|
||||
TempInput* m_tempCtrl_nozzle_deputy;
|
||||
int m_temp_nozzle_deputy_timeout{ 0 };
|
||||
|
||||
std::vector<TempInput*> m_tempCtrl_nozzles;
|
||||
std::vector<int> m_temp_nozzle_timeouts;
|
||||
|
||||
// Last (dev_id, device toolhead count, preset nozzle count) we warned about, to avoid log spam.
|
||||
std::string m_last_mismatch_dev_id;
|
||||
int m_last_mismatch_device_count{-1};
|
||||
int m_last_mismatch_preset_count{-1};
|
||||
|
||||
TempInput * m_tempCtrl_bed;
|
||||
int m_temp_bed_timeout {0};
|
||||
TempInput * m_tempCtrl_chamber;
|
||||
@@ -576,7 +550,6 @@ protected:
|
||||
|
||||
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
|
||||
SwitchBoard* m_ams_rack_switch{ nullptr };
|
||||
MultiSwitchButton* m_ams_nozzle_switch{ nullptr };
|
||||
|
||||
AMSControl* m_ams_control;
|
||||
StaticBox* m_ams_control_box;
|
||||
@@ -651,9 +624,6 @@ public:
|
||||
|
||||
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
|
||||
wxBoxSizer *create_temp_control(wxWindow *parent);
|
||||
TempInput* create_nozzle_temp_control(wxWindow *parent, wxWindowID id);
|
||||
void set_temp_input_colors(TempInput* temp_ctrl);
|
||||
void ensure_nozzle_temp_controls(size_t count);
|
||||
wxBoxSizer *create_misc_control(wxWindow *parent);
|
||||
wxBoxSizer *create_axis_control(wxWindow *parent);
|
||||
wxPanel *create_bed_control(wxWindow *parent);
|
||||
@@ -684,7 +654,6 @@ private:
|
||||
friend class MonitorPanel;
|
||||
void wire_controls();
|
||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||
void sync_nozzle_temp_controls(size_t count);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||
@@ -832,7 +801,6 @@ protected:
|
||||
void update_cloud_subtask(MachineObject *obj);
|
||||
void update_sdcard_subtask(MachineObject *obj);
|
||||
void update_temp_ctrl(MachineObject *obj);
|
||||
void check_extruder_count_mismatch(MachineObject* obj);
|
||||
void update_misc_ctrl(MachineObject *obj);
|
||||
void update_ams(MachineObject* obj);
|
||||
void update_rack(MachineObject* obj);
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/weakref.h>
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
#include "Widgets/StaticLine.hpp"
|
||||
@@ -37,7 +32,9 @@
|
||||
#include <mutex>
|
||||
#include <wx/button.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/dialog.h>
|
||||
@@ -512,6 +509,30 @@ static wxString auto_mix_mode_label(TextureAutoMixMode mode)
|
||||
_L("One-click RYBW auto-mix");
|
||||
}
|
||||
|
||||
static wxPoint constrained_dialog_position(wxWindow* anchor, const wxSize& dialog_size)
|
||||
{
|
||||
if (!anchor)
|
||||
return wxDefaultPosition;
|
||||
|
||||
wxSize size = dialog_size;
|
||||
if (size.x <= 0 || size.y <= 0)
|
||||
size = wxSize(anchor->FromDIP(450), anchor->FromDIP(350));
|
||||
|
||||
wxPoint pos = anchor->ClientToScreen(wxPoint(0, anchor->GetSize().y));
|
||||
wxRect display_rect;
|
||||
int display_idx = wxDisplay::GetFromPoint(pos);
|
||||
if (display_idx != wxNOT_FOUND)
|
||||
display_rect = wxDisplay(display_idx).GetClientArea();
|
||||
else
|
||||
display_rect = wxDisplay().GetClientArea();
|
||||
|
||||
pos.x = std::clamp(pos.x, display_rect.GetLeft(),
|
||||
std::max(display_rect.GetLeft(), display_rect.GetRight() - size.x));
|
||||
pos.y = std::clamp(pos.y, display_rect.GetTop(),
|
||||
std::max(display_rect.GetTop(), display_rect.GetBottom() - size.y));
|
||||
return pos;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FilamentSelectPopup
|
||||
// ============================================================
|
||||
@@ -632,22 +653,24 @@ public:
|
||||
auto on_add_filament = m_on_add_filament;
|
||||
wxWindow* popup_parent = GetParent();
|
||||
wxWindow* color_anchor = m_dialog_anchor ? m_dialog_anchor : popup_parent;
|
||||
wxWeakRef<FilamentSelectPopup> popup(this);
|
||||
m_closing_from_action = true;
|
||||
std::optional<wxColour> selected;
|
||||
{
|
||||
// Capture the stable mapping row before the transient popup is dismissed.
|
||||
ColorPickerDialog dlg(popup_parent, ColorRGBA(0.f, 0.f, 0.f, 1.f), {}, false, color_anchor);
|
||||
if (popup) popup->Dismiss();
|
||||
// Dismissal can delete the popup. Only copied locals are used below.
|
||||
if (dlg.ShowModal() == wxID_OK && dlg.selection()) {
|
||||
const auto& color = std::get<ColorRGBA>(*dlg.selection());
|
||||
selected = wxColour(static_cast<unsigned char>(std::lround(color.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(color.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(color.b() * 255.f)));
|
||||
}
|
||||
Dismiss();
|
||||
wxColourData cd;
|
||||
cd.SetChooseFull(true);
|
||||
wxColourDialog dlg(popup_parent, &cd);
|
||||
auto move_color_dialog = [&dlg, color_anchor]() {
|
||||
dlg.Move(constrained_dialog_position(color_anchor, dlg.GetBestSize()));
|
||||
};
|
||||
dlg.Bind(wxEVT_SHOW, [move_color_dialog](wxShowEvent& e) mutable {
|
||||
e.Skip();
|
||||
if (e.IsShown())
|
||||
move_color_dialog();
|
||||
});
|
||||
move_color_dialog();
|
||||
if (dlg.ShowModal() == wxID_OK) {
|
||||
wxColour clr = dlg.GetColourData().GetColour();
|
||||
if (on_add_filament) on_add_filament(clr);
|
||||
}
|
||||
if (selected && on_add_filament) on_add_filament(*selected);
|
||||
});
|
||||
|
||||
m_content->SetSizer(outer);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TextureLibrary.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <cstddef>
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
@@ -21,6 +22,7 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <utility>
|
||||
#include <wx/image.h>
|
||||
@@ -36,6 +38,9 @@
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
#include "slic3r/plugin/PluginFsUtils.hpp"
|
||||
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
namespace {
|
||||
@@ -50,7 +55,7 @@ bool is_image_file(const boost::filesystem::path &path)
|
||||
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
|
||||
}
|
||||
|
||||
void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out)
|
||||
void scan_dir(const boost::filesystem::path &dir, TextureSource source, std::vector<TextureLibraryEntry> &out)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
if (!boost::filesystem::is_directory(dir, ec))
|
||||
@@ -60,12 +65,43 @@ void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<Text
|
||||
for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) {
|
||||
if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path()))
|
||||
continue;
|
||||
out.push_back({ it->path().stem().string(), it->path().string(), is_user });
|
||||
out.push_back({ it->path().stem().string(), it->path().string(), source });
|
||||
}
|
||||
std::sort(out.begin() + first, out.end(),
|
||||
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
|
||||
}
|
||||
|
||||
// Appends every loaded pack's textures, ordered by the pack's display name and then by file name. Each
|
||||
// entry carries the contributing pack's display name, which the picker uses to group a pack's textures
|
||||
// into one section; a pack name is unique among loaded packs of this capability type, so the name both
|
||||
// groups the section and keeps its entries contiguous. A pack whose directory fails validation is
|
||||
// skipped, so one bad pack never hides the others.
|
||||
void scan_plugin_textures(std::vector<TextureLibraryEntry> &out)
|
||||
{
|
||||
std::vector<TextureLibraryEntry> found;
|
||||
for (const ResourceTextureEntry &pack : PluginResourceTextures::instance().entries()) {
|
||||
std::string error;
|
||||
const std::vector<std::string> files =
|
||||
collect_resource_texture_files(pack.texture_dir, pack.plugin_root, error);
|
||||
if (!error.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Texture pack '" << pack.display_name << "' was skipped: " << error;
|
||||
continue;
|
||||
}
|
||||
for (const std::string &file : files) {
|
||||
const boost::filesystem::path path(file);
|
||||
found.push_back({ path.stem().string(), file, TextureSource::Plugin, pack.display_name });
|
||||
}
|
||||
}
|
||||
|
||||
std::stable_sort(found.begin(), found.end(), [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) {
|
||||
if (a.pack_name != b.pack_name)
|
||||
return a.pack_name < b.pack_name;
|
||||
return a.name < b.name;
|
||||
});
|
||||
for (TextureLibraryEntry &entry : found)
|
||||
out.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A
|
||||
// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of
|
||||
// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer
|
||||
@@ -208,6 +244,11 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
bool g_library_scanned = false;
|
||||
// The generation g_library was last built from, so texture_library() can rescan after a pack change.
|
||||
uint64_t g_library_scan_generation = 0;
|
||||
|
||||
// Bumped by plugin callbacks, read by the GUI frame. Atomic so a read never races a write.
|
||||
std::atomic<uint64_t> g_library_generation{ 0 };
|
||||
|
||||
// Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture -
|
||||
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
|
||||
@@ -255,6 +296,8 @@ const size_t RECENT_MAX = 200;
|
||||
std::vector<std::string> recent_texture_names()
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to read
|
||||
return names;
|
||||
if (wxGetApp().app_config == nullptr)
|
||||
return names;
|
||||
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
|
||||
@@ -269,6 +312,8 @@ std::vector<std::string> recent_texture_names()
|
||||
|
||||
void store_recent_texture_names(std::vector<std::string> names)
|
||||
{
|
||||
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to write
|
||||
return;
|
||||
if (wxGetApp().app_config == nullptr)
|
||||
return;
|
||||
if (names.size() > RECENT_MAX)
|
||||
@@ -279,8 +324,9 @@ void store_recent_texture_names(std::vector<std::string> names)
|
||||
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
|
||||
}
|
||||
|
||||
// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep
|
||||
// the name order scan_dir() gave them, after every used one.
|
||||
// Reorders the user's entries by recent use. Stable, so textures never used keep the name order
|
||||
// scan_dir() gave them, after every used one. Only the user's own range is touched: shipped and pack
|
||||
// entries must keep the order their scans produced.
|
||||
void sort_user_textures(std::vector<TextureLibraryEntry> &library)
|
||||
{
|
||||
const std::vector<std::string> names = recent_texture_names();
|
||||
@@ -291,8 +337,11 @@ void sort_user_textures(std::vector<TextureLibraryEntry> &library)
|
||||
const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
|
||||
return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
|
||||
};
|
||||
const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; });
|
||||
std::stable_sort(first_user, library.end(),
|
||||
const auto first_user = std::find_if(library.begin(), library.end(),
|
||||
[](const TextureLibraryEntry &e) { return e.source == TextureSource::User; });
|
||||
const auto user_end = std::find_if(first_user, library.end(),
|
||||
[](const TextureLibraryEntry &e) { return e.source != TextureSource::User; });
|
||||
std::stable_sort(first_user, user_end,
|
||||
[&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); });
|
||||
}
|
||||
|
||||
@@ -311,6 +360,16 @@ bool in_user_texture_dir(const std::string &path)
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t texture_library_generation()
|
||||
{
|
||||
return g_library_generation.load();
|
||||
}
|
||||
|
||||
void bump_texture_library_generation()
|
||||
{
|
||||
g_library_generation.fetch_add(1);
|
||||
}
|
||||
|
||||
std::string user_texture_dir()
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
|
||||
@@ -325,17 +384,20 @@ std::string user_texture_dir()
|
||||
|
||||
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
|
||||
{
|
||||
if (g_library_scanned && !force_rescan)
|
||||
const uint64_t generation = texture_library_generation();
|
||||
if (g_library_scanned && !force_rescan && generation == g_library_scan_generation)
|
||||
return g_library;
|
||||
|
||||
g_library.clear();
|
||||
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library);
|
||||
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", TextureSource::Shipped, g_library);
|
||||
const std::string user_dir = user_texture_dir();
|
||||
if (!user_dir.empty())
|
||||
scan_dir(boost::filesystem::path(user_dir), true, g_library);
|
||||
scan_dir(boost::filesystem::path(user_dir), TextureSource::User, g_library);
|
||||
sort_user_textures(g_library);
|
||||
scan_plugin_textures(g_library);
|
||||
|
||||
g_library_scanned = true;
|
||||
g_library_scan_generation = generation;
|
||||
g_library_scanned = true;
|
||||
return g_library;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef slic3r_TextureLibrary_hpp_
|
||||
#define slic3r_TextureLibrary_hpp_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -10,26 +11,45 @@
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// One selectable height-map texture in the texture-displacement gizmo's texture picker.
|
||||
// Where a library texture came from. The picker lists each source under its own heading.
|
||||
enum class TextureSource { Shipped, User, Plugin };
|
||||
|
||||
// One selectable height-map texture in the texture-displacement gizmo's texture picker. A texture that
|
||||
// came from a plugin pack also carries the provenance the picker groups its sections by.
|
||||
struct TextureLibraryEntry
|
||||
{
|
||||
std::string name; // display name (the file's stem, e.g. "Wood Grain")
|
||||
std::string path; // absolute path on disk
|
||||
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
|
||||
std::string name; // display name (the file's stem, e.g. "Wood Grain")
|
||||
std::string path; // absolute path on disk
|
||||
TextureSource source; // which of the three sources it came from
|
||||
|
||||
std::string pack_name; // contributing pack's display name; empty for Shipped and User
|
||||
};
|
||||
|
||||
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
|
||||
// by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see
|
||||
// touch_user_texture()), then any never used, by name.
|
||||
// Every available height-map texture, in the order the picker lists them: the ones shipped in
|
||||
// resources/textures/displacement first, by name; then the user's own from <data_dir>/textures/displacement,
|
||||
// most recently used first (see touch_user_texture()) and the rest by name; then the loaded texture
|
||||
// packs, ordered by pack display name and then by file name.
|
||||
//
|
||||
// The two live in separate directories deliberately: an app update replaces the resources tree
|
||||
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
|
||||
// delete. `is_user` is what the picker uses to show them under separate headings.
|
||||
// The shipped and user textures live in separate directories deliberately: an app update replaces the
|
||||
// resources tree wholesale, so anything the user imported has to sit somewhere that update can never
|
||||
// overwrite or delete. Pack textures are read-only like the shipped ones. Every texture carries its
|
||||
// `source`; a pack texture also carries `pack_name`, and the picker groups pack textures into one
|
||||
// collapsible section per pack name. A pack's name is unique among loaded packs of this capability type -
|
||||
// the loader refuses a collision with "Capability collision for type ... and name ...", see
|
||||
// PluginManager::check_registry_locked (PluginManager.cpp:211-225) - so the name on its own identifies a
|
||||
// section and keeps that pack's entries contiguous.
|
||||
//
|
||||
// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped
|
||||
// into either folder by hand while the app was running.
|
||||
// into either folder by hand while the app was running. A rescan also happens on its own when the pack
|
||||
// registry changes, which is what texture_library_generation() tracks.
|
||||
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
|
||||
|
||||
// Monotonic counter, bumped whenever the set of loaded texture packs changes. The GUI reads it to
|
||||
// decide whether texture_library() must rescan, and plugin callbacks bump it. Safe to read from any
|
||||
// thread at any time.
|
||||
uint64_t texture_library_generation();
|
||||
void bump_texture_library_generation();
|
||||
|
||||
// Moves one of the user's own textures to the front of the recently-used order texture_library() lists
|
||||
// them in, and stores that order in the app config so it survives a restart. No-op for anything that is
|
||||
// not in user_texture_dir().
|
||||
|
||||
@@ -451,16 +451,6 @@ void GuideFrame::on_profile_loaded()
|
||||
SaveProfileData();
|
||||
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
|
||||
m_profile_ready = true;
|
||||
notify_profile_loaded();
|
||||
}
|
||||
|
||||
// The data is often ready before the loading page has run its scripts, and a
|
||||
// call into a page without HandleStudio is lost, so the message waits for both.
|
||||
void GuideFrame::notify_profile_loaded()
|
||||
{
|
||||
if (!m_profile_ready || !m_page_ready)
|
||||
return;
|
||||
json res;
|
||||
res["command"] = "userguide_profile_load_finish";
|
||||
res["sequence_id"] = "10001";
|
||||
@@ -556,11 +546,7 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
wxString strCmd = j["command"];
|
||||
BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;Command:" << strCmd;
|
||||
|
||||
if (strCmd == "userguide_loading_page_ready") {
|
||||
m_page_ready = true;
|
||||
notify_profile_loaded();
|
||||
}
|
||||
else if (strCmd == "close_page") {
|
||||
if (strCmd == "close_page") {
|
||||
this->EndModal(wxID_CANCEL);
|
||||
}
|
||||
if (strCmd == "user_clause") {
|
||||
|
||||
@@ -102,7 +102,6 @@ public:
|
||||
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
|
||||
void init_guide_paths();
|
||||
void on_profile_loaded();
|
||||
void notify_profile_loaded();
|
||||
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
|
||||
bool BuildProfileDataFromPresetBundle();
|
||||
bool BuildProfileDataFromVendors();
|
||||
@@ -143,10 +142,6 @@ private:
|
||||
|
||||
//First Load
|
||||
bool bFirstComplete{false};
|
||||
// The loading page gets userguide_profile_load_finish once both are true;
|
||||
// every instance of it reports in, so the flag is never reset.
|
||||
bool m_profile_ready{false};
|
||||
bool m_page_ready{false};
|
||||
// Set once in the destructor. Read through `this` by the loading thread
|
||||
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
|
||||
// lambdas so they don't touch `this` after the object is freed.
|
||||
|
||||
@@ -537,7 +537,7 @@ void WebViewPanel::cloud_signed_out(const wxString& signout_url)
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString& page_url)
|
||||
{
|
||||
if (on) {
|
||||
// load_cloud_url records the origin from whatever it loads, including the plain cloud
|
||||
@@ -546,6 +546,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
create_cloud_browser();
|
||||
m_cloud_seed_url = url;
|
||||
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
|
||||
} else if (!page_url.empty()) {
|
||||
// The pane already holds a session, so an explicit page loads straight in; the handoff is
|
||||
// only needed to establish one, not to reach an ordinary page.
|
||||
load_cloud_url(page_url);
|
||||
} else if (!url.empty() && url != m_cloud_seed_url) {
|
||||
// Re-seed only when the Start URL actually changed — a login, an account switch or a
|
||||
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
|
||||
@@ -556,6 +560,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
load_cloud_after_signout(url);
|
||||
}
|
||||
m_cloud_browser->Show();
|
||||
// The homepage shell owns the nav highlight and the cloud-mode layout class. A native open
|
||||
// (not the page's own GotoCloud) must ask it to switch; the page's SetCloudMode no-ops if it
|
||||
// already did. OnDocumentLoaded re-applies this when the homepage was still loading.
|
||||
RunScript("SetCloudMode(true)");
|
||||
} else if (m_cloud_browser) {
|
||||
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
|
||||
// again (guide §10). Nothing is cleared here.
|
||||
|
||||
@@ -118,7 +118,10 @@ public:
|
||||
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
|
||||
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
|
||||
// whether it already has a session; empty falls back to the plain cloud page.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString());
|
||||
// `page_url` is a full {web} page URL: an already-open pane is navigated straight there,
|
||||
// without re-running the handoff, which is what an explicit "open this page" wants. Empty
|
||||
// keeps whatever page the pane holds.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString(), const wxString& page_url = wxString());
|
||||
|
||||
// True while the OrcaCloud pane exists and is visible.
|
||||
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
|
||||
|
||||
@@ -244,7 +244,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
|
||||
m_sizer_ams_body->Add(m_sizer_ams_area_right, wxALIGN_CENTER, 0);
|
||||
|
||||
m_sizer_body->Add(m_sizer_ams_items, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_ams_gap = m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_body->Add(m_sizer_ams_body, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_down_road, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_ams_option, 0, wxEXPAND, 0);
|
||||
@@ -371,7 +371,7 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (m_total_ext_count == 1){
|
||||
else{
|
||||
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
|
||||
if (id == ams_id){
|
||||
return true;
|
||||
@@ -379,10 +379,6 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
|
||||
@@ -523,7 +519,7 @@ void AMSControl::msw_rescale()
|
||||
m_button_ams_setting_press.msw_rescale();
|
||||
m_button_ams_setting->SetBitmap(m_button_ams_setting_normal.bmp());
|
||||
|
||||
if (m_extruder) m_extruder->msw_rescale();
|
||||
m_extruder->msw_rescale();
|
||||
|
||||
if (m_button_extruder_feed) m_button_extruder_feed->Rescale(); // ORCA
|
||||
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
|
||||
@@ -607,8 +603,6 @@ void AMSControl::CreateAms()
|
||||
|
||||
|
||||
void AMSControl::ClearAms() {
|
||||
m_sizer_ams_items->Clear(false);
|
||||
|
||||
m_simplebook_ams_right->DeleteAllPages();
|
||||
m_simplebook_ams_left->DeleteAllPages();
|
||||
m_simplebook_ams_right->DestroyChildren();
|
||||
@@ -619,7 +613,7 @@ void AMSControl::ClearAms() {
|
||||
m_simplebook_ams_left->Refresh();
|
||||
|
||||
for (auto it : m_ams_preview_list) {
|
||||
if (it.second) it.second->Destroy();
|
||||
delete it.second;
|
||||
}
|
||||
m_ams_preview_list.clear();
|
||||
m_ext_image_list.clear();
|
||||
@@ -630,181 +624,12 @@ void AMSControl::ClearAms() {
|
||||
m_ams_item_list.clear();
|
||||
m_sizer_prv_right->Clear();
|
||||
m_sizer_prv_left->Clear();
|
||||
|
||||
if (m_nozzle_book) {
|
||||
m_nozzle_book->Destroy();
|
||||
m_nozzle_book = nullptr;
|
||||
} else {
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.preview_panel) pane.preview_panel->Destroy();
|
||||
if (pane.ams_book) pane.ams_book->Destroy();
|
||||
if (pane.page) pane.page->Destroy();
|
||||
}
|
||||
}
|
||||
m_item_ids.assign(std::max(m_total_ext_count, 2), {});
|
||||
m_nozzle_panes.clear();
|
||||
m_active_nozzle_id = 0;
|
||||
m_current_ams.clear();
|
||||
m_current_show_ams_left.clear();
|
||||
m_current_show_ams_right.clear();
|
||||
m_item_ids = { {}, {} };
|
||||
pair_id.clear();
|
||||
}
|
||||
|
||||
void AMSControl::restore_legacy_ams_layout()
|
||||
{
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxHORIZONTAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_ams_items->Add(m_panel_prv_left, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
m_sizer_ams_items->Add(m_panel_prv_right, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
|
||||
m_panel_prv_left->Show();
|
||||
m_panel_prv_right->Show();
|
||||
m_simplebook_ams_left->Show();
|
||||
m_simplebook_ams_right->Show();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_body->Show(m_sizer_ams_body, true);
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_down_road->SetSingleSideLayout(false);
|
||||
m_sizer_ams_gap->SetProportion(1);
|
||||
m_sizer_ams_gap->SetBorder(FromDIP(10));
|
||||
m_down_road->SetNozzleCount(m_total_ext_count);
|
||||
}
|
||||
|
||||
bool AMSControl::use_multi_nozzle_layout() const
|
||||
{
|
||||
const bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
return is_bbl ? m_total_ext_count > 2 : m_nozzle_book != nullptr;
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsMultiNozzle(const std::string &series_name, const std::string &printer_type) {
|
||||
const size_t pane_count = static_cast<size_t>(std::max(m_total_ext_count, 0));
|
||||
m_nozzle_panes.resize(pane_count);
|
||||
m_active_nozzle_id = 0;
|
||||
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxVERTICAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_body->Show(m_sizer_ams_body, false);
|
||||
m_sizer_ams_gap->SetProportion(0);
|
||||
m_sizer_ams_gap->SetBorder(0);
|
||||
|
||||
m_panel_prv_left->Hide();
|
||||
m_panel_prv_right->Hide();
|
||||
m_simplebook_ams_left->Hide();
|
||||
m_simplebook_ams_right->Hide();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_switcher->Hide();
|
||||
m_down_road->SetNozzleCount(1);
|
||||
m_down_road->SetSingleSideLayout(true, AMSPanelPos::LEFT_PANEL);
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_SINGLE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_extruder->updateNozzleNum(1, series_name);
|
||||
|
||||
m_nozzle_book = new wxSimplebook(m_amswin, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_nozzle_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
m_nozzle_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
m_sizer_ams_items->Add(m_nozzle_book, 0, wxALIGN_CENTER);
|
||||
|
||||
for (size_t nozzle_id = 0; nozzle_id < pane_count; ++nozzle_id) {
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
// A generic page is one H2C side. Keep the left-side geometry as the
|
||||
// canonical single-nozzle presentation; the road and AMS item code
|
||||
// still use the same side-specific paths as the legacy layout.
|
||||
pane.panel_pos = AMSPanelPos::LEFT_PANEL;
|
||||
pane.page = new wxPanel(m_nozzle_book, wxID_ANY);
|
||||
pane.page->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.page->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
pane.page_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
pane.page->SetSizer(pane.page_sizer);
|
||||
|
||||
pane.preview_panel = new wxScrolledWindow(pane.page, wxID_ANY);
|
||||
pane.preview_panel->SetScrollRate(10, 0);
|
||||
pane.preview_panel->SetSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetMinSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
|
||||
pane.preview_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.preview_panel->SetSizer(pane.preview_sizer);
|
||||
|
||||
// Generic lane cards grow to their calculated width for 1-8 lanes. Keep the
|
||||
// book at the legacy minimum, but allow the page to expand to that width.
|
||||
pane.ams_book = new wxSimplebook(pane.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
pane.ams_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.ams_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
// wxALIGN_CENTER does not centre horizontally inside a HORIZONTAL sizer, so a book wider
|
||||
// than pane.ams_area's own minimum would sit at ams_area's left edge and overflow to the
|
||||
// right, shifting the whole card column off the shared down-road connector axis. A pair of
|
||||
// stretch spacers keeps the book centred on the pane (and thus on the connector) at any
|
||||
// book width; ams_area is expanded below so the spacers have room to work with.
|
||||
pane.ams_area = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
pane.ams_area->Add(pane.ams_book, 0, wxALIGN_CENTER, 0);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
|
||||
pane.page_sizer->Add(pane.preview_panel, 0, wxALIGN_CENTER | wxTOP, FromDIP(5));
|
||||
pane.page_sizer->Add(pane.ams_area, 0, wxEXPAND | wxTOP, FromDIP(10));
|
||||
|
||||
m_nozzle_book->AddPage(pane.page, wxEmptyString, nozzle_id == 0);
|
||||
}
|
||||
|
||||
auto add_info = [this, series_name, printer_type, pane_count](AMSinfo info) {
|
||||
if (pane_count == 0) return;
|
||||
|
||||
info.nozzle_id = std::clamp(info.nozzle_id, 0, static_cast<int>(pane_count) - 1);
|
||||
AddAmsPreview(info, info.ams_type);
|
||||
if (info.ams_type == AMSModel::EXT_AMS && info.cans.size() == 1)
|
||||
AddAms({info}, series_name, printer_type, m_nozzle_panes[info.nozzle_id].panel_pos);
|
||||
else
|
||||
AddAms(info);
|
||||
};
|
||||
|
||||
for (const auto &info : m_ams_info)
|
||||
add_info(info);
|
||||
|
||||
for (auto &info : m_ext_info) {
|
||||
const int virtual_tray_id = std::atoi(info.ams_id.c_str());
|
||||
if (virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID &&
|
||||
virtual_tray_id >= VIRTUAL_TRAY_MAIN_ID - static_cast<int>(pane_count) + 1)
|
||||
info.nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
add_info(info);
|
||||
}
|
||||
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.ams_book && pane.ams_book->GetPageCount() > 0)
|
||||
pane.ams_book->SetSelection(0);
|
||||
if (pane.preview_sizer) pane.preview_sizer->Layout();
|
||||
if (pane.preview_panel) pane.preview_panel->FitInside();
|
||||
if (pane.ams_book) pane.ams_book->Layout();
|
||||
if (pane.page) pane.page->Layout();
|
||||
|
||||
if (!pane.current_ams.empty()) {
|
||||
auto preview = m_ams_preview_list.find(pane.current_ams);
|
||||
if (preview != m_ams_preview_list.end() && preview->second)
|
||||
preview->second->OnSelected();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_nozzle_book && pane_count > 0) {
|
||||
m_nozzle_book->SetSelection(0);
|
||||
m_current_ams = m_nozzle_panes[0].current_ams;
|
||||
if (!m_current_ams.empty())
|
||||
SwitchAms(m_current_ams);
|
||||
else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
}
|
||||
m_amswin->Layout();
|
||||
m_amswin->Fit();
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<AMSinfo> single_info_left;
|
||||
std::vector<AMSinfo> single_info_right;
|
||||
|
||||
@@ -926,8 +751,6 @@ void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std
|
||||
|
||||
void AMSControl::CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<int>m_item_nums{0,0};
|
||||
std::vector<AMSinfo> single_info;
|
||||
|
||||
@@ -1156,17 +979,6 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
if (m_ams_info[i].routes_to_main_extruder() != ams_info[i].routes_to_main_extruder()) {
|
||||
fresh = true;
|
||||
}
|
||||
if (m_ams_info[i].nozzle_id != ams_info[i].nozzle_id) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_ext_info.size()); ++i) {
|
||||
if (m_ext_info[i].ams_id != ext_info[i].ams_id ||
|
||||
m_ext_info[i].nozzle_id != ext_info[i].nozzle_id ||
|
||||
m_ext_info[i].ext_type != ext_info[i].ext_type) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
@@ -1181,16 +993,11 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
m_dev_id = dev_id;
|
||||
if (fresh){
|
||||
ClearAms();
|
||||
bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl) {
|
||||
if (m_total_ext_count == 1)
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
else if (m_total_ext_count == 2)
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
if (m_total_ext_count >= 2){
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}else{
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
}
|
||||
else
|
||||
CreateAmsMultiNozzle(series_name, printer_type);
|
||||
|
||||
SetSize(wxSize(FromDIP(578), -1));
|
||||
SetMinSize(wxSize(FromDIP(578), -1));
|
||||
Layout();
|
||||
@@ -1263,7 +1070,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
}
|
||||
|
||||
/*update ams extruder*/
|
||||
if (m_total_ext_count <= 2 && m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
if (m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
{
|
||||
m_amswin->Layout();
|
||||
}
|
||||
@@ -1299,15 +1106,7 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
{
|
||||
AMSPreview *ams_prv = nullptr;
|
||||
|
||||
// Match AddAms: the generic multi-nozzle layout's preview panel is the visible one for
|
||||
// non-Bambu vendors at any extruder count, not only above two.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
}
|
||||
else if (info.routes_to_main_extruder())
|
||||
if (info.routes_to_main_extruder())
|
||||
{
|
||||
ams_prv = new AMSPreview(m_panel_prv_right, wxID_ANY, info, type);
|
||||
m_sizer_prv_right->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
@@ -1330,54 +1129,6 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos) {
|
||||
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
|
||||
parent->InsertPage(idx, ams_item, wxEmptyString, true);
|
||||
|
||||
// Generic AMS cards may be wider than the legacy 264 DIP page. Propagate
|
||||
// that calculated width through the page and simplebook so 5-8 lanes are
|
||||
// laid out fully instead of being clipped by the old minimum width.
|
||||
if (info.ams_type == AMSModel::GENERIC_AMS) {
|
||||
const int required_width = ams_item->GetMinSize().x;
|
||||
if (required_width > 0) {
|
||||
const int page_width = std::max(parent->GetMinSize().x, required_width);
|
||||
parent->SetMinSize(wxSize(page_width, -1));
|
||||
if (auto *page = parent->GetParent()) {
|
||||
page->SetMinSize(wxSize(std::max(page->GetMinSize().x, page_width), -1));
|
||||
if (auto *nozzle_book = page->GetParent())
|
||||
nozzle_book->SetMinSize(wxSize(std::max(nozzle_book->GetMinSize().x, page_width), -1));
|
||||
}
|
||||
}
|
||||
// Above eight lanes, AmsItem is a fixed-minimum viewport over a wider
|
||||
// lane strip. Expand the viewport through its page's sizers rather
|
||||
// than propagating the strip's virtual width into the card/book.
|
||||
// Reset back to the centered fixed layout for eight or fewer lanes so a
|
||||
// later small unit is not left in expand mode.
|
||||
const bool generic_lane_layout = !wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (generic_lane_layout) {
|
||||
if (auto *page = parent->GetParent()) {
|
||||
auto pane = std::find_if(m_nozzle_panes.begin(), m_nozzle_panes.end(),
|
||||
[page](const NozzleAmsPane &candidate) { return candidate.page == page; });
|
||||
if (pane != m_nozzle_panes.end() && m_nozzle_book) {
|
||||
const bool expand_viewport = info.cans.size() > 8;
|
||||
if (auto *book_item = pane->ams_area->GetItem(pane->ams_book)) {
|
||||
book_item->SetProportion(expand_viewport ? 1 : 0);
|
||||
book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
}
|
||||
// Keep the centring spacers (items 0 and 2) elastic only in the centred layout;
|
||||
// in the >8-lane viewport they must collapse so the book can fill ams_area.
|
||||
if (auto *lead = pane->ams_area->GetItem(static_cast<size_t>(0)))
|
||||
lead->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *trail = pane->ams_area->GetItem(static_cast<size_t>(2)))
|
||||
trail->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *area_item = pane->page_sizer->GetItem(pane->ams_area))
|
||||
area_item->SetFlag(wxEXPAND | wxTOP);
|
||||
if (auto *nozzle_book_item = m_sizer_ams_items->GetItem(m_nozzle_book))
|
||||
nozzle_book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
if (auto *items_item = m_sizer_body->GetItem(m_sizer_ams_items))
|
||||
items_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
m_amswin->Layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ams_item->set_selection(idx);
|
||||
idx++;
|
||||
|
||||
@@ -1402,10 +1153,7 @@ AMSRoadShowMode AMSControl::findFirstMode(AMSPanelPos pos) {
|
||||
if (item->second->get_ams_model() == AMSModel::EXT_AMS && item->second->get_ext_type() == AMSModelOriginType::LITE_EXT) return AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
}
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM) {
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
}
|
||||
else {
|
||||
else{
|
||||
for (auto ids : pair_id){
|
||||
if (ids.first == ams_id || ids.second == ams_id){
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
@@ -1424,13 +1172,8 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
wxPanel* book_panel = new wxPanel(parent);
|
||||
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
// Size the page to the AMS book it becomes, not to the legacy fixed width: an AMS card is
|
||||
// added directly as the book page (so it always spans the book), while a single external
|
||||
// spool is wrapped here. Matching the book keeps the Ext card centred on the same axis as
|
||||
// the AMS cards and the shared down-road connector anchored below them.
|
||||
const wxSize book_panel_size(std::max(parent->GetMinSize().x, AMS_PANEL_SIZE.x), AMS_PANEL_SIZE.y);
|
||||
book_panel->SetSize(book_panel_size);
|
||||
book_panel->SetMinSize(book_panel_size);
|
||||
book_panel->SetSize(AMS_PANEL_SIZE);
|
||||
book_panel->SetMinSize(AMS_PANEL_SIZE);
|
||||
|
||||
AmsItem* ams1 = nullptr, * ams2 = nullptr;
|
||||
ams1 = new AmsItem(book_panel, infos[0], infos[0].ams_type, pos);
|
||||
@@ -1457,14 +1200,6 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
//book_sizer->Add(ams1, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, (book_panel->GetSize().x - ams1->GetSize().x) / 2);
|
||||
}
|
||||
else if (pos == AMSPanelPos::SINGLE_PANEL) {
|
||||
// Centre the single external-spool card explicitly, using the same fixed-margin
|
||||
// technique as the Lite-EXT branch above. Two stretch spacers centred the card
|
||||
// against the wrapper's own width rather than the AMS book, which left the card
|
||||
// (and the connector drawn below it) off the book/connector axis.
|
||||
book_sizer->Add(ams1, 0, wxLEFT,
|
||||
std::max(0, (book_panel_size.x - ams1->GetSize().x) / 2));
|
||||
}
|
||||
else{
|
||||
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, FromDIP(30));
|
||||
@@ -1493,20 +1228,7 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
|
||||
void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
{
|
||||
// The generic multi-nozzle layout (non-Bambu vendors) owns the only visible book; a unit must
|
||||
// land there for any extruder count. Keying off m_total_ext_count instead sent 1- and 2-extruder
|
||||
// generic printers into the legacy simplebooks that CreateAmsMultiNozzle hides.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
if (pane.current_ams.empty()) pane.current_ams = info.ams_id;
|
||||
createAms(pane.ams_book, pane.page_index, info, AMSPanelPos::SINGLE_PANEL);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2){
|
||||
if (m_total_ext_count > 1){
|
||||
if (info.routes_to_main_extruder()){
|
||||
createAms(m_simplebook_ams_right, m_right_page_index, info, AMSPanelPos::RIGHT_PANEL);
|
||||
}
|
||||
@@ -1517,9 +1239,6 @@ void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
else if (m_total_ext_count == 1){
|
||||
createAms(m_simplebook_ams_left, m_left_page_index, info, AMSPanelPos::LEFT_PANEL);
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
m_simplebook_ams_left->Refresh();
|
||||
@@ -1550,19 +1269,7 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
if (single_info.size() <= 0){
|
||||
return;
|
||||
}
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
}
|
||||
if (pane.current_ams.empty()) pane.current_ams = single_info.front().ams_id;
|
||||
createAmsPanel(pane.ams_book, pane.page_index, single_info, series_name, printer_type, AMSPanelPos::SINGLE_PANEL, m_total_ext_count);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2) {
|
||||
if (m_total_ext_count == 2) {
|
||||
if (single_info[0].routes_to_main_extruder()) {
|
||||
createAmsPanel(m_simplebook_ams_right, m_right_page_index, single_info, series_name, printer_type, AMSPanelPos::RIGHT_PANEL, m_total_ext_count);
|
||||
}
|
||||
@@ -1578,9 +1285,6 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
createAmsPanel(m_simplebook_ams_left, m_left_page_index, single_info, series_name, printer_type, AMSPanelPos::LEFT_PANEL, m_total_ext_count);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
@@ -1618,21 +1322,6 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos) {
|
||||
if (single_info.size() <= 0) return;
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
auto *ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, info.ams_type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
ams_prv->Bind(wxEVT_LEFT_DOWN, [this, ams_prv](wxMouseEvent &e) {
|
||||
SwitchAms(ams_prv->get_ams_id());
|
||||
e.Skip();
|
||||
});
|
||||
m_ams_preview_list[info.ams_id] = ams_prv;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
AMSPreview* ams_prv = nullptr;
|
||||
AMSPreview* ams_prv2 = nullptr;
|
||||
if (pos == AMSPanelPos::RIGHT_PANEL){
|
||||
@@ -1671,97 +1360,8 @@ void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos)
|
||||
}
|
||||
}
|
||||
|
||||
void AMSControl::SelectNozzle(int nozzle_id)
|
||||
{
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (m_active_nozzle_id == nozzle_id &&
|
||||
(!m_nozzle_book || m_nozzle_book->GetSelection() == nozzle_id))
|
||||
return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
m_current_ams = m_nozzle_panes[nozzle_id].current_ams;
|
||||
if (!m_current_ams.empty()) {
|
||||
SwitchAms(m_current_ams);
|
||||
return;
|
||||
}
|
||||
if (m_down_road) {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
}
|
||||
|
||||
void AMSControl::SwitchAms(std::string ams_id)
|
||||
{
|
||||
if (use_multi_nozzle_layout()) {
|
||||
NozzleAmsPane *pane = nullptr;
|
||||
int nozzle_id = -1;
|
||||
for (int index = 0; index < static_cast<int>(m_nozzle_panes.size()); ++index) {
|
||||
auto &candidate = m_nozzle_panes[index];
|
||||
if (std::find(candidate.item_ids.begin(), candidate.item_ids.end(), ams_id) != candidate.item_ids.end()) {
|
||||
pane = &candidate;
|
||||
nozzle_id = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pane) return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
|
||||
pane->current_ams = ams_id;
|
||||
m_current_ams = ams_id;
|
||||
m_current_select = ams_id;
|
||||
|
||||
for (const auto &id : pane->item_ids) {
|
||||
auto preview = m_ams_preview_list.find(id);
|
||||
if (preview == m_ams_preview_list.end() || !preview->second) continue;
|
||||
if (id == ams_id) preview->second->OnSelected();
|
||||
else preview->second->UnSelected();
|
||||
}
|
||||
|
||||
auto item = m_ams_item_list.find(ams_id);
|
||||
if (item != m_ams_item_list.end() && item->second) {
|
||||
if (pane->ams_book)
|
||||
pane->ams_book->SetSelection(item->second->get_selection());
|
||||
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (item->second->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->second->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (item->second->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(item->second->get_ams_model() == AMSModel::EXT_AMS &&
|
||||
item->second->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (m_down_road) {
|
||||
if (pane->panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
m_down_road->UpdatePassRoad(pane->panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
return;
|
||||
}
|
||||
|
||||
if(ams_id == m_current_show_ams_left || ams_id == m_current_show_ams_right){return;}
|
||||
|
||||
bool is_in_right = IsAmsInRightPanel(ams_id);
|
||||
@@ -1832,17 +1432,13 @@ void AMSControl::SwitchAms(std::string ams_id)
|
||||
else {
|
||||
AMSRoadShowMode mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
|
||||
if (item->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
|
||||
if (item->get_can_count() <= GENERIC_AMS_SLOT_NUM) {
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
}
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos == AMSPanelPos::LEFT_PANEL ? m_down_road->UpdateLeft(m_total_ext_count, mode)
|
||||
@@ -1904,20 +1500,6 @@ void AMSControl::SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std
|
||||
AmsItem *item = nullptr;
|
||||
if (m_ams_item_list.find(ams_id) != m_ams_item_list.end()) { item = m_ams_item_list[ams_id]; }
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (!m_extruder)
|
||||
return;
|
||||
|
||||
if (on_off && item)
|
||||
m_extruder->OnAmsLoading(true, 0, item->GetTagColr(slot_id));
|
||||
else
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (on_off && item) {
|
||||
auto col = item->GetTagColr(slot_id);
|
||||
m_extruder->OnAmsLoading(true, nozzle_id, col);
|
||||
@@ -2042,69 +1624,6 @@ void AMSControl::SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadTy
|
||||
else{
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(ams->get_nozzle_id(), 0, m_total_ext_count - 1);
|
||||
if (nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (ams->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (ams->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (ams->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (ams->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(ams->get_ams_model() == AMSModel::EXT_AMS && ams->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (can_index >= 0 && can_index < static_cast<int>(info.cans.size()) && m_down_road)
|
||||
m_down_road->SetPassRoadColour(pane.panel_pos == AMSPanelPos::LEFT_PANEL,
|
||||
info.cans[can_index].material_colour);
|
||||
if (m_down_road) {
|
||||
if (pane.panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
}
|
||||
|
||||
int road_length = 0;
|
||||
|
||||
if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_NONE) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP1) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (can_index >= 0 && can_index < info.cans.size())
|
||||
{
|
||||
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
|
||||
|
||||
@@ -47,20 +47,6 @@ public:
|
||||
void on_retry();
|
||||
|
||||
protected:
|
||||
struct NozzleAmsPane
|
||||
{
|
||||
wxPanel* page{nullptr};
|
||||
wxBoxSizer* page_sizer{nullptr};
|
||||
wxScrolledWindow* preview_panel{nullptr};
|
||||
wxBoxSizer* preview_sizer{nullptr};
|
||||
wxSimplebook* ams_book{nullptr};
|
||||
wxBoxSizer* ams_area{nullptr};
|
||||
AMSPanelPos panel_pos{AMSPanelPos::LEFT_PANEL};
|
||||
std::vector<std::string> item_ids;
|
||||
std::string current_ams;
|
||||
int page_index{0};
|
||||
};
|
||||
|
||||
std::string m_current_ams;
|
||||
std::string m_current_slot_left;
|
||||
std::string m_current_slot_right;
|
||||
@@ -77,9 +63,6 @@ protected:
|
||||
|
||||
std::string m_dev_id;
|
||||
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
|
||||
std::vector<NozzleAmsPane> m_nozzle_panes;
|
||||
wxSimplebook* m_nozzle_book{nullptr};
|
||||
int m_active_nozzle_id{0};
|
||||
std::vector<std::pair<std::string, std::string>> pair_id;
|
||||
|
||||
int m_total_ext_count = 1;
|
||||
@@ -120,7 +103,6 @@ protected:
|
||||
wxSimplebook *m_simplebook_ams_left{nullptr};
|
||||
wxSimplebook *m_simplebook_ams_right{ nullptr };
|
||||
wxSimplebook *m_simplebook_bottom{nullptr};
|
||||
wxSizerItem *m_sizer_ams_gap{nullptr};
|
||||
wxPanel *m_panel_down_road{ nullptr };
|
||||
int m_left_page_index = 0;
|
||||
int m_right_page_index = 0;
|
||||
@@ -174,7 +156,6 @@ public:
|
||||
void createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos);
|
||||
void createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSinfo> infos, const std::string &series_name, const std::string &printer_type, AMSPanelPos pos, int total_ext_num);
|
||||
AMSRoadShowMode findFirstMode(AMSPanelPos pos);
|
||||
void restore_legacy_ams_layout();
|
||||
|
||||
AMSModel m_ams_model{AMSModel::EXT_AMS};
|
||||
AMSModel m_ext_model{AMSModel::EXT_AMS};
|
||||
@@ -202,7 +183,6 @@ public:
|
||||
|
||||
void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void CreateAms();
|
||||
void CreateAmsMultiNozzle(const std::string& series_name, const std::string& printer_type);
|
||||
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
|
||||
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
|
||||
void ClearAms();
|
||||
@@ -230,8 +210,6 @@ public:
|
||||
void SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std::string slot_id);
|
||||
void SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void SwitchAms(std::string ams_id);
|
||||
void SelectNozzle(int nozzle_id);
|
||||
int GetActiveNozzleId() const { return m_active_nozzle_id; }
|
||||
|
||||
void msw_rescale();
|
||||
void on_filament_load(wxCommandEvent &event);
|
||||
@@ -256,11 +234,6 @@ public:
|
||||
private:
|
||||
std::string get_filament_id(const std::string& ams_id, const std::string& can_id);
|
||||
|
||||
// True when AMS units are laid out on the generic multi-nozzle book. Bambu
|
||||
// printers keep the legacy count-based single/double-nozzle layout at any
|
||||
// extruder count; other vendors use the book created for them.
|
||||
bool use_multi_nozzle_layout() const;
|
||||
|
||||
public:
|
||||
std::string m_current_select;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "CalibUtils.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -77,48 +76,6 @@ namespace Slic3r { namespace GUI {
|
||||
//#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), 144)
|
||||
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
|
||||
#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), FromDIP(174))
|
||||
|
||||
int generic_ams_layout_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(264);
|
||||
const int lane_width = window->FromDIP(52);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 4)
|
||||
return base_width;
|
||||
|
||||
// Lane centers are distributed as width * (index + 1) / (count + 1).
|
||||
// Keep at least one card width plus the requested gap between centers.
|
||||
return std::max(base_width, (lane_count + 1) * (lane_width + lane_gap));
|
||||
}
|
||||
|
||||
std::vector<int> generic_ams_lane_centers(int lane_count, int width)
|
||||
{
|
||||
std::vector<int> centers;
|
||||
if (lane_count <= 0)
|
||||
return centers;
|
||||
|
||||
centers.reserve(lane_count);
|
||||
for (int lane = 0; lane < lane_count; ++lane)
|
||||
centers.push_back(width * (lane + 1) / (lane_count + 1));
|
||||
return centers;
|
||||
}
|
||||
|
||||
int generic_ams_preview_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(52);
|
||||
const int lane_width = window->FromDIP(9);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 0)
|
||||
return base_width;
|
||||
|
||||
return std::max(base_width, lane_count * lane_width + (lane_count + 1) * lane_gap);
|
||||
}
|
||||
|
||||
bool use_generic_ams_layout(AMSModel model)
|
||||
{
|
||||
return model == AMSModel::GENERIC_AMS &&
|
||||
(!wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor());
|
||||
}
|
||||
bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, bool humidity_flag)
|
||||
{
|
||||
if (!ams) return false;
|
||||
@@ -213,14 +170,10 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
||||
info.can_id = std::to_string(0);
|
||||
this->cans.clear();
|
||||
|
||||
try {
|
||||
const int virtual_tray_id = std::stoi(tray.id);
|
||||
if (virtual_tray_id >= 0 && virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID)
|
||||
this->nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
}
|
||||
catch (...) {
|
||||
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
|
||||
this->nozzle_id = 0;
|
||||
}
|
||||
else if (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
this->nozzle_id = 1;
|
||||
|
||||
if (tray.is_tray_info_ready()) {
|
||||
info.ctype = tray.ctype;
|
||||
@@ -903,11 +856,7 @@ AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const
|
||||
|
||||
void AMSextruder::TurnOff()
|
||||
{
|
||||
if (m_left_extruder) m_left_extruder->TurnOff();
|
||||
if (m_right_extruder) m_right_extruder->TurnOff();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->TurnOff();
|
||||
}
|
||||
m_left_extruder->TurnOff();
|
||||
}
|
||||
|
||||
void AMSextruder::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, int nozzle_num)
|
||||
@@ -933,7 +882,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
m_left_extruder->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur_deputy = col;
|
||||
}
|
||||
else if (m_nozzle_num == 2){
|
||||
else if (m_nozzle_num > 1){
|
||||
if (nozzle_id == MAIN_EXTRUDER_ID) {
|
||||
m_right_extruder->OnAmsLoading(load, col);
|
||||
if (m_current_colur != col){
|
||||
@@ -947,15 +896,6 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(m_nozzle_extruders.size())) {
|
||||
m_nozzle_extruders[nozzle_id]->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur = col;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
/*return true if something is updated*/
|
||||
@@ -965,26 +905,12 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_series_name = series_name;
|
||||
m_nozzle_num = nozzle_num;
|
||||
this->DestroyChildren();
|
||||
m_left_extruder = nullptr;
|
||||
m_right_extruder = nullptr;
|
||||
m_nozzle_extruders.clear();
|
||||
|
||||
m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num == 1) {
|
||||
if (MachineObject::is_series_n(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
} else if (MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
} else {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
|
||||
m_left_extruder->setShowState(true);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
else if (m_nozzle_num == 2)
|
||||
wxBoxSizer* m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num >= 2)
|
||||
{
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "left_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(true);
|
||||
@@ -992,17 +918,25 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(2));
|
||||
m_bitmap_sizer->AddSpacer(2);
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
for (int nozzle_id = 0; nozzle_id < m_nozzle_num; ++nozzle_id) {
|
||||
auto *extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
extruder->setShowState(true);
|
||||
m_nozzle_extruders.push_back(extruder);
|
||||
m_bitmap_sizer->Add(extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generic N Nozzles
|
||||
if (MachineObject::is_series_n(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
}
|
||||
else if(MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(false);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(3));
|
||||
}
|
||||
|
||||
SetSizer(m_bitmap_sizer);
|
||||
@@ -1017,9 +951,6 @@ void AMSextruder::msw_rescale()
|
||||
//m_amsSextruder->msw_rescale();
|
||||
if (m_left_extruder) m_left_extruder->msw_rescale();
|
||||
if (m_right_extruder) m_right_extruder->msw_rescale();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->msw_rescale();
|
||||
}
|
||||
Layout();
|
||||
Update();
|
||||
Refresh();
|
||||
@@ -2192,10 +2123,7 @@ AMSRoadUpPart::AMSRoadUpPart(wxWindow* parent, wxWindowID id, AMSinfo info, AMSM
|
||||
m_ams_model = model;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
const int width = use_generic_ams_layout(m_ams_model)
|
||||
? generic_ams_layout_width(this, static_cast<int>(m_amsinfo.cans.size()))
|
||||
: FromDIP(264);
|
||||
create(parent, id, pos, wxSize(width, FromDIP(34)));
|
||||
create(parent, id, pos, wxSize(FromDIP(264), FromDIP(34)));
|
||||
}
|
||||
else{
|
||||
create(parent, id, pos, wxSize(FromDIP(78), FromDIP(34)));
|
||||
@@ -2307,38 +2235,6 @@ void AMSRoadUpPart::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
dc.SetBrush(wxBrush(*wxTRANSPARENT_BRUSH));
|
||||
|
||||
if (use_generic_ams_layout(m_ams_model)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
if (lane_count <= 0)
|
||||
return;
|
||||
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int height = FromDIP(21);
|
||||
for (const int lane_center : lane_centers)
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
|
||||
if (lane_centers.size() > 1)
|
||||
dc.DrawLine(lane_centers.front(), height, lane_centers.back(), height);
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
|
||||
if (m_load_step != AMSPassRoadSTEP::AMS_ROAD_STEP_NONE &&
|
||||
m_load_slot_index >= 0 && m_load_slot_index < lane_count) {
|
||||
const int lane_center = lane_centers[m_load_slot_index];
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_amsinfo.cans[m_load_slot_index].material_colour), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
dc.DrawLine(std::min(lane_center, size.x / 2), height,
|
||||
std::max(lane_center, size.x / 2), height);
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3)
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
}
|
||||
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(wxColour(194, 194, 194)));
|
||||
dc.DrawRectangle(size.x / 2 - FromDIP(14), height - FromDIP(5), FromDIP(28), FromDIP(10));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((m_ams_model == N3S_AMS || m_ams_model == EXT_AMS) && m_amsinfo.cans.size() != 4){
|
||||
dc.DrawLine(((float)size.x / 2), (0), ((float)size.x / 2), (size.y));
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 || m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3){
|
||||
@@ -2464,25 +2360,6 @@ void AMSRoadDownPart::UpdateRight(int nozzle_num, AMSRoadShowMode mode)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetNozzleCount(int nozzle_num)
|
||||
{
|
||||
const int new_nozzle_num = std::max(1, nozzle_num);
|
||||
if (m_nozzle_num == new_nozzle_num) return;
|
||||
|
||||
m_nozzle_num = new_nozzle_num;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetSingleSideLayout(bool enabled, AMSPanelPos pos)
|
||||
{
|
||||
if (m_single_side_layout == enabled && m_single_side_pos == pos)
|
||||
return;
|
||||
|
||||
m_single_side_layout = enabled;
|
||||
m_single_side_pos = pos;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::OnVamsLoading(bool load, wxColour col /*= AMS_CONTROL_GRAY500*/)
|
||||
{
|
||||
/*m_vams_loading = load;
|
||||
@@ -2556,14 +2433,8 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
/*if (m_road_color.Alpha() == 0) { dc.SetPen(wxPen(*wxWHITE, m_passroad_width, wxPENSTYLE_SOLID)); }
|
||||
else { dc.SetPen(wxPen(m_road_color, m_passroad_width, wxPENSTYLE_SOLID)); }*/
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
|
||||
auto xpos = left_nozzle_pos.x;
|
||||
if (m_single_side_layout) {
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
else if (
|
||||
(m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE ||
|
||||
m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE)){
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE){
|
||||
auto length = 50;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
|
||||
length = -13;
|
||||
@@ -2594,9 +2465,6 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
|
||||
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -2624,41 +2492,23 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(left_nozzle_pos.x, (size.y / 2), left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
dc.DrawLine(left_nozzle_pos.x + FromDIP(145), 0, left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_single_side_layout) {
|
||||
if (m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3) {
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_road_color[1]), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE){
|
||||
if (m_nozzle_num == 1) {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
const auto &nozzle_pos = m_single_side_layout && m_single_side_pos == AMSPanelPos::RIGHT_PANEL
|
||||
? right_nozzle_pos
|
||||
: left_nozzle_pos;
|
||||
dc.DrawLine(nozzle_pos.x, (size.y / 2), nozzle_pos.x, (size.y));
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 2) {
|
||||
/*dc.DrawLine(FromDIP(left_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(left_nozzle_pos.x), FromDIP(size.y));
|
||||
dc.DrawLine(FromDIP(right_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(right_nozzle_pos.x), FromDIP(size.y));*/
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
dc.DrawLine((right_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y));
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2683,16 +2533,10 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2700,16 +2544,10 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
else{
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2733,7 +2571,7 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP step) {
|
||||
if (m_nozzle_num == 2){
|
||||
if (m_nozzle_num >= 2){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL){
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
|
||||
m_left_road_length = len;;
|
||||
@@ -2745,7 +2583,7 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else if (m_nozzle_num == 1){
|
||||
else{
|
||||
if (pos == AMSPanelPos::LEFT_PANEL) {
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
|
||||
m_left_road_length = len;
|
||||
@@ -2759,9 +2597,6 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -2780,10 +2615,7 @@ AMSPreview::AMSPreview(wxWindow* parent, wxWindowID id, AMSinfo amsinfo, AMSMode
|
||||
{
|
||||
wxWindow::Create(parent, id, pos);
|
||||
if (itemType == AMSModel::GENERIC_AMS || itemType == AMSModel::AMS_LITE || itemType == AMSModel::N3F_AMS) {
|
||||
if (itemType == AMSModel::GENERIC_AMS && use_generic_ams_layout(itemType))
|
||||
create(parent, id, pos, wxSize(generic_ams_preview_width(this, static_cast<int>(amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y));
|
||||
else
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
}
|
||||
else {
|
||||
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
|
||||
@@ -2818,13 +2650,7 @@ void AMSPreview::UpdateInfo(AMSinfo amsinfo)
|
||||
m_amsinfo = amsinfo;
|
||||
m_ams_item_type = amsinfo.ams_type;
|
||||
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const wxSize generic_size(generic_ams_preview_width(this, static_cast<int>(m_amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
SetMinSize(AMS_PREV_FOUR_SIZE);
|
||||
SetMaxSize(AMS_PREV_FOUR_SIZE);
|
||||
} else {
|
||||
@@ -2932,41 +2758,8 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
|
||||
|
||||
auto left = 0;
|
||||
// Generic AMS previews use the same normalized lane centers as the card row and connector.
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int cube_width = FromDIP(9);
|
||||
const int cube_height = FromDIP(14);
|
||||
|
||||
for (int lane = 0; lane < lane_count; ++lane) {
|
||||
const Caninfo &can = m_amsinfo.cans[lane];
|
||||
const int cube_left = lane_centers[lane] - cube_width / 2;
|
||||
const int cube_top = (size.y - cube_height) / 2;
|
||||
wxRect rect(cube_left, cube_top, cube_width, cube_height);
|
||||
|
||||
if (can.material_colour.Alpha() == 0) {
|
||||
if (wxGetApp().dark_mode())
|
||||
dc.DrawBitmap(m_ts_bitmap_cube_dark.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube_dark.GetBmpHeight()) / 2);
|
||||
else
|
||||
dc.DrawBitmap(m_ts_bitmap_cube.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube.GetBmpHeight()) / 2);
|
||||
}
|
||||
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
dc.SetPen(wxPen(wxColor(0, 0, 0)));
|
||||
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1),
|
||||
rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
|
||||
}
|
||||
else {
|
||||
wxColour color = can.material_colour;
|
||||
change_the_opacity(color);
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(color));
|
||||
dc.DrawRoundedRectangle(rect, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// four slot
|
||||
else if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
//four slot
|
||||
if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
left = FromDIP(8);
|
||||
for (std::vector<Caninfo>::iterator iter = m_amsinfo.cans.begin(); iter != m_amsinfo.cans.end(); iter++) {
|
||||
|
||||
@@ -3365,19 +3158,10 @@ AmsItem::AmsItem(wxWindow *parent,AMSinfo info, AMSModel model, AMSPanelPos pos
|
||||
m_panel_pos = pos;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int width = use_generic_ams_layout(m_ams_model) && lane_count <= 8
|
||||
? generic_ams_layout_width(this, lane_count)
|
||||
: FromDIP(264);
|
||||
const wxSize generic_size(width, FromDIP(174));
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, generic_size);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
// For large lane counts the card is a viewport, not the lane strip. Let
|
||||
// its parent allocate the available width; the scrolled child retains
|
||||
// the wider virtual content size.
|
||||
if (!(use_generic_ams_layout(m_ams_model) && lane_count > 8))
|
||||
SetMaxSize(generic_size);
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_CANS_WINDOW_SIZE);
|
||||
SetSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMinSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMaxSize(AMS_CANS_WINDOW_SIZE);
|
||||
}
|
||||
else{
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_SINGLE_CAN_SIZE);
|
||||
@@ -3403,43 +3187,12 @@ void AmsItem::create(wxWindow *parent)
|
||||
if (m_ams_model != AMSModel::AMS_LITE) {
|
||||
sizer_can = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer_item = new wxBoxSizer(wxVERTICAL);
|
||||
const bool generic_layout = use_generic_ams_layout(m_ams_model);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int content_width = generic_layout ? generic_ams_layout_width(this, lane_count) : 0;
|
||||
const bool scroll_lanes = generic_layout && lane_count > 8;
|
||||
const auto lane_centers = generic_layout
|
||||
? generic_ams_lane_centers(lane_count, content_width)
|
||||
: std::vector<int>();
|
||||
|
||||
m_can_parent = this;
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll = new wxScrolledWindow(this, wxID_ANY);
|
||||
m_can_scroll->SetScrollRate(FromDIP(10), 0);
|
||||
m_can_scroll->SetMinSize(wxSize(FromDIP(264), FromDIP(143)));
|
||||
m_can_scroll->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
m_can_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
m_can_scroll->SetSizer(m_can_scroll_sizer);
|
||||
m_can_parent = m_can_scroll;
|
||||
}
|
||||
|
||||
int previous_right = 0;
|
||||
for (int can_index = 0; can_index < lane_count; ++can_index) {
|
||||
if (generic_layout) {
|
||||
const int left = lane_centers[can_index] - FromDIP(26);
|
||||
sizer_can->AddSpacer(std::max(0, left - previous_right));
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
previous_right = left + FromDIP(52);
|
||||
}
|
||||
else {
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
}
|
||||
auto it = m_info.cans.begin();
|
||||
for (; it != m_info.cans.end(); it++) {
|
||||
AddCan(*it, m_can_count, m_info.cans.size(), sizer_can);
|
||||
m_can_count++;
|
||||
}
|
||||
if (generic_layout)
|
||||
sizer_can->AddSpacer(std::max(0, content_width - previous_right));
|
||||
if (generic_layout)
|
||||
sizer_can->SetMinSize(wxSize(content_width, -1));
|
||||
|
||||
it = m_info.cans.begin();
|
||||
//auto road_panel = new wxWindow(this, wxID_ANY);
|
||||
//auto road_panel = new wxPanel(this, wxID_ANY);
|
||||
//road_panel->SetSize(AMS_CAN_ROAD_SIZE);
|
||||
@@ -3454,18 +3207,11 @@ void AmsItem::create(wxWindow *parent)
|
||||
sizer_item->Add(m_ext_image, 0, wxALIGN_CENTER, 0);
|
||||
}
|
||||
}
|
||||
m_panel_road = new AMSRoadUpPart(m_can_parent, wxID_ANY, m_info, m_ams_model);
|
||||
m_panel_road = new AMSRoadUpPart(this, wxID_ANY, m_info, m_ams_model);
|
||||
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll_sizer->Add(sizer_can, 0, wxALIGN_LEFT, 0);
|
||||
m_can_scroll_sizer->Add(m_panel_road, 0, wxALIGN_LEFT, 0);
|
||||
sizer_item->Add(m_can_scroll, 1, wxEXPAND);
|
||||
}
|
||||
else {
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
}
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
|
||||
SetSizer(sizer_item);
|
||||
}
|
||||
@@ -3489,17 +3235,13 @@ void AmsItem::create(wxWindow *parent)
|
||||
|
||||
Layout();
|
||||
Fit();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
Thaw();
|
||||
//Refresh();
|
||||
}
|
||||
|
||||
void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer)
|
||||
{
|
||||
auto amscan = new wxWindow(m_can_parent ? m_can_parent : this, wxID_ANY);
|
||||
auto amscan = new wxWindow(this, wxID_ANY);
|
||||
|
||||
amscan->SetSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
amscan->SetMinSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
@@ -3580,8 +3322,7 @@ void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* size
|
||||
amscan->Layout();
|
||||
amscan->Fit();
|
||||
|
||||
const int margin = use_generic_ams_layout(m_ams_model) ? 0 : FromDIP(5);
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, margin);
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, FromDIP(5));
|
||||
|
||||
m_can_lib_list[caninfo.can_id] = m_panel_lib;
|
||||
//m_can_road_list[caninfo.can_id] = m_panel_road;
|
||||
@@ -3706,10 +3447,6 @@ void AmsItem::UpdateInfo(AMSinfo info)
|
||||
}
|
||||
|
||||
Layout();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
}
|
||||
|
||||
void AmsItem::SetDefSelectCan()
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" // Orca: DevFilaSwitch::SwitchPos for inlet-aware AMS placement
|
||||
|
||||
@@ -97,7 +96,6 @@ enum class AMSRoadShowMode : int {
|
||||
AMS_ROAD_MODE_SINGLE,
|
||||
AMS_ROAD_MODE_SINGLE_N3S,
|
||||
AMS_ROAD_MODE_AMS_LITE,
|
||||
AMS_ROAD_MODE_GENERIC,
|
||||
AMS_ROAD_MODE_NONE
|
||||
};
|
||||
|
||||
@@ -482,7 +480,6 @@ public:
|
||||
//AMSextruderImage *m_amsSextruder{nullptr};
|
||||
AMSextruderImage* m_left_extruder = nullptr;
|
||||
AMSextruderImage* m_right_extruder = nullptr;
|
||||
std::vector<AMSextruderImage*> m_nozzle_extruders;
|
||||
AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
|
||||
~AMSextruder();
|
||||
|
||||
@@ -684,8 +681,6 @@ public:
|
||||
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
|
||||
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
|
||||
void UpdateRight(int nozzle_num, AMSRoadShowMode mode);
|
||||
void SetNozzleCount(int nozzle_num);
|
||||
void SetSingleSideLayout(bool enabled, AMSPanelPos pos = AMSPanelPos::LEFT_PANEL);
|
||||
|
||||
void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
|
||||
void SetPassRoadColour(bool left, wxColour col);
|
||||
@@ -703,8 +698,6 @@ private:
|
||||
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
bool m_selected = {false};
|
||||
bool m_single_side_layout = {false};
|
||||
AMSPanelPos m_single_side_pos = {AMSPanelPos::LEFT_PANEL};
|
||||
|
||||
int m_left_road_length = {-1};
|
||||
int m_right_road_length = {-1};
|
||||
@@ -871,9 +864,6 @@ private:
|
||||
|
||||
std::map<std::string, AMSLib*> m_can_lib_list;
|
||||
//std::map<std::string, AMSRoad*> m_can_road_list;
|
||||
wxScrolledWindow* m_can_scroll = { nullptr };
|
||||
wxBoxSizer* m_can_scroll_sizer = { nullptr };
|
||||
wxWindow* m_can_parent = { nullptr };
|
||||
AMSRoadUpPart* m_panel_road = { nullptr };
|
||||
std::map<std::string, AMSrefresh*> m_can_refresh_list;
|
||||
AMSHumidity* m_humidity = { nullptr };
|
||||
|
||||
@@ -420,14 +420,6 @@ ExtruderBadge::ExtruderBadge(wxWindow* parent) : wxPanel(parent)
|
||||
|
||||
void ExtruderBadge::SetExtruderInfo(int extruder_id, const std::string& diameter, const NozzleVolumeType& volume_type)
|
||||
{
|
||||
// The badge renders exactly two extruders (left/right). A generic non-BBL printer with
|
||||
// N > 2 can report an out-of-range id here (see MultiNozzleStatusTable::UpdateRackInfo);
|
||||
// reject it rather than writing past the two-element lists.
|
||||
if (extruder_id < 0 || extruder_id >= static_cast<int>(m_diameter_list.size())) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": out-of-range extruder_id " << extruder_id;
|
||||
return;
|
||||
}
|
||||
|
||||
m_diameter_list[extruder_id] = diameter;
|
||||
m_volume_type_list[extruder_id] = volume_type;
|
||||
|
||||
@@ -875,21 +867,14 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
|
||||
bool has_right = false;
|
||||
for (auto& elem : nozzles_in_extruder) {
|
||||
auto& nozzle = elem.second;
|
||||
|
||||
int extruder_id{};
|
||||
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
}
|
||||
else
|
||||
extruder_id = nozzle.GetExtruderId();
|
||||
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
|
||||
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
|
||||
|
||||
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
|
||||
}
|
||||
// TODO: Update for N extruders
|
||||
m_badge->SetExtruderValid(has_right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +184,6 @@ void TempInput::SetFinish()
|
||||
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
|
||||
event.SetInt(temp_type);
|
||||
event.SetString(wxString::Format("%d", m_input_type));
|
||||
// N-extruder temp controls all share TEMP_OF_NORMAL_TYPE, so the string payload above can't
|
||||
// tell them apart; carry widget identity so the listener can find which one fired.
|
||||
event.SetEventObject(this);
|
||||
wxPostEvent(this->GetParent(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/object.h>
|
||||
#include <wx/popupwin.h>
|
||||
@@ -630,6 +630,43 @@ std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> c
|
||||
return color_info;
|
||||
}
|
||||
|
||||
wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data)
|
||||
{
|
||||
wxColourData data = clr_data;
|
||||
data.SetChooseFull(true);
|
||||
|
||||
// Load custom colors from config (support both "r,g,b,a" and "#RRGGBB" formats)
|
||||
std::vector<std::string> colors = Slic3r::GUI::wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < (int)colors.size(); i++) {
|
||||
wxColour c;
|
||||
if (colors[i].find(',') != std::string::npos)
|
||||
c = string_to_wxColor(colors[i]);
|
||||
else
|
||||
c = wxColour(colors[i]);
|
||||
if (c.IsOk())
|
||||
data.SetCustomColour(i, c);
|
||||
}
|
||||
|
||||
wxColourDialog dialog(parent, &data);
|
||||
dialog.SetTitle(_L("Please choose the filament color"));
|
||||
|
||||
if (dialog.ShowModal() == wxID_OK) {
|
||||
data = dialog.GetColourData();
|
||||
|
||||
// Save custom colors to config (use RGBA string format for consistency)
|
||||
std::vector<std::string> colors;
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
wxColour custom_clr = data.GetCustomColour(i);
|
||||
if (custom_clr.IsOk())
|
||||
colors[i] = color_to_string(custom_clr);
|
||||
}
|
||||
Slic3r::GUI::wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
|
||||
const std::vector<wxColour> *ramp){
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cstddef>
|
||||
#include <wx/checklst.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/combo.h>
|
||||
#include <wx/dataview.h>
|
||||
#include <wx/button.h>
|
||||
@@ -20,6 +21,7 @@
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/artprov.h>
|
||||
#include <wx/colordlg.h>
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
@@ -87,6 +89,7 @@ wxBitmap * get_extruder_color_icon(std::string color, std::string label, int ico
|
||||
wxBitmap * get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
|
||||
const std::vector<wxColour> *ramp = nullptr);
|
||||
std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> color_pack);
|
||||
wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data);
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cctype>
|
||||
#include <ios>
|
||||
@@ -62,42 +61,6 @@ namespace websocket = beast::websocket;
|
||||
namespace net = boost::asio;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
// Decimal value of a string that is nothing but digits, or -1 when it is empty or not a number.
|
||||
int decimal_number(const std::string& digits)
|
||||
{
|
||||
if (digits.empty() || !std::all_of(digits.begin(), digits.end(), [](unsigned char c) { return std::isdigit(c) != 0; }))
|
||||
return -1;
|
||||
try {
|
||||
return std::stoi(digits);
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Lane database keys address a lane either as "T<N>" (0-based tool index) or as "lane<N>"
|
||||
// (1-based, the AFC/Happy Hare style). Returns -1 when the key carries no usable lane number.
|
||||
int lane_index_from_key(const std::string& lane_key)
|
||||
{
|
||||
if (lane_key.size() > 1 && (lane_key[0] == 'T' || lane_key[0] == 't'))
|
||||
return decimal_number(lane_key.substr(1));
|
||||
if (lane_key.size() > 4 && lane_key.compare(0, 4, "lane") == 0) {
|
||||
const int lane_number = decimal_number(lane_key.substr(4));
|
||||
return lane_number > 0 ? lane_number - 1 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// AFC names toolheads "extruder", "extruder1", ... and derives the toolhead index from the name
|
||||
// itself (AFC_lane.lane_extruder_index): no suffix is 0, otherwise the suffix is the index.
|
||||
int extruder_index_from_name(const std::string& toolhead)
|
||||
{
|
||||
if (toolhead == "extruder")
|
||||
return 0;
|
||||
if (toolhead.size() > 8 && toolhead.compare(0, 8, "extruder") == 0)
|
||||
return decimal_number(toolhead.substr(8));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct WsEndpoint
|
||||
{
|
||||
std::string host;
|
||||
@@ -480,7 +443,19 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn)
|
||||
|
||||
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays)
|
||||
{
|
||||
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build BBL-format JSON for DevFilaSystemParser::ParseV1_0
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
|
||||
// Calculate ams_exist_bits and tray_exist_bits
|
||||
@@ -488,9 +463,11 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
unsigned long tray_exist_bits = 0;
|
||||
|
||||
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
|
||||
if (ams_id < 32) {
|
||||
ams_exist_bits |= (1UL << ams_id);
|
||||
}
|
||||
ams_exist_bits |= (1 << ams_id);
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
ams_unit["info"] = "0002"; // treat as AMS_LITE
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
|
||||
@@ -506,135 +483,43 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
}
|
||||
}
|
||||
|
||||
if (tray && tray->has_filament && slot_index < 32) {
|
||||
tray_exist_bits |= (1UL << slot_index);
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = std::to_string(slot_id);
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray && tray->has_filament) {
|
||||
tray_exist_bits |= (1 << slot_index);
|
||||
|
||||
tray_json["tray_info_idx"] = tray->tray_info_idx;
|
||||
tray_json["tray_type"] = tray->tray_type;
|
||||
tray_json["tray_color"] = normalize_color_value(tray->tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray->bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
|
||||
}
|
||||
if (tray->nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
}
|
||||
|
||||
static const AmsTrayData empty_tray;
|
||||
tray_array.push_back(make_ams_tray_json(std::to_string(slot_id), tray ? *tray : empty_tray));
|
||||
tray_array.push_back(tray_json);
|
||||
}
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
// Generic unit: type 1 (AMS) maps to AMSModel::GENERIC_AMS, which drives the
|
||||
// variable-lane rendering. This is not a real Bambu AMS-Lite.
|
||||
ams_unit["info"] = "0001";
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
|
||||
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector<AmsTrayData>& trays)
|
||||
{
|
||||
// One AMS box per extruder, best effort. Lane-data changers (AFC, recent Happy Hare) report an
|
||||
// extruder per lane but no physical box layout, so a box here is a logical feed group, not a
|
||||
// discovered unit. Lanes without an extruder fall back to the main extruder.
|
||||
std::map<int, std::vector<const AmsTrayData*>> boxes;
|
||||
for (const auto& tray : trays) {
|
||||
boxes[tray.extruder_id].push_back(&tray);
|
||||
}
|
||||
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
unsigned long ams_exist_bits = 0;
|
||||
unsigned long tray_exist_bits = 0;
|
||||
|
||||
int ams_id = 0;
|
||||
// Cumulative lane index across all boxes, in box then slot order. A lane maps to exactly one
|
||||
// extruder, so this is the lane's identity; the parser derives the same index for generic units.
|
||||
int lane_index = 0;
|
||||
for (auto& [extruder_id, lanes] : boxes) {
|
||||
if (ams_id < 32) {
|
||||
ams_exist_bits |= (1UL << ams_id);
|
||||
}
|
||||
|
||||
// Keep a deterministic slot order within the box.
|
||||
std::stable_sort(lanes.begin(), lanes.end(),
|
||||
[](const AmsTrayData* a, const AmsTrayData* b) { return a->slot_index < b->slot_index; });
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
for (size_t slot = 0; slot < lanes.size(); ++slot) {
|
||||
const AmsTrayData& tray = *lanes[slot];
|
||||
// A logical feed group is not a four-slot Bambu AMS: the bit is the lane's cumulative
|
||||
// index, not ams_id*4+slot. DevFilaSystemParser::ParseV1_0 reads the same index.
|
||||
const int bit = lane_index++;
|
||||
if (tray.has_filament && bit < 32) {
|
||||
tray_exist_bits |= (1UL << bit);
|
||||
}
|
||||
tray_array.push_back(make_ams_tray_json(std::to_string(slot), tray));
|
||||
}
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
// info: bits 0-3 = unit type (1 = AMS, i.e. AMSModel::GENERIC_AMS), bits 8-11 = bound
|
||||
// extruder id. A logical feed group is a generic unit, not a Bambu AMS-Lite.
|
||||
const int info_val = 1 | ((extruder_id & 0x0F) << 8);
|
||||
const char* hex_digits = "0123456789ABCDEF";
|
||||
std::string info = "0000";
|
||||
info[3] = hex_digits[info_val & 0x0F];
|
||||
info[2] = hex_digits[(info_val >> 4) & 0x0F];
|
||||
info[1] = hex_digits[(info_val >> 8) & 0x0F];
|
||||
info[0] = hex_digits[(info_val >> 12) & 0x0F];
|
||||
ams_unit["info"] = info;
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
++ams_id;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: Parsed " << trays.size()
|
||||
<< " trays in " << boxes.size() << " extruder box(es)";
|
||||
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
|
||||
}
|
||||
|
||||
nlohmann::json MoonrakerPrinterAgent::make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray)
|
||||
{
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = tray_id;
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray.has_filament) {
|
||||
tray_json["tray_info_idx"] = tray.tray_info_idx;
|
||||
tray_json["tray_type"] = tray.tray_type;
|
||||
tray_json["tray_color"] = normalize_color_value(tray.tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray.bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray.bed_temp);
|
||||
}
|
||||
if (tray.nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray.nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
}
|
||||
|
||||
return tray_json;
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits)
|
||||
{
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format as hex strings (matching BBL protocol)
|
||||
std::ostringstream ams_exist_ss;
|
||||
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
|
||||
std::ostringstream tray_exist_ss;
|
||||
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
|
||||
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
ams_json["ams"] = ams_array;
|
||||
ams_json["ams_exist_bits"] = ams_exist_ss.str();
|
||||
ams_json["tray_exist_bits"] = tray_exist_ss.str();
|
||||
@@ -645,6 +530,7 @@ void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array,
|
||||
|
||||
// Call the parser to populate DevFilaSystem
|
||||
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
|
||||
|
||||
// Set printer_type so update_sync_status() can match it against the preset's printer type.
|
||||
// Without this, the comparison fails and all sync badges are cleared.
|
||||
@@ -684,15 +570,17 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
|
||||
if (fetch_moonraker_filament_data(trays, max_lane_index)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
|
||||
<< (max_lane_index + 1) << " lanes";
|
||||
build_ams_payload_grouped(trays);
|
||||
int ams_count = (max_lane_index + 4) / 4;
|
||||
build_ams_payload(ams_count, max_lane_index, trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fall back to the Happy Hare `mmu` object for HH builds that do not publish lane_data
|
||||
// Attempt Happy Hare first (more widely adopted, supports more filament changers)
|
||||
if (fetch_hh_filament_info(trays, max_lane_index)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
|
||||
<< (max_lane_index + 1) << " gates";
|
||||
build_ams_payload_grouped(trays);
|
||||
int ams_count = (max_lane_index + 4) / 4;
|
||||
build_ams_payload(ams_count, max_lane_index, trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -798,30 +686,6 @@ int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char*
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MoonrakerPrinterAgent::safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value)
|
||||
{
|
||||
auto it = obj.find(key);
|
||||
if (it == obj.end())
|
||||
return default_value;
|
||||
if (it->is_number_integer())
|
||||
return it->get<int>();
|
||||
if (it->is_string()) {
|
||||
const std::string s = it->get<std::string>();
|
||||
if (s.empty())
|
||||
return default_value;
|
||||
try {
|
||||
std::size_t consumed = 0;
|
||||
const int value = std::stoi(s, &consumed);
|
||||
if (consumed != s.size()) // reject partial parses such as "2oops"
|
||||
return default_value;
|
||||
return value;
|
||||
} catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx)
|
||||
{
|
||||
if (arr.is_array() && idx >= 0 && idx < static_cast<int>(arr.size()) && arr[idx].is_string())
|
||||
@@ -925,39 +789,28 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
trays.clear();
|
||||
max_lane_index = 0;
|
||||
|
||||
// The lane_data namespace carries no extruder field; read the lane -> toolhead binding from the
|
||||
// AFC printer objects so lanes on different toolheads end up in different AMS boxes.
|
||||
std::map<std::string, int> lane_extruder;
|
||||
fetch_afc_lane_extruders(lane_extruder);
|
||||
|
||||
for (const auto& [lane_key, lane_obj] : value.items()) {
|
||||
if (!lane_obj.is_object()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lane tool number (0-based). AFC reports it as a string in the documented sample but it
|
||||
// can also be a number; accept either and skip lanes without a usable index. Databases
|
||||
// written without a "lane" field fall back to the key, which names the lane either as
|
||||
// "T<N>" (0-based) or as "lane<N>" (1-based).
|
||||
int lane_index = safe_json_int_flexible(lane_obj, "lane", -1);
|
||||
if (lane_index < 0) {
|
||||
lane_index = lane_index_from_key(lane_key);
|
||||
if (lane_index < 0) {
|
||||
continue;
|
||||
// Extract lane index from the "lane" field (tool number, 0-based)
|
||||
std::string lane_str = safe_json_string(lane_obj, "lane");
|
||||
int lane_index = -1;
|
||||
if (!lane_str.empty()) {
|
||||
try {
|
||||
lane_index = std::stoi(lane_str);
|
||||
} catch (...) {
|
||||
lane_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (lane_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = lane_index;
|
||||
// AFC exposes the lane's extruder/toolhead as "extruder_index"; tolerate "extruder_id" as
|
||||
// an alias. lane_data has neither, so fall back to the AFC lane objects and only then to
|
||||
// the main extruder (best effort).
|
||||
tray.extruder_id = safe_json_int_flexible(lane_obj, "extruder_index",
|
||||
safe_json_int_flexible(lane_obj, "extruder_id", -1));
|
||||
if (tray.extruder_id < 0) {
|
||||
const auto it = lane_extruder.find(lane_key);
|
||||
tray.extruder_id = it != lane_extruder.end() ? it->second : 0;
|
||||
}
|
||||
tray.tray_color = safe_json_string(lane_obj, "color");
|
||||
tray.tray_type = safe_json_string(lane_obj, "material");
|
||||
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
|
||||
@@ -1062,121 +915,52 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse gate data. Every gate becomes a lane so empty/unknown gates surface
|
||||
// as empty AMS slots rather than vanishing. HH is single-extruder, so all
|
||||
// lanes belong to feed group 0.
|
||||
// Parse gate data
|
||||
trays.clear();
|
||||
max_lane_index = 0;
|
||||
|
||||
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
|
||||
// Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available
|
||||
int status = safe_array_int(gate_status, gate_idx);
|
||||
if (status <= 0) {
|
||||
continue; // Skip unknown or empty gates
|
||||
}
|
||||
|
||||
// Extract gate data
|
||||
std::string material = safe_array_string(gate_material, gate_idx);
|
||||
std::string color = safe_array_string(gate_color, gate_idx);
|
||||
int nozzle_temp = safe_array_int(gate_temperature, gate_idx);
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = gate_idx;
|
||||
tray.extruder_id = 0;
|
||||
|
||||
if (status > 0 && !material.empty()) {
|
||||
tray.tray_type = material;
|
||||
tray.tray_color = color;
|
||||
tray.nozzle_temp = nozzle_temp;
|
||||
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
|
||||
tray.has_filament = true;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
tray.tray_info_idx = bundle
|
||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
||||
: map_filament_type_to_generic_id(tray.tray_type);
|
||||
} else {
|
||||
tray.has_filament = false;
|
||||
// Skip if no material type (empty gate)
|
||||
if (material.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = gate_idx;
|
||||
tray.tray_type = material;
|
||||
tray.tray_color = color;
|
||||
tray.nozzle_temp = nozzle_temp;
|
||||
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
|
||||
tray.has_filament = true;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
tray.tray_info_idx = bundle
|
||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
||||
: map_filament_type_to_generic_id(tray.tray_type);
|
||||
|
||||
max_lane_index = std::max(max_lane_index, gate_idx);
|
||||
trays.push_back(tray);
|
||||
}
|
||||
|
||||
max_lane_index = num_gates - 1;
|
||||
if (trays.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// AFC writes its filament data into the lane_data database namespace, and the records of the
|
||||
// fixture/agent write path carry no extruder field, so the lane -> toolhead binding has to be read
|
||||
// from the printer objects: each AFC_lane <name>.extruder names the toolhead its lane feeds, and
|
||||
// the toolhead's name carries its own index. Best effort: on failure every lane keeps the fallback
|
||||
// of the main extruder.
|
||||
bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder)
|
||||
{
|
||||
auto query_objects = [this](const std::string& query_string, nlohmann::json& out) {
|
||||
auto http = Http::get(join_url(device_info.base_url, "/printer/objects/query?" + query_string));
|
||||
if (!device_info.api_key.empty()) {
|
||||
http.header("X-Api-Key", device_info.api_key);
|
||||
}
|
||||
std::string body;
|
||||
unsigned status_code = 0;
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
.on_complete([&](std::string response, unsigned status) {
|
||||
body = std::move(response);
|
||||
status_code = status;
|
||||
})
|
||||
.perform_sync();
|
||||
if (status_code != 200) {
|
||||
return false;
|
||||
}
|
||||
out = nlohmann::json::parse(body, nullptr, false, true);
|
||||
return !out.is_discarded();
|
||||
};
|
||||
|
||||
nlohmann::json afc_json;
|
||||
if (!query_objects("AFC", afc_json) || !afc_json.contains("result") ||
|
||||
!afc_json["result"].contains("status") || !afc_json["result"]["status"].contains("AFC")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& afc = afc_json["result"]["status"]["AFC"];
|
||||
const auto& lanes = afc.contains("lanes") ? afc["lanes"] : nlohmann::json::array();
|
||||
if (!lanes.is_array() || lanes.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Query every lane object in one request; the object names are "AFC_lane <lane name>".
|
||||
std::string query_string;
|
||||
for (const auto& lane : lanes) {
|
||||
if (!lane.is_string()) {
|
||||
return false;
|
||||
}
|
||||
if (!query_string.empty()) {
|
||||
query_string += "&";
|
||||
}
|
||||
query_string += "AFC_lane%20" + lane.get<std::string>();
|
||||
}
|
||||
|
||||
nlohmann::json lanes_json;
|
||||
if (!query_objects(query_string, lanes_json) || !lanes_json.contains("result") ||
|
||||
!lanes_json["result"].contains("status")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& status = lanes_json["result"]["status"];
|
||||
for (const auto& lane : lanes) {
|
||||
const std::string name = lane.get<std::string>();
|
||||
const std::string key = "AFC_lane " + name;
|
||||
if (!status.contains(key) || !status[key].is_object() || !status[key].contains("extruder") ||
|
||||
!status[key]["extruder"].is_string()) {
|
||||
continue;
|
||||
}
|
||||
const std::string toolhead = status[key]["extruder"].get<std::string>();
|
||||
const int toolhead_index = extruder_index_from_name(toolhead);
|
||||
if (toolhead_index >= 0) {
|
||||
lane_extruder[name] = toolhead_index;
|
||||
}
|
||||
}
|
||||
|
||||
return !lane_extruder.empty();
|
||||
}
|
||||
|
||||
int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str)
|
||||
{
|
||||
auto json = nlohmann::json::parse(json_str, nullptr, false);
|
||||
@@ -1275,25 +1059,6 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
|
||||
}
|
||||
}
|
||||
|
||||
// Tool selection is printer-specific G-code. Moonraker forwards the
|
||||
// conventional T<index> command to Klipper, where the printer can
|
||||
// implement logical selection or a physical toolhead attach macro.
|
||||
if (cmd == "select_extruder") {
|
||||
if (!json["print"].contains("extruder_index") || !json["print"]["extruder_index"].is_number_integer()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder missing integer extruder_index, full json: " << json_str;
|
||||
return BAMBU_NETWORK_ERR_INVALID_RESULT;
|
||||
}
|
||||
|
||||
const int extruder_idx = json["print"]["extruder_index"].get<int>();
|
||||
if (extruder_idx < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder received negative extruder_index: " << extruder_idx;
|
||||
return BAMBU_NETWORK_ERR_INVALID_RESULT;
|
||||
}
|
||||
|
||||
send_gcode_async(dev_id, "T" + std::to_string(extruder_idx));
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
if (cmd == "home") {
|
||||
send_gcode_async(dev_id, "G28");
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
@@ -1390,7 +1155,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url,
|
||||
nlohmann::json& status,
|
||||
std::string& error) const
|
||||
{
|
||||
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&toolhead&extruder&heater_bed&fan");
|
||||
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan");
|
||||
|
||||
std::string response_body;
|
||||
bool success = false;
|
||||
@@ -1798,12 +1563,11 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
||||
}
|
||||
|
||||
for (const auto& name : this->available_objects) {
|
||||
const bool is_main_extruder = name == "extruder";
|
||||
const bool is_additional_extruder =
|
||||
name.rfind("extruder", 0) == 0 && name.size() > 8 &&
|
||||
std::all_of(name.begin() + 8, name.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
|
||||
if (is_main_extruder || is_additional_extruder) {
|
||||
if (name == "extruder" || name.rfind("extruder", 0) == 0) {
|
||||
subscribe_objects.insert(name);
|
||||
if (name == "extruder") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -2099,81 +1863,6 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
}
|
||||
}
|
||||
|
||||
// MachineObject::parse_new_info() only parses device.extruder when the four
|
||||
// legacy new-protocol fields are present. Moonraker has no equivalents, so
|
||||
// empty values act as compatibility markers without enabling any features.
|
||||
std::map<int, const nlohmann::json*> extruder_status;
|
||||
for (const auto& item : status_cache.items()) {
|
||||
int extruder_id = -1;
|
||||
if (item.key() == "extruder") {
|
||||
extruder_id = 0;
|
||||
} else if (item.key().rfind("extruder", 0) == 0 && item.key().size() > 8 &&
|
||||
std::all_of(item.key().begin() + 8, item.key().end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
extruder_id = std::stoi(item.key().substr(8));
|
||||
} catch (...) {
|
||||
extruder_id = -1;
|
||||
}
|
||||
}
|
||||
if (extruder_id >= 0 && item.value().is_object()) {
|
||||
extruder_status.emplace(extruder_id, &item.value());
|
||||
}
|
||||
}
|
||||
|
||||
if (!extruder_status.empty()) {
|
||||
auto& device_extruder = payload["print"]["device"]["extruder"];
|
||||
|
||||
int current_extruder_id = 0;
|
||||
if (status_cache.contains("toolhead") && status_cache["toolhead"].is_object() &&
|
||||
status_cache["toolhead"].contains("extruder") && status_cache["toolhead"]["extruder"].is_string()) {
|
||||
const std::string active_extruder = status_cache["toolhead"]["extruder"].get<std::string>();
|
||||
if (active_extruder != "extruder" && active_extruder.rfind("extruder", 0) == 0 && active_extruder.size() > 8 &&
|
||||
std::all_of(active_extruder.begin() + 8, active_extruder.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
const int parsed_id = std::stoi(active_extruder.substr(8));
|
||||
if (parsed_id >= 0 && parsed_id < 16 && extruder_status.count(parsed_id) != 0) {
|
||||
current_extruder_id = parsed_id;
|
||||
}
|
||||
} catch (...) {
|
||||
// Keep the main extruder as the fallback for malformed status data.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int extruder_count = std::min<int>(static_cast<int>(extruder_status.size()), 0xF);
|
||||
device_extruder["state"] = extruder_count | (current_extruder_id << 4);
|
||||
device_extruder["info"] = nlohmann::json::array();
|
||||
|
||||
auto encoded_temperature = [](const nlohmann::json& status, const char* key) {
|
||||
if (!status.contains(key) || !status[key].is_number()) {
|
||||
return 0;
|
||||
}
|
||||
const double temperature = status[key].get<double>();
|
||||
return std::clamp(static_cast<int>(temperature + 0.5), 0, 65535);
|
||||
};
|
||||
|
||||
for (const auto& [extruder_id, status] : extruder_status) {
|
||||
const int current_temperature = encoded_temperature(*status, "temperature");
|
||||
const int target_temperature = encoded_temperature(*status, "target");
|
||||
device_extruder["info"].push_back({
|
||||
{"id", extruder_id},
|
||||
{"filam_bak", nlohmann::json::array()},
|
||||
{"info", 1 << 3}, // The configured Moonraker extruder has a toolhead.
|
||||
{"temp", current_temperature | (target_temperature << 16)},
|
||||
{"spre", 0},
|
||||
{"snow", 0},
|
||||
{"star", 0},
|
||||
{"stat", 0},
|
||||
{"hnow", 0},
|
||||
});
|
||||
}
|
||||
|
||||
payload["print"]["cfg"] = "";
|
||||
payload["print"]["fun"] = "";
|
||||
payload["print"]["aux"] = "";
|
||||
payload["print"]["stat"] = "";
|
||||
}
|
||||
|
||||
if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) {
|
||||
const auto& bed = status_cache["heater_bed"];
|
||||
if (bed.contains("temperature") && bed["temperature"].is_number()) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -89,7 +88,6 @@ protected:
|
||||
// Tray data for AMS payload building
|
||||
struct AmsTrayData {
|
||||
int slot_index = 0; // 0-based slot index
|
||||
int extruder_id = 0; // Extruder this lane feeds (AFC extruder_index); 0 when unknown
|
||||
bool has_filament = false;
|
||||
std::string tray_type; // Material type (e.g., "PLA", "ASA")
|
||||
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
||||
@@ -101,10 +99,6 @@ protected:
|
||||
// Build ams JSON and call parser
|
||||
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Build ams JSON with one box per extruder (best effort, for lane_data-based filament
|
||||
// changers such as AFC) and call the parser. Boxes are ordered by extruder id.
|
||||
void build_ams_payload_grouped(const std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Methods that derived classes may need to override or access
|
||||
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port);
|
||||
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
|
||||
@@ -169,22 +163,10 @@ private:
|
||||
// System-specific filament fetch methods
|
||||
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
// Lane name -> toolhead index, read from the AFC printer objects (the lane_data database has no
|
||||
// extruder field). Returns false when AFC is not present or the objects cannot be read.
|
||||
bool fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder);
|
||||
|
||||
// Build one BBL tray JSON object (placeholder when the tray has no filament).
|
||||
static nlohmann::json make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray);
|
||||
|
||||
// Wrap the AMS boxes into the payload ParseV1_0 understands and apply the pull-mode
|
||||
// readiness state (printer_type, push counters, storage, module versions).
|
||||
void submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits);
|
||||
|
||||
// JSON helper methods
|
||||
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
|
||||
static int safe_json_int(const nlohmann::json& obj, const char* key);
|
||||
// Accepts either a JSON number or a numeric string (AFC reports some fields as strings).
|
||||
static int safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value);
|
||||
static std::string safe_array_string(const nlohmann::json& arr, int idx);
|
||||
static int safe_array_int(const nlohmann::json& arr, int idx);
|
||||
static std::string normalize_color_value(const std::string& color);
|
||||
|
||||
@@ -529,6 +529,7 @@ std::string PluginConfig::capability_type_label(PluginCapabilityType type)
|
||||
case PluginCapabilityType::Visualization: return _u8L("Visualization");
|
||||
case PluginCapabilityType::Script: return _u8L("Script");
|
||||
case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline");
|
||||
case PluginCapabilityType::ResourceTexture: return _u8L("Resource Texture");
|
||||
default: return _u8L("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
#include <boost/filesystem/file_status.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
@@ -1255,4 +1256,76 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
|
||||
const std::string& plugin_root,
|
||||
std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Picker sanity bound, not a security control: the popup draws every entry in its grid.
|
||||
constexpr size_t kMaxResourceTextureFiles = 64;
|
||||
|
||||
std::vector<std::string> files;
|
||||
error.clear();
|
||||
|
||||
if (dir.empty()) {
|
||||
error = _u8L("Resource texture directory is not set");
|
||||
return files;
|
||||
}
|
||||
if (plugin_root.empty()) {
|
||||
error = _u8L("Plugin folder could not be determined.");
|
||||
return files;
|
||||
}
|
||||
|
||||
boost::system::error_code ec;
|
||||
const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec);
|
||||
if (ec) {
|
||||
error = _u8L("Resource texture directory could not be resolved");
|
||||
return files;
|
||||
}
|
||||
|
||||
if (!fs::is_directory(resolved_dir, ec) || ec) {
|
||||
error = _u8L("Resource texture path is not a directory");
|
||||
return files;
|
||||
}
|
||||
|
||||
const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec);
|
||||
if (ec) {
|
||||
error = _u8L("Plugin folder could not be resolved");
|
||||
return files;
|
||||
}
|
||||
|
||||
if (!is_inside_allowed_root(resolved_dir, resolved_root)) {
|
||||
error = _u8L("Resource texture directory is outside the plugin folder");
|
||||
return files;
|
||||
}
|
||||
|
||||
for (fs::directory_iterator it(resolved_dir, ec), end; it != end && !ec; it.increment(ec)) {
|
||||
const fs::path& path = it->path();
|
||||
|
||||
if (!fs::is_regular_file(path, ec) || ec)
|
||||
continue;
|
||||
|
||||
std::string ext = path.extension().string();
|
||||
boost::algorithm::to_lower(ext);
|
||||
if (ext != ".png" && ext != ".jpg" && ext != ".jpeg" && ext != ".bmp")
|
||||
continue;
|
||||
|
||||
// Re-check per file: a symlink inside the pack must not resolve outside it.
|
||||
if (!is_inside_allowed_root(path, resolved_root))
|
||||
continue;
|
||||
|
||||
files.push_back(path.string());
|
||||
if (files.size() >= kMaxResourceTextureFiles)
|
||||
break;
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
error = _u8L("Resource texture directory could not be read");
|
||||
return {};
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -176,6 +176,12 @@ bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginD
|
||||
// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
|
||||
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
|
||||
|
||||
// Validated non-recursive .png/.jpg/.jpeg/.bmp files directly inside `dir`, every one contained in
|
||||
// `plugin_root`. Empty with `error` set on failure.
|
||||
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
|
||||
const std::string& plugin_root,
|
||||
std::string& error);
|
||||
|
||||
// Canonical writer: emits the .install_state.json schema for the given state.
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
|
||||
// Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PythonPluginBridge.hpp"
|
||||
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
@@ -387,6 +388,37 @@ bool load(const PluginDescriptor& descriptor,
|
||||
instance->set_config_ui_available(false);
|
||||
}
|
||||
|
||||
// ResourceTexture capabilities resolve their texture directory here, under the
|
||||
// GIL this block already holds, and cache the validated result. A resolution
|
||||
// fault only costs this capability its textures, so it is caught locally like
|
||||
// has_config_ui() above rather than failing the load.
|
||||
if (auto* resource_texture = dynamic_cast<ResourceTexturePluginCapability*>(instance.get())) {
|
||||
try {
|
||||
const std::string texture_dir = resource_texture->get_texture_dir();
|
||||
if (texture_dir.empty()) {
|
||||
resource_texture->set_texture_dir({});
|
||||
} else {
|
||||
std::string texture_error;
|
||||
collect_resource_texture_files(texture_dir, descriptor.plugin_root, texture_error);
|
||||
if (texture_error.empty()) {
|
||||
resource_texture->set_texture_dir(texture_dir);
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
|
||||
<< "': texture directory rejected (" << texture_error
|
||||
<< "); the capability will provide no textures";
|
||||
resource_texture->set_texture_dir({});
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
|
||||
<< "': get_texture_dir() failed (" << ex.what()
|
||||
<< "); the capability will provide no textures";
|
||||
resource_texture->set_texture_dir({});
|
||||
}
|
||||
}
|
||||
|
||||
capabilities.push_back(std::move(instance));
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
|
||||
#include "pluginTypes/pages/PagesPluginCapability.hpp"
|
||||
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
|
||||
#include "pluginTypes/script/ScriptPluginCapability.hpp"
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
#include <pybind11/pytypes.h>
|
||||
@@ -374,6 +375,7 @@ void bind_python_api(pybind11::module_& m)
|
||||
.value("Visualization", PluginCapabilityType::Visualization)
|
||||
.value("Script", PluginCapabilityType::Script)
|
||||
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
|
||||
.value("ResourceTexture", PluginCapabilityType::ResourceTexture)
|
||||
.value("Unknown", PluginCapabilityType::Unknown)
|
||||
.export_values();
|
||||
|
||||
@@ -527,6 +529,7 @@ void bind_python_api(pybind11::module_& m)
|
||||
PagesPluginCapability::RegisterBindings(m);
|
||||
ScriptPluginCapability::RegisterBindings(m);
|
||||
SlicingPipelinePluginCapability::RegisterBindings(m);
|
||||
ResourceTexturePluginCapability::RegisterBindings(m);
|
||||
PluginHost::RegisterBindings(m);
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, ResourceTexture, Unknown };
|
||||
|
||||
struct PluginCapabilityId
|
||||
{
|
||||
@@ -48,6 +48,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
case PluginCapabilityType::Visualization: return "visualization";
|
||||
case PluginCapabilityType::Script: return "script";
|
||||
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
|
||||
case PluginCapabilityType::ResourceTexture: return "resource-texture";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -63,6 +64,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
|
||||
case PluginCapabilityType::Visualization: return "Visualization";
|
||||
case PluginCapabilityType::Script: return "Script";
|
||||
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
|
||||
case PluginCapabilityType::ResourceTexture: return "Resource Texture";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -92,6 +94,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
|
||||
return PluginCapabilityType::Script;
|
||||
if (lowered == "slicing-pipeline")
|
||||
return PluginCapabilityType::SlicingPipeline;
|
||||
if (lowered == "resource-texture")
|
||||
return PluginCapabilityType::ResourceTexture;
|
||||
return PluginCapabilityType::Unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "PluginResourceTextures.hpp"
|
||||
|
||||
#include "slic3r/plugin/PluginDescriptor.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
PluginResourceTextures &PluginResourceTextures::instance()
|
||||
{
|
||||
static PluginResourceTextures registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
std::optional<ResourceTextureEntry> PluginResourceTextures::build_entry(const PluginCapabilityId &id) const
|
||||
{
|
||||
// Query by id and re-check state; the manager copies the shared_ptr and releases its lock.
|
||||
auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false);
|
||||
if (!capability || !capability->is_enabled() || capability->type() != PluginCapabilityType::ResourceTexture)
|
||||
return std::nullopt;
|
||||
|
||||
auto texture_capability = std::dynamic_pointer_cast<ResourceTexturePluginCapability>(capability);
|
||||
if (!texture_capability)
|
||||
return std::nullopt;
|
||||
|
||||
// An empty directory means resolution failed at load, so the pack contributes nothing.
|
||||
const std::string &texture_dir = texture_capability->texture_dir();
|
||||
if (texture_dir.empty())
|
||||
return std::nullopt;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor_for_capability(capability->name(),
|
||||
PluginCapabilityType::ResourceTexture,
|
||||
descriptor))
|
||||
return std::nullopt;
|
||||
|
||||
ResourceTextureEntry entry;
|
||||
entry.id = capability->identity();
|
||||
entry.plugin_key = entry.id.plugin_key;
|
||||
entry.display_name = capability->name();
|
||||
entry.texture_dir = texture_dir;
|
||||
entry.plugin_root = descriptor.plugin_root;
|
||||
return entry;
|
||||
}
|
||||
|
||||
void PluginResourceTextures::on_plugin_register(const std::string &plugin_key)
|
||||
{
|
||||
for (const auto &capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::ResourceTexture)) {
|
||||
if (capability)
|
||||
on_cap_register(capability->identity());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginResourceTextures::on_plugin_deregister(const std::string &plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_entries.begin(); it != m_entries.end();) {
|
||||
if (it->first.plugin_key == plugin_key)
|
||||
it = m_entries.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
void PluginResourceTextures::on_cap_register(const PluginCapabilityId &id)
|
||||
{
|
||||
std::optional<ResourceTextureEntry> entry = build_entry(id);
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (entry)
|
||||
m_entries[entry->id] = std::move(*entry);
|
||||
}
|
||||
|
||||
void PluginResourceTextures::on_cap_deregister(const PluginCapabilityId &id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_entries.erase(id);
|
||||
}
|
||||
|
||||
void PluginResourceTextures::initial_sweep()
|
||||
{
|
||||
std::map<PluginCapabilityId, ResourceTextureEntry> entries;
|
||||
for (const auto &capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::ResourceTexture)) {
|
||||
if (!capability)
|
||||
continue;
|
||||
std::optional<ResourceTextureEntry> entry = build_entry(capability->identity());
|
||||
if (entry)
|
||||
entries[entry->id] = std::move(*entry);
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_entries = std::move(entries);
|
||||
}
|
||||
|
||||
std::vector<ResourceTextureEntry> PluginResourceTextures::entries() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<ResourceTextureEntry> result;
|
||||
result.reserve(m_entries.size());
|
||||
for (const auto &entry : m_entries)
|
||||
result.push_back(entry.second);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// One ResourceTexture capability contributing a texture directory to the texture picker.
|
||||
struct ResourceTextureEntry
|
||||
{
|
||||
PluginCapabilityId id;
|
||||
std::string plugin_key;
|
||||
std::string display_name;
|
||||
std::string texture_dir;
|
||||
std::string plugin_root; // so a scan can re-check containment
|
||||
};
|
||||
|
||||
// Tracks every loaded and enabled ResourceTexture capability by id. Holds strings only: a
|
||||
// shared_ptr here would keep a Python object alive past Py_Finalize.
|
||||
class PluginResourceTextures
|
||||
{
|
||||
public:
|
||||
static PluginResourceTextures &instance();
|
||||
|
||||
PluginResourceTextures(const PluginResourceTextures &) = delete;
|
||||
PluginResourceTextures &operator=(const PluginResourceTextures &) = delete;
|
||||
|
||||
void on_plugin_register(const std::string &plugin_key);
|
||||
void on_plugin_deregister(const std::string &plugin_key);
|
||||
void on_cap_register(const PluginCapabilityId &id);
|
||||
void on_cap_deregister(const PluginCapabilityId &id);
|
||||
void initial_sweep();
|
||||
|
||||
std::vector<ResourceTextureEntry> entries() const;
|
||||
|
||||
private:
|
||||
PluginResourceTextures() = default;
|
||||
|
||||
std::optional<ResourceTextureEntry> build_entry(const PluginCapabilityId &id) const;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::map<PluginCapabilityId, ResourceTextureEntry> m_entries;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "ResourceTexturePluginCapability.hpp"
|
||||
#include "ResourceTexturePluginCapabilityTrampoline.hpp"
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void ResourceTexturePluginCapability::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering orca.resource_texture bindings";
|
||||
|
||||
auto resource_texture = module.def_submodule("resource_texture", "Plugin resource texture API");
|
||||
|
||||
py::class_<ResourceTexturePluginCapability, PluginCapabilityInterface,
|
||||
PyResourceTexturePluginCapabilityTrampoline,
|
||||
std::shared_ptr<ResourceTexturePluginCapability>>(resource_texture,
|
||||
"ResourceTexturePluginCapabilityBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &ResourceTexturePluginCapability::get_type)
|
||||
.def("get_texture_dir", &ResourceTexturePluginCapability::get_texture_dir);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../PythonPluginInterface.hpp"
|
||||
#include "pybind11/pybind11.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
class ResourceTexturePluginCapability : public PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::ResourceTexture; }
|
||||
|
||||
// Python contract: the directory the host scans for textures. Resolved once at materialization
|
||||
// under the GIL; never called from the GUI. Overridden in Python through the trampoline.
|
||||
virtual std::string get_texture_dir() const = 0;
|
||||
|
||||
// C++-only read of the validated directory cached by the loader; empty if resolution failed.
|
||||
const std::string& texture_dir() const { return m_texture_dir; }
|
||||
void set_texture_dir(std::string dir) { m_texture_dir = std::move(dir); }
|
||||
|
||||
private:
|
||||
std::string m_texture_dir;
|
||||
};
|
||||
} // namespace Slic3r
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "ResourceTexturePluginCapability.hpp"
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
#include <string>
|
||||
#include "slic3r/plugin/PythonPluginInterface.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PyResourceTexturePluginCapabilityTrampoline :
|
||||
public PyPluginCommonTrampoline<ResourceTexturePluginCapability>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<ResourceTexturePluginCapability>::PyPluginCommonTrampoline;
|
||||
|
||||
std::string get_texture_dir() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
std::string,
|
||||
ResourceTexturePluginCapability,
|
||||
get_texture_dir);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -5,13 +5,10 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_config_value_formatter.cpp
|
||||
test_creality_cfs_match.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_dev_extder_system.cpp
|
||||
test_device_access.cpp
|
||||
test_moonraker_discovery.cpp
|
||||
test_network_discovery.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_colour_picker_field.cpp
|
||||
test_filament_color_picker.cpp
|
||||
test_device_progress.cpp
|
||||
test_device_manager_integration.cpp
|
||||
test_web_media_controller.cpp
|
||||
@@ -33,6 +30,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_printer_agent.cpp
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_plugin_resource_texture.cpp
|
||||
test_plugin_printer_agent.cpp
|
||||
test_printhost.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
@@ -45,6 +43,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_texture_color_palette.cpp
|
||||
test_texture_island_view.cpp
|
||||
test_file_url.cpp
|
||||
test_imgui_wrapper.cpp
|
||||
test_user_manager.cpp
|
||||
../fff_print/test_helpers.cpp
|
||||
)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
#include "slic3r/GUI/Field.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <boost/any.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <string>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/string.h>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
namespace {
|
||||
class TestColourPicker : public ColourPicker {
|
||||
public:
|
||||
TestColourPicker() : ColourPicker(ConfigOptionDef{}, "filament_colour") {}
|
||||
std::string value() { return boost::any_cast<std::string>(get_value()); }
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Undefined color fields remain distinct from opaque black", "[ColourPickerField]")
|
||||
{
|
||||
TestColourPicker field;
|
||||
REQUIRE(field.value().empty());
|
||||
field.set_value(std::string("#000000"));
|
||||
REQUIRE(field.value() == "#000000");
|
||||
field.set_value(std::string("invalid color"));
|
||||
REQUIRE(field.value().empty());
|
||||
field.set_value(boost::any(wxString("#123456")));
|
||||
REQUIRE(field.value() == "#123456");
|
||||
field.set_value(std::string());
|
||||
REQUIRE(field.value().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Programmatic color field assignment stays silent", "[ColourPickerField]")
|
||||
{
|
||||
TestColourPicker field;
|
||||
int changes = 0;
|
||||
field.m_on_change = [&](const std::string&, const boost::any&) { ++changes; };
|
||||
field.set_value(std::string("#123456"), false);
|
||||
field.set_value(boost::any(wxString("#654321")), true);
|
||||
REQUIRE(changes == 0);
|
||||
REQUIRE(field.value() == "#654321");
|
||||
field.m_disable_change_event = true;
|
||||
field.set_value(std::string("#FF0000"));
|
||||
REQUIRE(field.value() == "#FF0000");
|
||||
REQUIRE(changes == 0);
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
// Match the include environment that libslic3r_gui TUs get from pchheader.hpp: Windows.h with
|
||||
// WIN32_LEAN_AND_MEAN/NOMINMAX must come first so rpcndr.h's `byte` is processed before <cstddef>
|
||||
// makes std::byte a competing candidate (otherwise the Windows COM headers pulled in via
|
||||
// DeviceManager.hpp error with an ambiguous `byte`). wx/timer.h must precede DeviceManager.hpp,
|
||||
// which includes DeviceErrorDialog.hpp (uses wxTimerEvent) before its own wx/timer.h include.
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include "catch2/catch_message.hpp"
|
||||
#include <slic3r/GUI/DeviceCore/DevDefs.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Extruder state decodes a generic toolhead set", "[DevExtderSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// device.extruder.state packing: count 0..3, current 4..7, target 8..11,
|
||||
// switch 12..14, loading 15..18, busy 19.
|
||||
const int extruder_count = 4;
|
||||
const int current_extruder = 2;
|
||||
const int switch_state = ES_BUSY;
|
||||
const int loading_extruder = 1;
|
||||
const int state = extruder_count | (current_extruder << 4) | (3 << 8) |
|
||||
(switch_state << 12) | (loading_extruder << 15) | (1 << 19);
|
||||
|
||||
json extruder = { {"state", state}, {"info", json::array()} };
|
||||
for (int id = 0; id < extruder_count; ++id) {
|
||||
// info: bit 1 has filament, bit 3 nozzle present.
|
||||
// temp: current 0..15, target 16..31. spre/snow/star: slot 0..7, ams 8..15.
|
||||
extruder["info"].push_back({
|
||||
{"id", id},
|
||||
{"filam_bak", json::array({id})},
|
||||
{"info", (id % 2 == 0 ? 2 : 0) | 8},
|
||||
{"temp", (200 + id) | ((250 + id) << 16)},
|
||||
{"spre", 10 << 8},
|
||||
{"snow", id | ((20 + id) << 8)},
|
||||
{"star", 30 << 8},
|
||||
{"stat", 0},
|
||||
{"hnow", id},
|
||||
});
|
||||
}
|
||||
|
||||
ExtderSystemParser::ParseV2_0(extruder, obj.GetExtderSystem());
|
||||
|
||||
REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == extruder_count);
|
||||
CHECK(obj.GetExtderSystem()->GetCurrentExtderId() == current_extruder);
|
||||
CHECK(static_cast<int>(obj.GetExtderSystem()->GetSwitchState()) == switch_state);
|
||||
CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == loading_extruder);
|
||||
CHECK(obj.GetExtderSystem()->IsBusyLoading());
|
||||
|
||||
for (int id = 0; id < extruder_count; ++id) {
|
||||
const auto ext = obj.GetExtderSystem()->GetExtderById(id);
|
||||
REQUIRE(ext.has_value());
|
||||
INFO("extruder " << id);
|
||||
CHECK(ext->GetExtId() == id);
|
||||
CHECK(ext->GetCurrentTemp() == 200 + id);
|
||||
CHECK(ext->GetTargetTemp() == 250 + id);
|
||||
CHECK(ext->HasFilamentInExt() == (id % 2 == 0));
|
||||
CHECK(ext->GetFilamBackup() == std::vector<int>{id});
|
||||
CHECK(ext->GetSlotPre().ams_id == "10");
|
||||
CHECK(ext->GetSlotNow().ams_id == std::to_string(20 + id));
|
||||
CHECK(ext->GetSlotNow().slot_id == std::to_string(id));
|
||||
CHECK(ext->GetSlotTarget().ams_id == "30");
|
||||
CHECK(ext->GetNozzleId() == id);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Legacy single-toolhead parser fills only the main extruder", "[DevExtderSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == 1);
|
||||
|
||||
// ParseV1_0 is the legacy single/dual fallback. It returns unless the system reports exactly
|
||||
// one toolhead, so it only ever writes MAIN_EXTRUDER_ID's fields.
|
||||
const json legacy = {
|
||||
{"nozzle_temper", 210},
|
||||
{"nozzle_target_temper", 245},
|
||||
{"ams", {{"tray_now", "2"}, {"tray_tar", "3"}}}
|
||||
};
|
||||
ExtderSystemParser::ParseV1_0(legacy, obj.GetExtderSystem());
|
||||
|
||||
const auto ext = obj.GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID);
|
||||
REQUIRE(ext.has_value());
|
||||
CHECK(ext->GetCurrentTemp() == 210);
|
||||
CHECK(ext->GetTargetTemp() == 245);
|
||||
|
||||
// Below 0x80 the tray pointer encodes ams_id = value / 4, slot_id = value % 4.
|
||||
CHECK(ext->GetSlotNow().ams_id == "0");
|
||||
CHECK(ext->GetSlotNow().slot_id == "2");
|
||||
CHECK(ext->GetSlotTarget().ams_id == "0");
|
||||
CHECK(ext->GetSlotTarget().slot_id == "3");
|
||||
CHECK(obj.GetExtderSystem()->IsBusyLoading());
|
||||
CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == MAIN_EXTRUDER_ID);
|
||||
}
|
||||
|
||||
// ParseV2_0 does not enforce the documented extruder dialect invariants: the count in `state` and
|
||||
// the id ordering of `info[]` are trusted, not checked. This test pins the resulting behavior.
|
||||
TEST_CASE("Malformed extruder payload is stored by info array position", "[DevExtderSystem]")
|
||||
{
|
||||
const auto extder_info = [](int id) {
|
||||
// Every key ParseV2_0 reads must be present.
|
||||
return json{ {"id", id}, {"filam_bak", json::array()}, {"info", 0}, {"temp", 0},
|
||||
{"spre", 0}, {"snow", 0}, {"star", 0}, {"stat", 0}, {"hnow", 0} };
|
||||
};
|
||||
|
||||
SECTION("state count disagrees with info length") {
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// `state` reports 2 toolheads but info[] carries 3.
|
||||
json info = json::array();
|
||||
for (int id = 0; id < 3; ++id)
|
||||
info.push_back(extder_info(id));
|
||||
ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem());
|
||||
|
||||
// The info array wins: three toolheads are stored. m_total_extder_count still holds 2, so
|
||||
// GetTotalExtderCount() would trip its debug assert; only the stored vector is inspected.
|
||||
CHECK(obj.GetExtderSystem()->GetTotalExtderSize() == 3);
|
||||
for (int id = 0; id < 3; ++id) {
|
||||
const auto ext = obj.GetExtderSystem()->GetExtderById(id);
|
||||
REQUIRE(ext.has_value());
|
||||
CHECK(ext->GetExtId() == id);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("info id differs from the array index") {
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// ids 1 and 2 sit at array positions 0 and 1.
|
||||
json info = json::array({extder_info(1), extder_info(2)});
|
||||
ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem());
|
||||
|
||||
// Toolheads are stored by array position; the payload id is kept but is not the lookup key.
|
||||
const auto first = obj.GetExtderSystem()->GetExtderById(0);
|
||||
REQUIRE(first.has_value());
|
||||
CHECK(first->GetExtId() == 1);
|
||||
const auto second = obj.GetExtderSystem()->GetExtderById(1);
|
||||
REQUIRE(second.has_value());
|
||||
CHECK(second->GetExtId() == 2);
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,10 @@
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include <vector>
|
||||
#include <catch2/catch_message.hpp>
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
@@ -69,162 +65,6 @@ TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSyste
|
||||
CHECK_FALSE(tray->is_slot_placeholder);
|
||||
}
|
||||
|
||||
TEST_CASE("Agent filament slots preserve every extruder and AMS binding", "[DevFilaSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
const json filament = json::parse(R"({
|
||||
"units": [
|
||||
{ "id": "0", "extruder": 0, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "PLA", "color": "FF5733FF" },
|
||||
{ "index": 1, "loaded": true, "material": "PLA", "color": "33C1FFFF" },
|
||||
{ "index": 2, "loaded": true, "material": "PLA", "color": "8D33FFFF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] },
|
||||
{ "id": "1", "extruder": 1, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "ASA", "color": "33C1FFFF" },
|
||||
{ "index": 1, "loaded": true, "material": "ASA", "color": "8D33FFFF" },
|
||||
{ "index": 2, "loaded": true, "material": "ASA", "color": "FF5733FF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] },
|
||||
{ "id": "2", "extruder": 2, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "ABS", "color": "8D33FFFF" },
|
||||
{ "index": 1, "loaded": true, "material": "ABS", "color": "FF5733FF" },
|
||||
{ "index": 2, "loaded": true, "material": "ABS", "color": "33C1FFFF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] },
|
||||
{ "id": "3", "extruder": 3, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "PETG", "color": "FF5733FF" },
|
||||
{ "index": 1, "loaded": true, "material": "PETG", "color": "33C1FFFF" },
|
||||
{ "index": 2, "loaded": true, "material": "PETG", "color": "8D33FFFF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] }
|
||||
],
|
||||
"external": [
|
||||
{ "extruder": 3, "loaded": true, "material": "PETG", "preset_id": "P3" },
|
||||
{ "extruder": 0, "loaded": true, "material": "PLA", "preset_id": "P0" },
|
||||
{ "extruder": 2, "loaded": false, "material": "ABS", "preset_id": "P2" },
|
||||
{ "extruder": 1, "loaded": true, "material": "ASA", "preset_id": "P1" }
|
||||
]
|
||||
})");
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
REQUIRE(obj.vt_slot.size() == 4);
|
||||
CHECK(obj.vt_slot[0].id == "255");
|
||||
CHECK(obj.vt_slot[1].id == "254");
|
||||
CHECK(obj.vt_slot[2].id == "253");
|
||||
CHECK(obj.vt_slot[3].id == "252");
|
||||
CHECK(obj.vt_slot[0].setting_id == "P0");
|
||||
CHECK(obj.vt_slot[1].setting_id == "P1");
|
||||
CHECK(obj.vt_slot[2].setting_id == "P2");
|
||||
CHECK(obj.vt_slot[3].setting_id == "P3");
|
||||
|
||||
REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 4);
|
||||
for (int extruder = 0; extruder < 4; ++extruder) {
|
||||
const auto& ams = obj.GetFilaSystem()->GetAmsList().at(std::to_string(extruder));
|
||||
CHECK(ams->GetExtruderId() == extruder);
|
||||
CHECK(ams->GetBindedExtruderSet() == std::set<int>{extruder});
|
||||
REQUIRE(ams->GetTrays().size() == 4);
|
||||
CHECK(ams->GetTrays().at("0")->color != ams->GetTrays().at("1")->color);
|
||||
CHECK(ams->GetTrays().at("1")->color != ams->GetTrays().at("2")->color);
|
||||
CHECK_FALSE(ams->GetTrays().at("3")->is_exists);
|
||||
}
|
||||
|
||||
CHECK(obj.contains_tray("253", "0"));
|
||||
CHECK(obj.get_tray("253", "0").setting_id == "P2");
|
||||
CHECK(obj.GetFilaSystem()->GetExtruderIdByAmsId("253") == 2);
|
||||
|
||||
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
|
||||
REQUIRE(tray_index_map.count(252) == 1);
|
||||
CHECK(tray_index_map.at(252).first == 252);
|
||||
CHECK(tray_index_map.at(252).second == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Generic AMS tray index map uses cumulative lane counts", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// A well-formed generic payload pairs "units" with "external". An empty external array resets
|
||||
// vt_slot, clearing the default virtual tray (255) MachineObject's constructor seeds; otherwise
|
||||
// that phantom external tray lands in GetTrayIndexMap and inflates the map size.
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
for (int ams_id = 0; ams_id < 2; ++ams_id) {
|
||||
json unit = {
|
||||
{"id", std::to_string(ams_id)},
|
||||
{"extruder", ams_id},
|
||||
{"slots", json::array()}
|
||||
};
|
||||
for (int slot_id = 0; slot_id < 5; ++slot_id)
|
||||
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
|
||||
REQUIRE(tray_index_map.size() == 10);
|
||||
CHECK(tray_index_map.at(0).first == 0);
|
||||
CHECK(tray_index_map.at(0).second == 0);
|
||||
CHECK(tray_index_map.at(4).first == 0);
|
||||
CHECK(tray_index_map.at(4).second == 4);
|
||||
CHECK(tray_index_map.at(5).first == 1);
|
||||
CHECK(tray_index_map.at(5).second == 0);
|
||||
CHECK(tray_index_map.at(9).first == 1);
|
||||
CHECK(tray_index_map.at(9).second == 4);
|
||||
}
|
||||
|
||||
TEST_CASE("Generic tray index map handles unequal per-unit slot counts", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// An "external" array must accompany "units" so vt_slot is reset (see the cumulative test above).
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
const std::vector<int> slot_counts = {4, 12};
|
||||
for (int ams_id = 0; ams_id < static_cast<int>(slot_counts.size()); ++ams_id) {
|
||||
json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} };
|
||||
for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id)
|
||||
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
|
||||
REQUIRE(tray_index_map.size() == 16);
|
||||
// Unit 0 occupies global lanes 0..3; unit 1 continues at 4..15 because the counter advances
|
||||
// by the reported tray count, not by a fixed four-slot stride.
|
||||
CHECK(tray_index_map.at(0).first == 0);
|
||||
CHECK(tray_index_map.at(0).second == 0);
|
||||
CHECK(tray_index_map.at(3).first == 0);
|
||||
CHECK(tray_index_map.at(3).second == 3);
|
||||
CHECK(tray_index_map.at(4).first == 1);
|
||||
CHECK(tray_index_map.at(4).second == 0);
|
||||
CHECK(tray_index_map.at(15).first == 1);
|
||||
CHECK(tray_index_map.at(15).second == 11);
|
||||
}
|
||||
|
||||
TEST_CASE("A generic AMS unit reports its reported tray count", "[DevFilaSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
const std::vector<int> slot_counts = {8, 1};
|
||||
for (int ams_id = 0; ams_id < static_cast<int>(slot_counts.size()); ++ams_id) {
|
||||
json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} };
|
||||
for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id)
|
||||
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
// Non-Bambu units are not capped at the four-slot Bambu contract; the reported tray count wins.
|
||||
const auto& ams_list = obj.GetFilaSystem()->GetAmsList();
|
||||
REQUIRE(ams_list.at("0")->GetSlotCount() == 8);
|
||||
REQUIRE(ams_list.at("1")->GetSlotCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <wx/colour.h>
|
||||
#ifdef __WXMSW__
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/image.h>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#endif
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
@@ -267,71 +260,3 @@ TEST_CASE("sample_gradient_ramp returns nothing without a usable curve or step c
|
||||
curve.points = {{0.0, kGradientMaxRatio, NAN, NAN}, {1.0, kGradientMinRatio, NAN, NAN}};
|
||||
REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 0).empty());
|
||||
}
|
||||
|
||||
// The headless GTK/macOS suite does not initialize GUI display/font stock objects.
|
||||
// Exercise native bitmap pixels on Windows without adding that dependency elsewhere.
|
||||
#ifdef __WXMSW__
|
||||
TEST_CASE("Filament gradient pixels follow both endpoint directions without changing input", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const bool reverse = GENERATE(false, true);
|
||||
const std::vector<wxColour> colors = reverse ? std::vector<wxColour>{wxColour(0, 0, 255), wxColour(255, 0, 0)}
|
||||
: std::vector<wxColour>{wxColour(255, 0, 0), wxColour(0, 0, 255)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 16), true);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
// Interior pixels avoid backend-specific rasterization at the gradient boundary.
|
||||
for (const auto& sample : {std::pair<int, wxColour>{2, colors.front()}, {61, colors.back()}}) {
|
||||
REQUIRE(std::abs(int(image.GetRed(sample.first, 8)) - int(sample.second.Red())) <= 16);
|
||||
REQUIRE(std::abs(int(image.GetBlue(sample.first, 8)) - int(sample.second.Blue())) <= 16);
|
||||
}
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Filament halves preserve the same caller color order as gradients", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const bool reverse = GENERATE(false, true);
|
||||
const std::vector<wxColour> colors = reverse ? std::vector<wxColour>{wxColour(0, 0, 255), wxColour(255, 0, 0)}
|
||||
: std::vector<wxColour>{wxColour(255, 0, 0), wxColour(0, 0, 255)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 16), false);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
require_same_rgb(wxColour(image.GetRed(16, 8), image.GetGreen(16, 8), image.GetBlue(16, 8)), colors.front());
|
||||
require_same_rgb(wxColour(image.GetRed(48, 8), image.GetGreen(48, 8), image.GetBlue(48, 8)), colors.back());
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Three color filament stripes retain caller order", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const std::vector<wxColour> colors{wxColour(0, 0, 255), wxColour(0, 255, 0), wxColour(255, 0, 0)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(72, 24), false);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int x = 12 + i * 24;
|
||||
require_same_rgb(wxColour(image.GetRed(x, 12), image.GetGreen(x, 12), image.GetBlue(x, 12)), colors[i]);
|
||||
}
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Four color filament quadrants retain caller order", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const std::vector<wxColour> colors{wxColour(0, 0, 255), wxColour(0, 255, 0), wxColour(255, 0, 0), wxColour(255, 255, 0)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 32), false);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const int x = i % 2 ? 48 : 16, y = i / 2 ? 24 : 8;
|
||||
require_same_rgb(wxColour(image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)), colors[i]);
|
||||
}
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
#endif // __WXMSW__
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
#include "slic3r/GUI/ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
// One color more than an ordered gradient may hold.
|
||||
static std::vector<std::string> too_many_gradient_colors()
|
||||
{
|
||||
return std::vector<std::string>(COLOR_PICKER_MAX_GRADIENT_COLORS + 1, "#FF0000");
|
||||
}
|
||||
|
||||
TEST_CASE("Project gradient direction survives editing and RGB serialization", "[FilamentColorPicker]")
|
||||
{
|
||||
const std::vector<std::string> endpoints = {"#FF0000", "#0000FF"};
|
||||
const FilamentColorPickerValue stored{endpoints, true};
|
||||
const auto initial = filament_color_picker_selection(stored);
|
||||
REQUIRE(initial);
|
||||
REQUIRE(filament_color_picker_result(*initial).colors == endpoints);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*initial, initial));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*initial, std::nullopt));
|
||||
const auto reverse = filament_color_picker_selection({{endpoints[1], endpoints[0]}, true});
|
||||
REQUIRE(filament_color_picker_changed(*initial, reverse));
|
||||
REQUIRE(filament_color_picker_result(*reverse).colors.front() == endpoints.back());
|
||||
}
|
||||
|
||||
TEST_CASE("Unrepresentable project colors remain unchanged until a different selection is confirmed", "[FilamentColorPicker]")
|
||||
{
|
||||
const auto value = GENERATE(FilamentColorPickerValue{too_many_gradient_colors(), true},
|
||||
FilamentColorPickerValue{{"#FF0000", "#0000FF"}, false});
|
||||
REQUIRE_FALSE(filament_color_picker_selection(value));
|
||||
const auto seed = filament_color_picker_initial(value);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(seed, seed));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(seed, std::nullopt));
|
||||
const auto changed = filament_color_picker_selection({{"#FE0000"}, false});
|
||||
REQUIRE(filament_color_picker_changed(seed, changed));
|
||||
REQUIRE(filament_color_picker_result(*changed).colors == std::vector<std::string>{"#FE0000"});
|
||||
}
|
||||
|
||||
TEST_CASE("Filament seeds preserve primary alpha when colors are invalid or absent", "[FilamentColorPicker]")
|
||||
{
|
||||
const FilamentColorPickerValue stale{{"invalid", "#00FF00"}, false, "#12345680"};
|
||||
REQUIRE_FALSE(filament_color_picker_selection(stale));
|
||||
REQUIRE(filament_color_picker_result(filament_color_picker_initial(stale)).colors == std::vector<std::string>{"#12345680"});
|
||||
const auto value = filament_color_picker_selection({{"#12345680", "#ABCDEF01"}, true});
|
||||
REQUIRE(value);
|
||||
REQUIRE(filament_color_picker_result(*value).colors == std::vector<std::string>{"#12345680", "#ABCDEF01"});
|
||||
const auto missing = filament_color_picker_initial({});
|
||||
REQUIRE(filament_color_picker_result(missing).colors == std::vector<std::string>{"#000000"});
|
||||
}
|
||||
|
||||
TEST_CASE("Filament project colors round trip every alpha boundary with compatible opaque spelling", "[FilamentColorPicker]")
|
||||
{
|
||||
const auto hex = GENERATE(std::string("#12345600"), std::string("#12345601"),
|
||||
std::string("#12345680"), std::string("#123456FE"),
|
||||
std::string("#123456FF"), std::string("#123456"));
|
||||
const FilamentColorPickerValue stored{{hex}, false};
|
||||
const auto selection = filament_color_picker_selection(stored);
|
||||
REQUIRE(selection);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, selection));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, std::nullopt));
|
||||
const auto result = filament_color_picker_result(*selection);
|
||||
const std::string expected = hex == "#123456FF" ? "#123456" : hex;
|
||||
REQUIRE(result.colors == std::vector<std::string>{expected});
|
||||
ColorRGBA decoded;
|
||||
REQUIRE(decode_color(result.colors.front(), decoded));
|
||||
REQUIRE(filament_color_picker_result(ColorSelection{decoded}).colors == result.colors);
|
||||
const auto reloaded = filament_color_picker_selection(result);
|
||||
REQUIRE(reloaded);
|
||||
REQUIRE(filament_color_picker_result(*reloaded).colors == result.colors);
|
||||
}
|
||||
|
||||
TEST_CASE("Alpha only filament edits preserve ordered independent gradient endpoint alpha", "[FilamentColorPicker]")
|
||||
{
|
||||
const auto original = filament_color_picker_selection({{"#FF000000", "#0000FF80"}, true});
|
||||
const auto edited = filament_color_picker_selection({{"#FF000001", "#0000FFFE"}, true});
|
||||
REQUIRE(original);
|
||||
REQUIRE(edited);
|
||||
REQUIRE(filament_color_picker_changed(*original, edited));
|
||||
const auto result = filament_color_picker_result(*edited);
|
||||
REQUIRE(result.gradient);
|
||||
REQUIRE(result.colors == std::vector<std::string>{"#FF000001", "#0000FFFE"});
|
||||
const auto reloaded = filament_color_picker_selection(result);
|
||||
REQUIRE(reloaded);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*edited, reloaded));
|
||||
const auto reversed = filament_color_picker_selection({{result.colors[1], result.colors[0]}, true});
|
||||
REQUIRE(filament_color_picker_changed(*edited, reversed));
|
||||
const auto solid = filament_color_picker_selection({{"#12345680"}, false});
|
||||
const auto alpha_only = filament_color_picker_selection({{"#12345681"}, false});
|
||||
REQUIRE(solid);
|
||||
REQUIRE(alpha_only);
|
||||
REQUIRE(filament_color_picker_changed(*solid, alpha_only));
|
||||
REQUIRE(filament_color_picker_result(*alpha_only).colors == std::vector<std::string>{"#12345681"});
|
||||
const auto mixed = filament_color_picker_selection({{"#FF0000FF", "#0000FF00"}, true});
|
||||
REQUIRE(mixed);
|
||||
REQUIRE(filament_color_picker_result(*mixed).colors == std::vector<std::string>{"#FF0000", "#0000FF00"});
|
||||
}
|
||||
|
||||
TEST_CASE("Unrepresentable filament seeds retain first endpoint or valid primary alpha", "[FilamentColorPicker]")
|
||||
{
|
||||
auto too_many = too_many_gradient_colors();
|
||||
too_many.front() = "#FF000000";
|
||||
const auto value = GENERATE_COPY(FilamentColorPickerValue{too_many, true},
|
||||
FilamentColorPickerValue{{"#FF000000", "#0000FF80"}, false},
|
||||
FilamentColorPickerValue{{"invalid", "#FF0000"}, true, "#FF000000"});
|
||||
REQUIRE_FALSE(filament_color_picker_selection(value));
|
||||
const auto initial = filament_color_picker_initial(value);
|
||||
REQUIRE(filament_color_picker_result(initial).colors == std::vector<std::string>{"#FF000000"});
|
||||
REQUIRE_FALSE(filament_color_picker_changed(initial, initial));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(initial, std::nullopt));
|
||||
const auto edited = filament_color_picker_selection({{"#FF000001"}, false});
|
||||
REQUIRE(filament_color_picker_changed(initial, edited));
|
||||
}
|
||||
|
||||
TEST_CASE("Ordered multi-color gradients round trip up to the color limit", "[FilamentColorPicker]")
|
||||
{
|
||||
const std::vector<std::string> all = {"#FF0000", "#FFFF0080", "#00FF00", "#00FFFF01", "#0000FF", "#FF00FF00", "#123456"};
|
||||
for (std::size_t count = 2; count <= std::min(all.size(), COLOR_PICKER_MAX_GRADIENT_COLORS); ++count) {
|
||||
const std::vector<std::string> colors(all.begin(), all.begin() + count);
|
||||
const auto selection = filament_color_picker_selection({colors, true});
|
||||
REQUIRE(selection);
|
||||
const auto result = filament_color_picker_result(*selection);
|
||||
REQUIRE(result.gradient);
|
||||
REQUIRE(result.colors == colors);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, filament_color_picker_selection(result)));
|
||||
std::vector<std::string> reordered = colors;
|
||||
std::swap(reordered.front(), reordered.back());
|
||||
REQUIRE(filament_color_picker_changed(*selection, filament_color_picker_selection({reordered, true})));
|
||||
}
|
||||
REQUIRE_FALSE(filament_color_picker_selection({too_many_gradient_colors(), true}));
|
||||
}
|
||||
|
||||
TEST_CASE("Filament selections require complete hexadecimal colors and exact endpoint counts", "[FilamentColorPicker]")
|
||||
{
|
||||
const std::string hex = GENERATE("", "112233", "#ABC", "#1234567", "#123456789", "#GG2233", "#11223Z", "#112233G0", " #112233", "#112233 ");
|
||||
REQUIRE_FALSE(filament_color_picker_selection({{hex}, false}));
|
||||
REQUIRE_FALSE(filament_color_picker_selection({{"#112233", hex}, true}));
|
||||
const auto malformed = GENERATE(FilamentColorPickerValue{{}, false},
|
||||
FilamentColorPickerValue{{}, true},
|
||||
FilamentColorPickerValue{{"#112233"}, true});
|
||||
REQUIRE_FALSE(filament_color_picker_selection(malformed));
|
||||
}
|
||||
|
||||
TEST_CASE("Every RGBA byte survives the public filament conversion round trip", "[FilamentColorPicker]")
|
||||
{
|
||||
constexpr char digits[] = "0123456789ABCDEF";
|
||||
for (int byte = 0; byte < 256; ++byte) {
|
||||
std::string hex = "#";
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
hex += digits[byte >> 4];
|
||||
hex += digits[byte & 15];
|
||||
}
|
||||
const auto selection = filament_color_picker_selection({{hex}, false});
|
||||
REQUIRE(selection);
|
||||
const auto result = filament_color_picker_result(*selection);
|
||||
REQUIRE(result.colors == std::vector<std::string>{byte == 255 ? hex.substr(0, 7) : hex});
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, filament_color_picker_selection(result)));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Filament serialization rejects invalid native RGBA channels", "[FilamentColorPicker]")
|
||||
{
|
||||
const float invalid = GENERATE(-0.1f, 1.1f, std::numeric_limits<float>::infinity(), std::numeric_limits<float>::quiet_NaN());
|
||||
const int channel = GENERATE(0, 1, 2, 3);
|
||||
ColorRGBA color;
|
||||
color[channel] = invalid;
|
||||
REQUIRE_THROWS_AS(filament_color_picker_result(color), std::invalid_argument);
|
||||
REQUIRE_THROWS_AS(filament_color_picker_result(ColorGradient{ColorRGBA::WHITE(), color}), std::invalid_argument);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// How ImGuiWrapper turns wx mouse events into ImGui input. ImGuiWrapper lives in libslic3r_gui; this is
|
||||
// the suite that links it. Same Windows include prologue as test_texture_color_palette.cpp (wx pulls in
|
||||
// <windows.h>; keep WIN32_LEAN_AND_MEAN / NOMINMAX ahead of it).
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
#include <wx/event.h>
|
||||
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Slic3r::GUI::ImGuiWrapper;
|
||||
|
||||
namespace {
|
||||
|
||||
// ImGui's wheel after one wheel event, as wx on macOS builds it: a delta of 10 and the rotation in whole
|
||||
// points of trackpad travel.
|
||||
float wheel_after(int rotation, wxMouseWheelAxis axis)
|
||||
{
|
||||
ImGuiWrapper imgui;
|
||||
imgui.set_display_size(800.f, 600.f); // update_mouse_data() ignores input until the display has a size
|
||||
|
||||
wxMouseEvent evt(wxEVT_MOUSEWHEEL);
|
||||
evt.m_wheelRotation = rotation;
|
||||
evt.m_wheelDelta = 10;
|
||||
evt.m_wheelAxis = axis;
|
||||
imgui.update_mouse_data(evt);
|
||||
return ImGui::GetIO().MouseWheel;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("a vertical wheel event steps ImGui's wheel in its own direction", "[ImGuiWrapper]")
|
||||
{
|
||||
const int rotation = GENERATE(1, 7, 120, -1, -7, -120);
|
||||
CHECK_THAT(wheel_after(rotation, wxMOUSE_WHEEL_VERTICAL), WithinAbs(rotation > 0 ? 1.0 : -1.0, 0.0));
|
||||
}
|
||||
|
||||
// Both kinds arrive from a macOS trackpad while scrolling; see ImGuiWrapper::update_mouse_data().
|
||||
TEST_CASE("a wheel event with no rotation or on the horizontal axis does not step ImGui's wheel", "[ImGuiWrapper][Regression]")
|
||||
{
|
||||
const auto [rotation, axis] = GENERATE(table<int, wxMouseWheelAxis>({
|
||||
{ 0, wxMOUSE_WHEEL_VERTICAL },
|
||||
{ 5, wxMOUSE_WHEEL_HORIZONTAL },
|
||||
{ -5, wxMOUSE_WHEEL_HORIZONTAL },
|
||||
}));
|
||||
CHECK_THAT(wheel_after(rotation, axis), WithinAbs(0.0, 0.0));
|
||||
}
|
||||
@@ -0,0 +1,941 @@
|
||||
// ResourceTexture plugin type: directory validation, the host registry's lifecycle, the texture
|
||||
// listing's plugin section, hostile get_texture_dir() overrides, decode parity and the .3mf round
|
||||
// trip without the pack installed.
|
||||
//
|
||||
// The GUI wiring lives in GUI_App::init_plugin_gui_wiring; the test binary has no app, so the same
|
||||
// registry callbacks are installed here. As in test_plugin_lifecycle.cpp, each Catch2 test case runs
|
||||
// in its own process (catch_discover_tests), so the PluginManager and interpreter come up at most once
|
||||
// per test.
|
||||
//
|
||||
// Same Windows include prologue as test_texture_color_palette.cpp: wx pulls in <windows.h>, so keep
|
||||
// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers.
|
||||
#include "test_utils.hpp"
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <wx/image.h>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Zipper.hpp"
|
||||
|
||||
#include "slic3r/GUI/TextureLibrary.hpp"
|
||||
#include "slic3r/plugin/PluginFsUtils.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/plugin/PythonInterpreter.hpp"
|
||||
#include "slic3r/plugin/PythonPluginInterface.hpp"
|
||||
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// Brings the plugin system up, and tears it down explicitly at the end of the test, as
|
||||
// test_plugin_lifecycle.cpp does. Declare this FIRST in a test so it is destroyed last.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log.
|
||||
ScopedDataDir python_data_dir{"texture-python"};
|
||||
bool initialized = false;
|
||||
|
||||
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
|
||||
~ScopedPluginManager()
|
||||
{
|
||||
PluginManager::instance().shutdown();
|
||||
PythonInterpreter::instance().shutdown();
|
||||
}
|
||||
};
|
||||
|
||||
// The app installs these in GUI_App::init_plugin_gui_wiring. Without an app the callbacks run inline
|
||||
// on the load worker, which is what lets the tests observe the registry right after the wait.
|
||||
void install_resource_texture_wiring()
|
||||
{
|
||||
static bool installed = false;
|
||||
if (installed)
|
||||
return;
|
||||
installed = true;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.subscribe_on_load_callback([](const std::string& plugin_key) {
|
||||
PluginResourceTextures::instance().on_plugin_register(plugin_key);
|
||||
GUI::bump_texture_library_generation();
|
||||
});
|
||||
manager.subscribe_on_unload_callback([](const std::string& plugin_key) {
|
||||
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
|
||||
GUI::bump_texture_library_generation();
|
||||
});
|
||||
manager.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
|
||||
if (capability.type != PluginCapabilityType::ResourceTexture)
|
||||
return;
|
||||
PluginResourceTextures::instance().on_cap_register(capability);
|
||||
GUI::bump_texture_library_generation();
|
||||
});
|
||||
manager.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
|
||||
if (capability.type != PluginCapabilityType::ResourceTexture)
|
||||
return;
|
||||
PluginResourceTextures::instance().on_cap_deregister(capability);
|
||||
GUI::bump_texture_library_generation();
|
||||
});
|
||||
}
|
||||
|
||||
// A minimal resource-texture plugin. `display_name` is the PEP723 package name and, unless
|
||||
// `capability_name` is given, the capability's get_name() too; pass `capability_name` to keep the
|
||||
// package name and the capability name independent.
|
||||
std::string make_resource_texture_source(const std::string& display_name,
|
||||
const std::string& get_texture_dir_body,
|
||||
const std::string& capability_name = "")
|
||||
{
|
||||
const std::string cap_name = capability_name.empty() ? display_name : capability_name;
|
||||
return std::string{}
|
||||
+ "# /// script\n"
|
||||
+ "# requires-python = \">=3.12\"\n"
|
||||
+ "# dependencies = []\n"
|
||||
+ "#\n"
|
||||
+ "# [tool.orcaslicer.plugin]\n"
|
||||
+ "# name = \"" + display_name + "\"\n"
|
||||
+ "# description = \"Resource texture pack fixture.\"\n"
|
||||
+ "# author = \"tests\"\n"
|
||||
+ "# version = \"1.0.0\"\n"
|
||||
+ "# type = \"resource-texture\"\n"
|
||||
+ "# ///\n"
|
||||
+ "import os\n"
|
||||
+ "import orca\n"
|
||||
+ "\n\n"
|
||||
+ "class PackTextures(orca.resource_texture.ResourceTexturePluginCapabilityBase):\n"
|
||||
+ " def get_name(self):\n"
|
||||
+ " return \"" + cap_name + "\"\n"
|
||||
+ "\n"
|
||||
+ " def get_texture_dir(self):\n"
|
||||
+ " " + get_texture_dir_body + "\n"
|
||||
+ "\n\n"
|
||||
+ "@orca.plugin\n"
|
||||
+ "class TexturePack(orca.base):\n"
|
||||
+ " def register_capabilities(self):\n"
|
||||
+ " orca.register_capability(PackTextures)\n";
|
||||
}
|
||||
|
||||
std::string default_texture_dir_body()
|
||||
{
|
||||
return "return os.path.join(os.path.dirname(__file__), \"textures\")";
|
||||
}
|
||||
|
||||
// Writes {data_dir}/orca_plugins/<stem>/<stem>.py and returns the plugin directory.
|
||||
fs::path write_plugin(const ScopedDataDir& data_dir_guard, const std::string& stem, const std::string& source)
|
||||
{
|
||||
const fs::path plugin_dir = data_dir_guard.plugins_dir() / stem;
|
||||
fs::create_directories(plugin_dir);
|
||||
|
||||
std::ofstream out((plugin_dir / (stem + ".py")).string(), std::ios::binary);
|
||||
out << source;
|
||||
out.close();
|
||||
|
||||
return plugin_dir;
|
||||
}
|
||||
|
||||
bool load_and_wait(PluginManager& manager, const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
manager.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
return manager.wait_for_plugin_load(plugin_key, std::chrono::seconds(120), error);
|
||||
}
|
||||
|
||||
// The Plugin-sourced slice of the texture listing, which is exactly what the picker's third heading
|
||||
// draws. Pass force to call texture_library(true); without it the listing is reused unless a pack
|
||||
// callback moved the generation.
|
||||
std::vector<GUI::TextureLibraryEntry> plugin_textures(bool force)
|
||||
{
|
||||
std::vector<GUI::TextureLibraryEntry> found;
|
||||
for (const GUI::TextureLibraryEntry& entry : GUI::texture_library(force))
|
||||
if (entry.source == GUI::TextureSource::Plugin)
|
||||
found.push_back(entry);
|
||||
return found;
|
||||
}
|
||||
|
||||
void write_text_file(const fs::path& path, const std::string& content = "x")
|
||||
{
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream out(path.string(), std::ios::binary);
|
||||
out << content;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> read_file_bytes(const fs::path& path)
|
||||
{
|
||||
std::ifstream ifs(path.string(), std::ios::binary);
|
||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
void write_bytes(const fs::path& path, const std::vector<unsigned char>& bytes)
|
||||
{
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream out(path.string(), std::ios::binary);
|
||||
out.write(reinterpret_cast<const char*>(bytes.data()), std::streamsize(bytes.size()));
|
||||
}
|
||||
|
||||
void write_gray_png(const fs::path& path, uint8_t value, size_t w = 4, size_t h = 4)
|
||||
{
|
||||
fs::create_directories(path.parent_path());
|
||||
const std::vector<uint8_t> pixels(w * h, value);
|
||||
REQUIRE(Slic3r::png::write_gray_to_file(path.string(), w, h, pixels));
|
||||
}
|
||||
|
||||
// A 24-bit gray BMP (R == G == B), synthesised without an image toolkit. Rows are bottom-up and
|
||||
// padded to four bytes; the image is uniform, so the orientation does not matter.
|
||||
std::vector<unsigned char> make_gray_bmp(int width, int height, uint8_t value)
|
||||
{
|
||||
const int row_bytes = ((width * 3 + 3) / 4) * 4;
|
||||
const uint32_t pixel_bytes = uint32_t(row_bytes) * uint32_t(height);
|
||||
const uint32_t file_size = 14 + 40 + pixel_bytes;
|
||||
|
||||
std::vector<unsigned char> bmp(file_size, 0);
|
||||
const auto put16 = [&bmp](size_t at, uint16_t v) {
|
||||
bmp[at] = uint8_t(v);
|
||||
bmp[at + 1] = uint8_t(v >> 8);
|
||||
};
|
||||
const auto put32 = [&bmp](size_t at, uint32_t v) {
|
||||
bmp[at] = uint8_t(v);
|
||||
bmp[at + 1] = uint8_t(v >> 8);
|
||||
bmp[at + 2] = uint8_t(v >> 16);
|
||||
bmp[at + 3] = uint8_t(v >> 24);
|
||||
};
|
||||
|
||||
bmp[0] = 'B';
|
||||
bmp[1] = 'M';
|
||||
put32(2, file_size);
|
||||
put32(10, 54); // pixel data offset
|
||||
put32(14, 40); // BITMAPINFOHEADER size
|
||||
put32(18, uint32_t(width));
|
||||
put32(22, uint32_t(height));
|
||||
put16(26, 1); // planes
|
||||
put16(28, 24); // bits per pixel
|
||||
put32(34, pixel_bytes);
|
||||
|
||||
for (int y = 0; y < height; ++y) {
|
||||
unsigned char* row = bmp.data() + 54 + size_t(y) * size_t(row_bytes);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
row[x * 3] = value;
|
||||
row[x * 3 + 1] = value;
|
||||
row[x * 3 + 2] = value;
|
||||
}
|
||||
}
|
||||
return bmp;
|
||||
}
|
||||
|
||||
// The app calls wxInitAllImageHandlers() during startup and, before that, wxEntryStart() runs the
|
||||
// wxImageModule, which registers the standard handlers. wxInitAllImageHandlers() adds PNG, JPEG and
|
||||
// the rest but NOT the BMP handler, so a test binary without a wxApp must add the standard set too or
|
||||
// a .bmp texture cannot be loaded here at all.
|
||||
void ensure_wx_image_handlers()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
wxImage::InitStandardHandlers();
|
||||
wxInitAllImageHandlers();
|
||||
}
|
||||
|
||||
// Stores `model` as a one-plate project, mirroring tests/libslic3r/test_3mf.cpp.
|
||||
void store_project(Model& model, const std::string& path)
|
||||
{
|
||||
ScopedTemporaryDir backup_dir("orca_tex_pack_src");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
DynamicPrintConfig cfg;
|
||||
PlateData plate;
|
||||
plate.plate_index = 0;
|
||||
StoreParams sp;
|
||||
sp.path = path.c_str();
|
||||
sp.model = &model;
|
||||
sp.config = &cfg;
|
||||
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
sp.plate_data_list.push_back(&plate);
|
||||
REQUIRE(store_bbs_3mf(sp));
|
||||
}
|
||||
|
||||
bool load_project(const std::string& path, Model& model, const ScopedTemporaryDir& backup_dir)
|
||||
{
|
||||
model.set_backup_path(backup_dir.string());
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false;
|
||||
bool is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
const bool loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates, &project_presets,
|
||||
&is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
release_PlateData_list(plates);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── Validation (collect_resource_texture_files, no Python) ──────────────────────────────────────
|
||||
|
||||
TEST_CASE("A texture file directly inside the plugin folder is listed", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-root");
|
||||
write_text_file(root.path() / "textures" / "grain.png");
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files =
|
||||
collect_resource_texture_files((root.path() / "textures").string(), root.string(), error);
|
||||
|
||||
CHECK(error.empty());
|
||||
REQUIRE(files.size() == 1);
|
||||
CHECK(fs::path(files.front()).filename() == "grain.png");
|
||||
}
|
||||
|
||||
TEST_CASE("A directory reached with a parent-directory escape is refused", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir parent("tex-escape");
|
||||
const fs::path root = parent.path() / "pack";
|
||||
const fs::path outside = parent.path() / "outside";
|
||||
fs::create_directories(root);
|
||||
write_text_file(outside / "leak.png");
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files =
|
||||
collect_resource_texture_files((root / ".." / "outside").string(), root.string(), error);
|
||||
|
||||
CHECK(files.empty());
|
||||
CHECK_FALSE(error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("An absolute directory outside the plugin folder is refused", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-abs-root");
|
||||
ScopedTemporaryDir elsewhere("tex-abs-elsewhere");
|
||||
write_text_file(elsewhere.path() / "leak.png");
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files = collect_resource_texture_files(elsewhere.string(), root.string(), error);
|
||||
|
||||
CHECK(files.empty());
|
||||
CHECK_FALSE(error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("A symlink pointing outside the plugin folder is skipped, not reported as an error", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-symlink-root");
|
||||
ScopedTemporaryDir target("tex-symlink-target");
|
||||
write_text_file(target.path() / "secret.png");
|
||||
|
||||
const fs::path link = root.path() / "textures" / "link.png";
|
||||
fs::create_directories(link.parent_path());
|
||||
boost::system::error_code ec;
|
||||
fs::create_symlink(target.path() / "secret.png", link, ec);
|
||||
if (ec)
|
||||
SKIP("symlinks unavailable on this filesystem: " + ec.message());
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files =
|
||||
collect_resource_texture_files(link.parent_path().string(), root.string(), error);
|
||||
|
||||
// The directory itself is legitimate; the escaping link is dropped, not an error.
|
||||
CHECK(error.empty());
|
||||
CHECK(files.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Files in a subdirectory of the texture directory are not listed", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-recursive");
|
||||
write_text_file(root.path() / "top.png");
|
||||
write_text_file(root.path() / "nested" / "deep.png");
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
|
||||
|
||||
CHECK(error.empty());
|
||||
REQUIRE(files.size() == 1);
|
||||
CHECK(fs::path(files.front()).filename() == "top.png");
|
||||
}
|
||||
|
||||
TEST_CASE("An entry that is not a regular file is skipped", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-nonregular");
|
||||
write_text_file(root.path() / "real.png");
|
||||
fs::create_directories(root.path() / "fake.png"); // a directory named like an image
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
|
||||
|
||||
CHECK(error.empty());
|
||||
REQUIRE(files.size() == 1);
|
||||
CHECK(fs::path(files.front()).filename() == "real.png");
|
||||
}
|
||||
|
||||
TEST_CASE("Image extensions are matched case-insensitively and other extensions are ignored",
|
||||
"[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-extensions");
|
||||
for (const char* name : { "a.png", "b.PNG", "c.jpg", "d.JPG", "e.jpeg", "f.JPEG", "g.bmp", "h.BMP" })
|
||||
write_text_file(root.path() / name);
|
||||
for (const char* name : { "i.txt", "j.gif", "k.pngx", "l.pn", "m.png.bak" })
|
||||
write_text_file(root.path() / name);
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
|
||||
|
||||
CHECK(error.empty());
|
||||
CHECK(files.size() == 8);
|
||||
|
||||
std::vector<std::string> names;
|
||||
for (const std::string& file : files)
|
||||
names.push_back(fs::path(file).filename().string());
|
||||
for (const char* accepted : { "a.png", "b.PNG", "c.jpg", "d.JPG", "e.jpeg", "f.JPEG", "g.bmp", "h.BMP" })
|
||||
CHECK(std::find(names.begin(), names.end(), accepted) != names.end());
|
||||
for (const char* rejected : { "i.txt", "j.gif", "k.pngx", "l.pn", "m.png.bak" })
|
||||
CHECK(std::find(names.begin(), names.end(), rejected) == names.end());
|
||||
}
|
||||
|
||||
TEST_CASE("The listing stops at 64 image files", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-cap");
|
||||
for (int i = 0; i < 70; ++i)
|
||||
write_text_file(root.path() / ("file" + std::to_string(i) + ".png"));
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files = collect_resource_texture_files(root.string(), root.string(), error);
|
||||
|
||||
CHECK(error.empty());
|
||||
CHECK(files.size() == 64);
|
||||
}
|
||||
|
||||
TEST_CASE("A missing texture directory yields no files and an error", "[PluginResourceTexture]")
|
||||
{
|
||||
ScopedTemporaryDir root("tex-missing");
|
||||
|
||||
std::string error;
|
||||
const std::vector<std::string> files =
|
||||
collect_resource_texture_files((root.path() / "textures").string(), root.string(), error);
|
||||
|
||||
CHECK(files.empty());
|
||||
CHECK_FALSE(error.empty());
|
||||
}
|
||||
|
||||
// ── Lifecycle through the real listing ──────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("A resource-texture pack's images appear in the listing and clear on disable and unload",
|
||||
"[PluginResourceTexture][PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-listing");
|
||||
|
||||
const std::string plugin_key = "Texture_Pack";
|
||||
const fs::path plugin_dir =
|
||||
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Texture Pack", default_texture_dir_body()));
|
||||
|
||||
const fs::path textures = plugin_dir / "textures";
|
||||
write_gray_png(textures / "alpha.png", 0x20);
|
||||
write_gray_png(textures / "beta.png", 0x80);
|
||||
write_text_file(textures / "gamma.jpg"); // the listing only checks the extension; decode is covered below
|
||||
write_text_file(textures / "notes.txt");
|
||||
|
||||
// A user-owned texture, so the listing has a non-pack entry whose provenance must be empty.
|
||||
write_gray_png(fs::path(GUI::user_texture_dir()) / "user_made.png", 0x40);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, plugin_key, error));
|
||||
INFO("load error: " << error);
|
||||
CHECK(error.empty());
|
||||
|
||||
CHECK(PluginResourceTextures::instance().entries().size() == 1);
|
||||
|
||||
const uint64_t loaded_generation = GUI::texture_library_generation();
|
||||
const auto loaded = plugin_textures(true);
|
||||
REQUIRE(loaded.size() == 3);
|
||||
std::vector<std::string> names;
|
||||
for (const GUI::TextureLibraryEntry& entry : loaded)
|
||||
names.push_back(entry.name);
|
||||
CHECK(std::find(names.begin(), names.end(), "alpha") != names.end());
|
||||
CHECK(std::find(names.begin(), names.end(), "beta") != names.end());
|
||||
CHECK(std::find(names.begin(), names.end(), "gamma") != names.end());
|
||||
CHECK(std::find(names.begin(), names.end(), "notes") == names.end());
|
||||
|
||||
// Every pack entry names its contributing pack; shipped and user entries carry no pack.
|
||||
for (const GUI::TextureLibraryEntry& entry : GUI::texture_library(true)) {
|
||||
if (entry.source == GUI::TextureSource::Plugin) {
|
||||
CHECK_FALSE(entry.pack_name.empty());
|
||||
CHECK(entry.pack_name == "Texture Pack");
|
||||
} else {
|
||||
CHECK(entry.pack_name.empty());
|
||||
}
|
||||
}
|
||||
|
||||
// A no-argument call reuses the cached listing until a pack callback moves the generation.
|
||||
CHECK(plugin_textures(false).size() == 3);
|
||||
|
||||
manager.set_capability_enabled({ PluginCapabilityType::ResourceTexture, "Texture Pack", plugin_key }, false);
|
||||
CHECK(GUI::texture_library_generation() > loaded_generation);
|
||||
CHECK(PluginResourceTextures::instance().entries().empty());
|
||||
CHECK(plugin_textures(false).empty());
|
||||
|
||||
manager.set_capability_enabled({ PluginCapabilityType::ResourceTexture, "Texture Pack", plugin_key }, true);
|
||||
CHECK(plugin_textures(false).size() == 3);
|
||||
|
||||
REQUIRE(manager.unload_plugin(plugin_key));
|
||||
CHECK(PluginResourceTextures::instance().entries().empty());
|
||||
CHECK(plugin_textures(false).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Two distinctly named packs each list as one contiguous run",
|
||||
"[PluginResourceTexture][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-contiguous");
|
||||
|
||||
// Loaded out of name order, so it is the listing's pack-name sort that orders the runs.
|
||||
const std::string key_zeta = "Pack_Zeta";
|
||||
const std::string key_alpha = "Pack_Alpha";
|
||||
|
||||
const fs::path dir_zeta = write_plugin(
|
||||
data_dir_guard, key_zeta, make_resource_texture_source("Zeta Pack", default_texture_dir_body()));
|
||||
const fs::path dir_alpha = write_plugin(
|
||||
data_dir_guard, key_alpha, make_resource_texture_source("Alpha Pack", default_texture_dir_body()));
|
||||
|
||||
write_gray_png(dir_zeta / "textures" / "zeta_one.png", 0x20);
|
||||
write_gray_png(dir_zeta / "textures" / "zeta_two.png", 0x40);
|
||||
write_gray_png(dir_alpha / "textures" / "alpha_one.png", 0x60);
|
||||
write_gray_png(dir_alpha / "textures" / "alpha_two.png", 0x80);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, key_zeta, error));
|
||||
REQUIRE(load_and_wait(manager, key_alpha, error));
|
||||
|
||||
std::vector<GUI::TextureLibraryEntry> pack_entries;
|
||||
for (const GUI::TextureLibraryEntry& entry : GUI::texture_library(true))
|
||||
if (entry.source == GUI::TextureSource::Plugin)
|
||||
pack_entries.push_back(entry);
|
||||
REQUIRE(pack_entries.size() == 4);
|
||||
|
||||
const auto count_pack = [&pack_entries](const std::string& name) {
|
||||
return std::count_if(pack_entries.begin(), pack_entries.end(),
|
||||
[&name](const GUI::TextureLibraryEntry& e) { return e.pack_name == name; });
|
||||
};
|
||||
CHECK(count_pack("Zeta Pack") == 2);
|
||||
CHECK(count_pack("Alpha Pack") == 2);
|
||||
|
||||
// Collapsing consecutive entries by pack name must leave one run per pack: a name never reappears
|
||||
// after another name intervenes, which is what lets the picker draw one section per pack.
|
||||
std::vector<std::string> runs;
|
||||
for (const GUI::TextureLibraryEntry& entry : pack_entries)
|
||||
if (runs.empty() || runs.back() != entry.pack_name)
|
||||
runs.push_back(entry.pack_name);
|
||||
|
||||
std::vector<std::string> sorted_runs = runs;
|
||||
std::sort(sorted_runs.begin(), sorted_runs.end());
|
||||
CHECK(std::unique(sorted_runs.begin(), sorted_runs.end()) == sorted_runs.end());
|
||||
CHECK(runs == std::vector<std::string>({ "Alpha Pack", "Zeta Pack" }));
|
||||
|
||||
manager.unload_plugin(key_zeta);
|
||||
manager.unload_plugin(key_alpha);
|
||||
}
|
||||
|
||||
TEST_CASE("A second pack declaring a loaded capability name is refused",
|
||||
"[PluginResourceTexture][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-capability-clash");
|
||||
|
||||
// Distinct package names, one capability name: the loader allows only the first.
|
||||
const std::string key_first = "Clash_First";
|
||||
const std::string key_second = "Clash_Second";
|
||||
const std::string capability = "Shared Textures";
|
||||
|
||||
const fs::path dir_first = write_plugin(
|
||||
data_dir_guard, key_first,
|
||||
make_resource_texture_source("Clash Package One", default_texture_dir_body(), capability));
|
||||
write_plugin(data_dir_guard, key_second,
|
||||
make_resource_texture_source("Clash Package Two", default_texture_dir_body(), capability));
|
||||
|
||||
write_gray_png(dir_first / "textures" / "one.png", 0x20);
|
||||
write_gray_png(dir_first / "textures" / "two.png", 0x40);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, key_first, error));
|
||||
REQUIRE(manager.is_plugin_loaded(key_first));
|
||||
|
||||
const std::vector<GUI::TextureLibraryEntry> before = plugin_textures(true);
|
||||
REQUIRE(before.size() == 2);
|
||||
for (const GUI::TextureLibraryEntry& entry : before)
|
||||
CHECK(entry.pack_name == capability);
|
||||
|
||||
// The second package claims the capability name the first already owns, so the load is refused.
|
||||
std::string second_error;
|
||||
CHECK_FALSE(load_and_wait(manager, key_second, second_error));
|
||||
CHECK_FALSE(manager.is_plugin_loaded(key_second));
|
||||
|
||||
// The first pack's textures are untouched by the refused load.
|
||||
const std::vector<GUI::TextureLibraryEntry> after = plugin_textures(true);
|
||||
REQUIRE(after.size() == before.size());
|
||||
for (size_t i = 0; i < before.size(); ++i) {
|
||||
CHECK(after[i].name == before[i].name);
|
||||
CHECK(after[i].path == before[i].path);
|
||||
CHECK(after[i].pack_name == before[i].pack_name);
|
||||
}
|
||||
|
||||
manager.unload_plugin(key_first);
|
||||
}
|
||||
|
||||
TEST_CASE("A resource-texture pack with no images contributes no textures", "[PluginResourceTexture][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-empty");
|
||||
|
||||
const std::string plugin_key = "Empty_Pack";
|
||||
const fs::path plugin_dir =
|
||||
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Empty Pack", default_texture_dir_body()));
|
||||
fs::create_directories(plugin_dir / "textures"); // valid, but empty
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, plugin_key, error));
|
||||
CHECK(manager.is_plugin_loaded(plugin_key));
|
||||
CHECK(plugin_textures(true).empty());
|
||||
|
||||
manager.unload_plugin(plugin_key);
|
||||
}
|
||||
|
||||
// ── Hostile get_texture_dir() overrides ─────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("A texture directory outside the plugin root contributes no textures", "[PluginResourceTexture][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-hostile");
|
||||
ScopedResourcesDir resources_dir_guard("texture-hostile-res");
|
||||
write_text_file(fs::path(resources_dir_guard.dir) / "sentinel.png"); // would list if it were scanned
|
||||
|
||||
const std::string data_pack = "Data_Dir_Pack";
|
||||
write_plugin(data_dir_guard, data_pack,
|
||||
make_resource_texture_source("Data Dir Pack", "return r\"" + data_dir_guard.dir.string() + "\""));
|
||||
const std::string resources_pack = "Resources_Dir_Pack";
|
||||
write_plugin(data_dir_guard, resources_pack,
|
||||
make_resource_texture_source("Resources Dir Pack", "return r\"" + resources_dir_guard.dir.string() + "\""));
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, data_pack, error));
|
||||
REQUIRE(load_and_wait(manager, resources_pack, error));
|
||||
|
||||
// The packs load; their out-of-root directories are simply refused.
|
||||
CHECK(manager.is_plugin_loaded(data_pack));
|
||||
CHECK(manager.is_plugin_loaded(resources_pack));
|
||||
CHECK(PluginResourceTextures::instance().entries().empty());
|
||||
CHECK(plugin_textures(true).empty());
|
||||
|
||||
manager.unload_plugin(data_pack);
|
||||
manager.unload_plugin(resources_pack);
|
||||
}
|
||||
|
||||
TEST_CASE("A raising get_texture_dir leaves the pack loaded with no textures", "[PluginResourceTexture][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-raising");
|
||||
|
||||
const std::string plugin_key = "Raising_Pack";
|
||||
write_plugin(data_dir_guard, plugin_key,
|
||||
make_resource_texture_source("Raising Pack", "raise RuntimeError(\"no textures today\")"));
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, plugin_key, error));
|
||||
CHECK(error.empty());
|
||||
CHECK(manager.is_plugin_loaded(plugin_key));
|
||||
CHECK(PluginResourceTextures::instance().entries().empty());
|
||||
CHECK(plugin_textures(true).empty());
|
||||
|
||||
manager.unload_plugin(plugin_key);
|
||||
}
|
||||
|
||||
// ── Decoding parity ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("Pack images decode through the same path as hand-picked files", "[PluginResourceTexture]")
|
||||
{
|
||||
ensure_wx_image_handlers();
|
||||
|
||||
ScopedTemporaryDir pack_root("tex-pack");
|
||||
ScopedTemporaryDir hand_root("tex-hand");
|
||||
const fs::path pack_dir = pack_root.path() / "textures";
|
||||
fs::create_directories(pack_dir);
|
||||
|
||||
// PNG: grayscale, so load_texture_image_data passes it through verbatim.
|
||||
const fs::path pack_png = pack_dir / "grain.png";
|
||||
write_gray_png(pack_png, 0x40);
|
||||
const std::vector<unsigned char> png_bytes = read_file_bytes(pack_png);
|
||||
REQUIRE_FALSE(png_bytes.empty());
|
||||
write_bytes(hand_root.path() / "grain.png", png_bytes);
|
||||
|
||||
// BMP: not a PNG, so it goes through wxImage and is re-encoded; the same file must give the same
|
||||
// bytes wherever it sits.
|
||||
const std::vector<unsigned char> bmp_bytes = make_gray_bmp(4, 4, 0x40);
|
||||
const fs::path pack_bmp = pack_dir / "bump.bmp";
|
||||
write_bytes(pack_bmp, bmp_bytes);
|
||||
const fs::path hand_bmp = hand_root.path() / "bump.bmp";
|
||||
write_bytes(hand_bmp, bmp_bytes);
|
||||
|
||||
// The collector lists both pack files, so these are the paths a pack would serve.
|
||||
std::string collect_error;
|
||||
const std::vector<std::string> pack_files =
|
||||
collect_resource_texture_files(pack_dir.string(), pack_root.string(), collect_error);
|
||||
REQUIRE(collect_error.empty());
|
||||
CHECK(pack_files.size() == 2);
|
||||
|
||||
std::string pack_error;
|
||||
std::string hand_error;
|
||||
|
||||
const auto pack_png_decoded = GUI::load_texture_image_data(pack_png.string(), pack_error);
|
||||
const auto hand_png_decoded = GUI::load_texture_image_data((hand_root.path() / "grain.png").string(), hand_error);
|
||||
REQUIRE(pack_png_decoded != nullptr);
|
||||
REQUIRE(hand_png_decoded != nullptr);
|
||||
CHECK(*pack_png_decoded == png_bytes); // supported PNG is passed through unchanged
|
||||
CHECK(*pack_png_decoded == *hand_png_decoded);
|
||||
|
||||
const auto pack_bmp_decoded = GUI::load_texture_image_data(pack_bmp.string(), pack_error);
|
||||
const auto hand_bmp_decoded = GUI::load_texture_image_data(hand_bmp.string(), hand_error);
|
||||
REQUIRE(pack_bmp_decoded != nullptr);
|
||||
REQUIRE(hand_bmp_decoded != nullptr);
|
||||
REQUIRE_FALSE(pack_bmp_decoded->empty());
|
||||
CHECK(*pack_bmp_decoded == *hand_bmp_decoded);
|
||||
}
|
||||
|
||||
TEST_CASE("Reinstalling a pack with different bytes at the same path changes the decoded content",
|
||||
"[PluginResourceTexture][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-reinstall");
|
||||
|
||||
const std::string plugin_key = "Reinstall_Pack";
|
||||
const fs::path plugin_dir =
|
||||
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Reinstall Pack", default_texture_dir_body()));
|
||||
const fs::path texture = plugin_dir / "textures" / "grain.png";
|
||||
write_gray_png(texture, 0x10);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, plugin_key, error));
|
||||
|
||||
auto loaded = plugin_textures(true);
|
||||
REQUIRE(loaded.size() == 1);
|
||||
const std::string path = loaded.front().path;
|
||||
|
||||
std::string decode_error;
|
||||
const auto first = GUI::load_texture_image_data(path, decode_error);
|
||||
REQUIRE(first != nullptr);
|
||||
|
||||
REQUIRE(manager.unload_plugin(plugin_key));
|
||||
|
||||
// The reinstalled pack writes different bytes to the same file path.
|
||||
write_gray_png(texture, 0xE0);
|
||||
REQUIRE(load_and_wait(manager, plugin_key, error));
|
||||
|
||||
loaded = plugin_textures(true);
|
||||
REQUIRE(loaded.size() == 1);
|
||||
CHECK(loaded.front().path == path);
|
||||
|
||||
const auto second = GUI::load_texture_image_data(path, decode_error);
|
||||
REQUIRE(second != nullptr);
|
||||
CHECK(*first != *second);
|
||||
}
|
||||
|
||||
// ── Wheel metadata ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("A minimal resource-texture wheel is accepted", "[PluginResourceTexture][Wheel]")
|
||||
{
|
||||
ScopedTemporaryDir dir("texture-wheel");
|
||||
const fs::path wheel = dir.path() / "texture_pack-1.0-py3-none-any.whl";
|
||||
|
||||
const std::string dist_info = "texture_pack-1.0.dist-info";
|
||||
const std::string metadata =
|
||||
"Metadata-Version: 2.1\n"
|
||||
"Name: texture_pack\n"
|
||||
"Version: 1.0\n"
|
||||
"Summary: Resource texture pack fixture\n"
|
||||
"Author: tests\n";
|
||||
const std::string wheel_metadata =
|
||||
"Wheel-Version: 1.0\n"
|
||||
"Generator: manual\n"
|
||||
"Root-Is-Purelib: true\n"
|
||||
"Tag: py3-none-any\n";
|
||||
const std::string record = dist_info + "/METADATA,,\n" + dist_info + "/WHEEL,,\n" + dist_info + "/RECORD,,\n";
|
||||
const std::string top_level = "texture_pack\n";
|
||||
|
||||
{
|
||||
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
|
||||
zipper.add_entry(dist_info + "/METADATA", metadata.data(), metadata.size());
|
||||
zipper.add_entry(dist_info + "/WHEEL", wheel_metadata.data(), wheel_metadata.size());
|
||||
zipper.add_entry(dist_info + "/RECORD", record.data(), record.size());
|
||||
// read_wheel_plugin_metadata leaves `error` set when an optional top_level.txt is missing.
|
||||
zipper.add_entry(dist_info + "/top_level.txt", top_level.data(), top_level.size());
|
||||
zipper.finalize();
|
||||
}
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
std::string error;
|
||||
REQUIRE(read_wheel_plugin_metadata(wheel, descriptor, error));
|
||||
INFO("wheel metadata error: " << error);
|
||||
CHECK(error.empty());
|
||||
CHECK(descriptor.name == "texture_pack");
|
||||
CHECK(descriptor.version == "1.0");
|
||||
CHECK(descriptor.entry_package == "texture_pack");
|
||||
}
|
||||
|
||||
// ── .3mf round trip ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("A project keeps a pack texture after the pack is uninstalled",
|
||||
"[PluginResourceTexture][PluginLifecycle][Python][3mf]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("texture-3mf");
|
||||
|
||||
const std::string plugin_key = "Texture_Pack_3mf";
|
||||
const fs::path plugin_dir =
|
||||
write_plugin(data_dir_guard, plugin_key, make_resource_texture_source("Texture Pack", default_texture_dir_body()));
|
||||
write_gray_png(plugin_dir / "textures" / "grain.png", 0x60);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
install_resource_texture_wiring();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, plugin_key, error));
|
||||
|
||||
const auto loaded = plugin_textures(true);
|
||||
REQUIRE(loaded.size() == 1);
|
||||
|
||||
std::string decode_error;
|
||||
const auto pack_bytes = GUI::load_texture_image_data(loaded.front().path, decode_error);
|
||||
REQUIRE(pack_bytes != nullptr);
|
||||
REQUIRE_FALSE(pack_bytes->empty());
|
||||
|
||||
// Build a project the way the gizmo would: the layer carries the pack texture's bytes.
|
||||
Model model;
|
||||
ModelObject* object = model.add_object();
|
||||
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
||||
object->add_instance();
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.name = loaded.front().name;
|
||||
layer.path = loaded.front().path;
|
||||
layer.image_data = pack_bytes;
|
||||
volume->texture_displacement_layers.push_back(layer);
|
||||
|
||||
ScopedTemporaryFile first(".3mf");
|
||||
store_project(model, first.string());
|
||||
|
||||
// Uninstall the pack; the project still has to open and re-save with the same image bytes.
|
||||
REQUIRE(manager.unload_plugin(plugin_key));
|
||||
CHECK(plugin_textures(true).empty());
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_tex_pack_dst");
|
||||
Model reloaded;
|
||||
REQUIRE(load_project(first.string(), reloaded, backup_dir));
|
||||
REQUIRE(reloaded.objects.size() == 1);
|
||||
REQUIRE(reloaded.objects.front()->volumes.size() == 1);
|
||||
const std::vector<TextureDisplacementLayer>& reloaded_layers =
|
||||
reloaded.objects.front()->volumes.front()->texture_displacement_layers;
|
||||
REQUIRE(reloaded_layers.size() == 1);
|
||||
REQUIRE(reloaded_layers.front().image_data);
|
||||
CHECK(*reloaded_layers.front().image_data == *pack_bytes);
|
||||
|
||||
ScopedTemporaryFile second(".3mf");
|
||||
store_project(reloaded, second.string());
|
||||
|
||||
ScopedTemporaryDir backup_dir2("orca_tex_pack_dst2");
|
||||
Model reloaded_again;
|
||||
REQUIRE(load_project(second.string(), reloaded_again, backup_dir2));
|
||||
REQUIRE(reloaded_again.objects.size() == 1);
|
||||
REQUIRE(reloaded_again.objects.front()->volumes.size() == 1);
|
||||
const std::vector<TextureDisplacementLayer>& again_layers =
|
||||
reloaded_again.objects.front()->volumes.front()->texture_displacement_layers;
|
||||
REQUIRE(again_layers.size() == 1);
|
||||
REQUIRE(again_layers.front().image_data);
|
||||
CHECK(*again_layers.front().image_data == *pack_bytes);
|
||||
}
|
||||
Reference in New Issue
Block a user