Compare commits

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

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

The loading page now reports in from its OnInit, the way the other wizard
pages request what they need, and the host sends the message once both
the data is ready and the page has reported; every instance of the page
reports again, so one that replaced an earlier one gets its own message.
2026-10-14 19:08:07 +08:00
54 changed files with 1068 additions and 8189 deletions

No files matched your search

-131
View File
@@ -1,131 +0,0 @@
# Color picker
The color picker is a local HTML dialog hosted by `ColorPickerDialog`, a
`WebViewHostDialog` subclass. Its header and implementation own the ordered
selection contract and shared filament conversion. Bridge validation, favorites
persistence, placement and gettext strings remain private to the implementation.
Its colors use the existing `ColorRGBA` utility.
It uses a native title bar and fixed window border, allowing dragging but no
manual resizing or maximization. Native title-bar colors use the application's
shared dialog dark-theme update on creation and theme changes.
Like SpeedDial, the host disables browser zoom accelerators, restores native
WebView focus on activation and readiness, and synchronizes layout and the macOS
viewport before repainting the backend. Activation never dismisses this modal
dialog; focus restoration preserves an active editor and its partial input.
The initialized page reports
its intrinsic content height; native code validates the current page identifier
and bounded numeric height, converts CSS pixels to device-independent window
size, and fits within the display's work area. Identical sizes are ignored. The
page observes content rather than viewport height, so resizing does not create
a feedback loop; oversized content remains scrollable.
The panel snapshots the triggering color or More Colors button's screen rectangle.
It aligns below the button, flips above when needed, and clamps to that button's
display work area on initial display. Later content and DPI changes update only
the client size, preserving the position chosen by dragging the title bar.
wxGTK clears the dialog's default
parent-centering constraint and reapplies its native position after showing,
bypassing wxGTK's requested-position cache. Absolute top-level placement remains
subject to the window manager; Wayland compositors control it. The official filament palette's
existing placement is independent of this custom panel.
A solid selection contains one color; a gradient contains exactly two ordered
endpoints. The bridge represents these as `{"type":"solid|gradient",
"colors":["#RRGGBBAA", ...]}`. RGB input is also accepted and becomes opaque.
Native validation rejects malformed hex and incorrect cardinality.
The project filament color action and the official filament picker's More Colors
action enable two-endpoint gradients and independent endpoint alpha. The preset
combo box reads project colors in their stored vector order locally and applies
changes through `sync_colour_config`,
retaining the existing RGB/RGBA profile/project formats and dirty marking.
Sidebar gradient and segmented multicolor swatches draw colors in the supplied
order without sorting them.
Opaque colors use `#RRGGBB`; nonopaque colors use `#RRGGBBAA`, including fully
transparent alpha `00`. Initial selections, alpha-only edits and cancellation
retain stored alpha. Ordered
custom results bypass the official palette's `FilamentColor` set. Choosing an
official swatch retains its existing palette behavior.
The Linux sidebar's Change extruder color menu uses this same custom editor and
the filament swatch anchor, including alpha, ordered gradients and configuration syncing.
Striped colors and gradients with more than two endpoints cannot be represented
by this editor. A notice explains that the current selection is retained; the
editor starts from the first color, and unchanged confirmation is a no-op.
Cancellation is also a no-op. If the WebView backend cannot initialize, a local
unavailable panel offers Cancel and Escape, without opening a system color picker.
Generic color option fields use an ordinary color button on every platform and
open the same panel with gradients and alpha disabled. The panel is anchored to
the field button. Empty or invalid field values remain undefined (the empty
string), distinct from opaque black. Right-click resets to undefined. Cancel does
not notify the field; confirmation and reset notify only when its RGB string
changes. Programmatic assignments remain silent, and reading a field never
writes legacy favorites.
AMS material settings retain their fixed/AMS swatches and first-level popup. Only
its custom-color action opens the solid, opaque editor, anchored to that custom
control. The transient popup releases its grab while the modal is open and returns
to the same selection on cancellation. Confirmation uses the existing default-color
and packed RGBA event path. Device permission/status checks remain at the first-level
entry.
Texture import's color-mapping Add Material action opens a solid, opaque editor
anchored to the mapping row. The existing filament limit is checked before opening.
The first-level material popup closes as an action; its callback is copied before
dismissal, so popup destruction does not invalidate the modal's completion path.
Confirmation invokes the existing virtual-filament callback once; cancellation
adds no material.
Native gettext strings are installed at document start. Static page labels and
palette names use stable English keys; RAL codes, channel symbols, and numeric
labels remain data. The palette color values do not depend on the UI language.
The application locale also sets the document language. Numeric editor validation
uses the application's gettext messages, rather than browser language defaults;
missing translations fall back to the stable English keys.
The pure color/state regressions run with
`node --test resources/web/dialog/ColorPickerDialog/colorpicker.test.js`.
Optional real-browser regressions live beside them in
`colorpicker.browser.test.js`. They use an existing Playwright installation
(`ORCA_PLAYWRIGHT_MODULE` can name its module path) and an installed browser
(`ORCA_BROWSER_EXECUTABLE` can name its executable), without adding a production
dependency. They cover bridge initialization, capability combinations, invalid
editor recovery, ordered endpoint alpha, favorites, gettext keys, and live theme
changes. These browser checks complement native contract/persistence tests; they
do not establish the full native modal confirmation and favorites journey.
Gradient and alpha capabilities are independent and default off. Unsupported
gradients are rejected; disabled alpha normalizes the selected result to opaque.
Favorites retain both capabilities independently of the current caller. The page
stores alpha as a byte and only converts to percentage when displaying or editing
opacity, so switching endpoints does not change alpha precision.
The page starts disabled. Each document supplies a page identifier in its ready
message, receives native initialization, and acknowledges it before native code
accepts confirmation or favorite updates. Repeated initialization does not reset
edits, and messages from an obsolete document are ignored. Native WebView message
handling is deferred and guarded by a lifetime token. Only a validated confirmation
sets the optional result. Cancel, Escape and native window close leave it empty.
Favorites belong to application configuration, separately from the selected result,
in the editor configuration returned by `AppConfig::config_path()` under `data_dir()`.
The `color_picker` section has string `version` equal to `1` and a `favorites`
string containing a JSON array of typed selections. Native helpers normalize and
deduplicate this array in order and limit it to 24 entries. Updates replace the
entire section and immediately call `AppConfig::save()`, so accepted favorite
changes survive cancellation. Empty arrays are saved explicitly. Core `AppConfig`
only stores the raw section; color validation belongs to the GUI boundary.
When the section is absent, valid solid colors from `custom_color_list` are imported
once, without changing that legacy section. The legacy native picker saves decimal
`r,g,b,a` byte strings; hexadecimal RGB and RGBA strings are also supported. Migration
strictly validates all four decimal channels and preserves alpha. Gradient strings
and malformed values are ignored. The new version and an empty array are
saved even when there is nothing to import. A present section with an unknown or
missing version is left untouched and disables favorite writes. Malformed version
1 data is displayed as empty and remains on disk until the user explicitly saves
a valid replacement. Other configuration sections and project/profile color
formats are independent of this storage.
Interface colors consume the host's `data-orca-theme` and `--orca-*` contract through
shared styles. The picker opts into scoped global control roles. Application theme
changes update UI surfaces without reloading the page or changing color data.
-233
View File
@@ -1,233 +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.
### External spool slots
External/manual spools are virtual trays in `MachineObject::vt_slot`, separate from the trays owned
by `DevFilaSystem`. Their stable id is `vt_id = VIRTUAL_TRAY_MAIN_ID - extruder_id`: 255 for
extruder 0, 254 for extruder 1, then 253, 252, and so on. The index in `vt_slot` is therefore
`VIRTUAL_TRAY_MAIN_ID - vt_id`, not the order in which a printer happens to report trays.
For non-Bambu printers, slot topology comes from the live `DevExtderSystem` extruder count, not from
external-spool telemetry. `MachineObject::ensure_external_virtual_tray_slots()` grows `vt_slot` to
one entry per extruder and enables virtual-tray support even when the agent reports no `vir_slot`,
`vt_tray`, or external-spool data. It only adds missing entries, so reported tray metadata is kept.
The shared ensure runs after push-status parsing has updated the extruder count and after the generic
AMS/agent-filament parsers. Thus agents may report external-spool metadata, but do not have to invent
empty slots just to make each toolhead's external-spool row visible.
When a payload does report virtual trays, `vir_slot` entries are placed by their id and missing
indices are filled with placeholders. Generic agent `external` entries are likewise placed by
extruder id, so sparse or out-of-order reports do not shift a spool onto another toolhead. An empty
placeholder represents the slot's existence, not a claim that the printer reported a loaded spool.
Bambu keeps its fixed 255/254 virtual-tray pair and does not use generic slot synthesis.
## Mapping
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
index. Bambu mapping computes `ams_id * 4 + slot_id` for its fixed-width units; generic mapping in
`DevFilaSystem` and the sidebar matcher uses the count of lanes in preceding units plus the slot id.
The two agree for Bambu units and differ 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.
The sidebar sync matcher uses the same identities. Bambu retains its fixed left/right AMS and
external-spool choices. For a generic printer, a full sync considers AMS units according to their
bound extruder ids and considers every external slot by `255 - vt_id`; a split left/right mapping
still restricts itself to the corresponding legacy extruders 0 and 1. Generic AMS lanes use the
cumulative tray index above, while their `ams_id`/`slot_id` pair remains the address used to update
the matching sidebar entry. The runtime filament-list configs also carry a transient device-extruder
association so generic entries are grouped under the correct toolhead after sync; that association
is removed before the configs enter the slicing pipeline. Bambu keeps the existing key-based
left/right grouping.
### 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. Slot
topology is grown from the live device extruder count, while this recognition range still uses the
preset count; the preset is expected to match the connected device. A mismatch can therefore make
the recognized virtual-id range disagree with `vt_slot`, and is reported by the existing count
warning 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 builds external-spool entries from `MachineObject::vt_slot`; generic slot
synthesis keeps one empty entry available for each live toolhead even when the printer does not
report external-spool state.
- 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.
- **Runtime counts.** The agent reports toolhead count and AMS unit/tray data. The shared model derives
generic external-spool slot count from the parsed live toolhead count; nothing caps the number of
toolheads or generic slots except the id range and 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
- [`DevMapping.cpp`](../../src/slic3r/GUI/DeviceCore/DevMapping.cpp) — filament-to-device tray matching
for Bambu and generic external/AMS slots
- [`PresetBundle.cpp`](../../src/libslic3r/PresetBundle.cpp) — per-extruder grouping of the transient
device filament list
- [`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, external-slot topology, 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)
-1
View File
@@ -325,7 +325,6 @@ src/slic3r/GUI/BaseTransparentDPIFrame.cpp
src/slic3r/GUI/CloneDialog.cpp
src/slic3r/GUI/ColorDecomposeDialog.cpp
src/slic3r/GUI/ColorDecomposeSupport.cpp
src/slic3r/GUI/ColorPickerDialog.cpp
src/slic3r/GUI/ConfigWizard.cpp
src/slic3r/GUI/DeviceCore/DevCalib.cpp
src/slic3r/GUI/DeviceCore/DevUpgrade.cpp
@@ -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; }
-9
View File
@@ -55,13 +55,6 @@
/* Speed dial icon tile treatment */
--speed-tile-bg: #efefef;
--speed-tile-border: #d8d8d8;
/* Transparency and control markers are UI surfaces, never color data. */
--checker-light: #eeeeee;
--checker-dark: #aaaaaa;
--color-marker-outline: #ffffff;
--color-marker-shadow: rgba(0, 0, 0, 0.7);
--control-invalid: var(--plugin-status-danger);
}
:root[data-orca-theme="dark"] {
@@ -100,8 +93,6 @@
/* Speed dial icon tile treatment */
--speed-tile-bg: #34343b;
--speed-tile-border: #50505a;
--checker-light: #68686f;
--checker-dark: #414148;
}
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
+53 -162
View File
@@ -4511,53 +4511,6 @@ static void warn_ambiguous_filament_id_match(const PresetCollection &filaments,
<< "\", also matches " << others;
}
// Orca: resolve a compatible system preset from a printer-reported filament type. Prefers an exact
// "Generic <type>" match, then the longest generic whose filament_type appears as a whole word in
// the reported type (e.g. "ASA" in "ASA Sparkle"). On success `generic_name_out` holds the
// "Generic <...>" name used for compatibility messages. Shared by the known-filament_id lookup and
// the data-bearing-but-unmapped tray path so both pick the same preset.
static PresetCollection::Iterator find_generic_preset_for_type(PresetCollection &filaments, const std::string &filament_type, std::string &generic_name_out)
{
if (filament_type.empty())
return filaments.end();
const std::string generic_name = "Generic " + filament_type;
auto iter = std::find_if(filaments.begin(), filaments.end(), [&generic_name](const Preset &f) {
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, generic_name);
});
if (iter != filaments.end()) {
generic_name_out = generic_name;
return iter;
}
// Similarity fallback: find a generic preset whose filament_type
// appears as a whole word in the reported type (e.g. "ASA" in "ASA Sparkle").
auto upper_type = boost::to_upper_copy(filament_type);
auto contains_word = [](const std::string& haystack, const std::string& needle) {
auto pos = haystack.find(needle);
while (pos != std::string::npos) {
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
bool end_ok = (pos + needle.size() >= haystack.size() ||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
if (start_ok && end_ok)
return true;
pos = haystack.find(needle, pos + 1);
}
return false;
};
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
size_t best_len = 0;
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
continue;
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
iter = it;
best_len = preset_type.size();
generic_name_out = "Generic " + it->config.opt_string("filament_type", 0u);
}
}
return iter;
}
void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
{
combox_info.clear();
@@ -4613,9 +4566,7 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfig *,std::string>> &unknowns, bool use_map, std::map<int, AMSMapInfo> &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": use_map %1% enable_append %2% color_only %3% start_presets %4% start_colors %5%")
% use_map % enable_append % color_only % this->filament_presets.size()
% project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append;
std::vector<std::string> ams_filament_presets;
std::vector<std::string> ams_filament_colors;
std::vector<std::string> ams_filament_color_types;
@@ -4627,10 +4578,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
bool valid{false};
bool is_map{false};
bool is_placeholder{false};
// Orca: set for a tray that reports filament data (type and/or loaded flag) but carries
// no preset id. It is not `valid` (no id to map through), yet it still holds a real spool
// and is eligible for the same append step as a mapped tray.
bool has_reported_data{false};
std::string filament_color = "";
std::string filament_color_type = "";
std::string filament_preset = "";
@@ -4644,32 +4591,12 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto filament_id = ams.opt_string("filament_id", 0u);
auto filament_color = ams.opt_string("filament_colour", 0u);
auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
auto filament_type = ams.opt_string("filament_type", 0u);
auto filament_exist = ams.has("filament_exist") && ams.opt_bool("filament_exist", 0u);
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
auto ams_id = ams.opt_string("ams_id", 0u);
auto slot_id = ams.opt_string("slot_id", 0u);
auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u);
// Real reported filament data: the device named a type or flagged the slot loaded. A
// topology-only synthetic virtual tray carries neither, so it must not become a filament.
bool has_reported_data = !is_placeholder && filament_id.empty() && (!filament_type.empty() || filament_exist);
AmsInfo ams_info;
ams_info.valid = !filament_id.empty();
ams_info.is_placeholder = is_placeholder;
ams_info.has_reported_data = has_reported_data;
ams_info.filament_color = filament_color;
// Only the append path of a data-bearing, id-less tray reads these; the known-id path keeps
// taking its color and color type from ams_filament_colors/ams_filament_color_types.
if (has_reported_data) {
if (ams_info.filament_color.empty())
ams_info.filament_color = "#CECECE";
ams_info.filament_color_type = filament_color_type.empty() ? "1" : filament_color_type;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% valid %7% placeholder %8% reported_data %9% changed %10%")
% index % ams_id % slot_id % filament_id % filament_type % filament_color
% (!filament_id.empty()) % is_placeholder % has_reported_data % filament_changed;
ams_infos.push_back(std::move(ams_info));
ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color});
AMSMapInfo temp = {ams_id, slot_id};
ams_array_maps.push_back(temp);
index++;
@@ -4680,55 +4607,20 @@ 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("Generic PLA");//for unknow matieral
auto default_unknown_color = "#CECECE";
ams_filament_colors.push_back(default_unknown_color);
ams_filament_color_types.push_back("1");
if (filament_multi_color.size() == 0) {
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({});
} else if (use_map) {
if (has_reported_data) {
// Orca: the tray reports a filament but no preset id, so it never reaches the
// filament_id lookup. Resolve the same generic preset the id path would pick
// from the reported type and keep the reported color; the append step below (if
// enabled) then turns it into a sidebar filament. Without a generic match the
// slot is dropped rather than invented, exactly like an empty tray.
std::string resolved_generic;
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
if (generic == filaments.end()) {
// Reported loaded but no usable type: fall back to the first compatible
// generic, matching the id path's fallback for an unknown model.
generic = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, "Generic ");
});
}
if (generic != filaments.end()) {
auto append_color = filament_color.empty() ? std::string("#CECECE") : filament_color;
ams_filament_presets.push_back(generic->name);
ams_filament_colors.push_back(append_color);
ams_filament_color_types.push_back(filament_color_type.empty() ? "1" : filament_color_type);
if (filament_multi_color.size() == 0)
filament_multi_color.push_back(append_color);
ams_multi_color_filment.push_back(filament_multi_color);
continue;
}
ams_filament_presets.push_back("");
ams_filament_colors.push_back("");
ams_filament_color_types.push_back("");
ams_multi_color_filment.push_back({});
} else {
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
auto default_unknown_color = "#CECECE";
ams_filament_colors.push_back(default_unknown_color);
ams_filament_color_types.push_back("1");
if (filament_multi_color.size() == 0) {
filament_multi_color.push_back(default_unknown_color);
}
ams_multi_color_filment.push_back(filament_multi_color);
}
}
continue;
}
@@ -4740,19 +4632,49 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
continue;
}
bool has_type = false;
auto filament_type = ams.opt_string("filament_type", 0u);
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
std::string resolved_generic;
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
if (!filament_type.empty())
if (!filament_type.empty()) {
auto original_type = filament_type;
filament_type = "Generic " + filament_type;
if (generic != filaments.end()) {
iter = generic;
filament_type = resolved_generic;
iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
return f.is_compatible && f.is_system
&& boost::algorithm::starts_with(f.name, filament_type);
});
if (iter == filaments.end()) {
// Similarity fallback: find a generic preset whose filament_type
// appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle").
auto upper_type = boost::to_upper_copy(original_type);
auto contains_word = [](const std::string& haystack, const std::string& needle) {
auto pos = haystack.find(needle);
while (pos != std::string::npos) {
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
bool end_ok = (pos + needle.size() >= haystack.size() ||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
if (start_ok && end_ok)
return true;
pos = haystack.find(needle, pos + 1);
}
return false;
};
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
size_t best_len = 0;
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
continue;
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
iter = it;
best_len = preset_type.size();
filament_type = "Generic " + it->config.opt_string("filament_type", 0u);
}
}
}
}
if (iter == filaments.end()) {
// Prefer old selection
@@ -4912,8 +4834,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
}
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": color_only mapped_any %1% maps %2% entries %3%")
% mapped_any % maps.size() % ams_infos.size();
filament_color->values = exist_colors;
ams_multi_color_filment = exist_multi_color_filment;
merge_info.merges.clear();
@@ -4973,18 +4893,13 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
}
}
for (size_t i = 0; i < ams_infos.size(); i++) {// check append
// A mapped tray (valid id) and a data-bearing tray without an id both append the same
// way; only a tray with neither contributes nothing.
if (ams_infos[i].valid || ams_infos[i].has_reported_data) {
if (ams_infos[i].valid) {
if (i >= ams_filament_presets.size()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)";
continue;
}
ams_infos[i].filament_preset = ams_filament_presets[i];
ams_infos[i].mutli_filament_color = ams_multi_color_filment[i];
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% mapped %2% preset %3% color %4% valid %5% reported_data %6%")
% i % ams_infos[i].is_map % ams_infos[i].filament_preset % ams_infos[i].filament_color
% ams_infos[i].valid % ams_infos[i].has_reported_data;
if (!ams_infos[i].is_map) {
need_append_colors.emplace_back(ams_infos[i]);
ams_filament_colors[i] = "";
@@ -4994,7 +4909,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
}
}
else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% dropped (no valid id or reported data)") % i;
ams_filament_colors[i] = "";
ams_filament_color_types[i] = "";
ams_filament_presets[i] = "";
@@ -5012,7 +4926,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
[](std::vector<std::string> &value) { return value.empty(); }),
ams_multi_color_filment.end());
if (need_append_colors.size() > 0 && enable_append) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append candidates %1% (start existing %2%)") % need_append_colors.size() % exist_filament_presets.size();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append";
auto get_idx_in_array = [](std::vector<std::string> &presets, std::vector<std::string> &colors, const std::string &preset, const std::string &color) -> int {
for (size_t i = 0; i < presets.size(); i++) {
if (presets[i] == preset && colors[i] == color) {
@@ -5023,22 +4937,17 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
};
for (size_t i = 0; i < need_append_colors.size(); i++){
if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append capped at %1%, %2% candidate(s) left") % MAXIMUM_AMS_SYNC_FILAMENT_NUMBER % (need_append_colors.size() - i);
break;
}
auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color);
if (idx >= 0) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append dedup preset %1% color %2% already at %3%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % idx;
continue;
}
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
exist_colors.push_back(need_append_colors[i].filament_color);
exist_color_types.push_back(need_append_colors[i].filament_color_type);
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": appended preset %1% color %2% type %3% -> slot %4%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % need_append_colors[i].filament_color_type % (exist_filament_presets.size() - 1);
}
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append skipped candidates %1% enable_append %2%") % need_append_colors.size() % enable_append;
}
filament_color->values = exist_colors;
filament_color_type->values = exist_color_types;
@@ -5075,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]);
@@ -5103,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;
@@ -5154,7 +5059,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
// Update ams_multi_color_filment
update_filament_multi_color();
update_multi_material_filament_presets();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finish sync ams list, end_presets %1% end_colors %2%") % this->filament_presets.size() % filament_color->values.size();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish sync ams list";
return this->filament_presets.size();
}
@@ -5233,20 +5138,6 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
if (extruder_nums > 1) {
filament_infos.resize(extruder_nums, std::vector<DynamicPrintConfig>());
for (auto ams_item : filament_ams_list) {
// A generic slot carries the extruder it actually belongs to, so it lands in that
// extruder's bucket instead of the legacy left/right pair. Bambu entries (and anything
// without the field) keep the existing 0x10000 key convention.
const bool has_extruder_id = ams_item.second.has("device_tray_extruder_id");
const int entry_extruder = has_extruder_id ? ams_item.second.opt_int("device_tray_extruder_id") : -1;
if (has_extruder_id) {
if (entry_extruder >= 0 && entry_extruder < extruder_nums) {
// Drop the transient association before the config reaches the slicing pipeline.
DynamicPrintConfig entry = ams_item.second;
entry.erase("device_tray_extruder_id");
filament_infos[entry_extruder].push_back(std::move(entry));
}
continue;
}
if (ams_item.first & 0x10000) { // right
filament_infos[1].push_back(ams_item.second);
} else { // left
-2
View File
@@ -129,8 +129,6 @@ set(SLIC3R_GUI_SOURCES
GUI/Shortcuts.hpp
GUI/SpeedDialDialog.cpp
GUI/SpeedDialDialog.hpp
GUI/ColorPickerDialog.cpp
GUI/ColorPickerDialog.hpp
GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
+44 -30
View File
@@ -1,9 +1,4 @@
#include "AMSMaterialsSetting.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <variant>
#include <cstdint>
#include "ExtrusionCalibration.hpp"
#include "MsgDialog.hpp"
#include "GUI_App.hpp"
@@ -41,9 +36,11 @@
#include <cmath>
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include "slic3r/GUI/Widgets/StaticBox.hpp"
#include <wx/colordlg.h>
#include <wx/dcclient.h>
#include <wx/dc.h>
#include <wx/colour.h>
#include <wx/colourdata.h>
#include <wx/dcgraph.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
@@ -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)
+1
View File
@@ -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
-92
View File
@@ -1,92 +0,0 @@
#pragma once
#include "libslic3r/Color.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <array>
#include <cstddef>
#include <variant>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <wx/gdicmn.h>
class wxWindow;
namespace Slic3r::GUI {
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
// and never offers more, so the picker, favorites and project colors all share one limit.
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
using ColorGradient = std::vector<ColorRGBA>;
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
struct ColorPickerOptions {
bool allow_gradient = false;
bool allow_alpha = false;
};
// Project storage preserves direction; FilamentColor's set is only for the
// official palette and cannot represent ordered custom gradients.
struct FilamentColorPickerValue {
std::vector<std::string> colors;
bool gradient = false;
std::string primary;
};
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
class ColorPickerDialog : public WebViewHostDialog
{
public:
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
~ColorPickerDialog() override;
const std::optional<ColorSelection>& selection() const { return m_selection; }
private:
void add_user_scripts() override;
void on_script_message(const nlohmann::json& payload) override;
void handle_web_command(const nlohmann::json& payload);
void send_initial_state();
void finish(int return_code);
void position_panel();
void move_to_anchor();
void resize_to_content(int height);
void focus_webview();
void repaint_webview();
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_sys_color_changed() override;
ColorSelection m_initial;
ColorPickerOptions m_options;
std::optional<ColorSelection> m_selection;
std::vector<ColorSelection> m_favorites;
std::string m_page_id;
bool m_preserve_multi_color = false;
bool m_init_sent = false;
bool m_page_ready = false;
bool m_closing = false;
bool m_favorites_writable = false;
bool m_sidebar_open = false;
std::string m_palette;
bool m_initial_positioned = false;
wxRect m_anchor_rect;
wxRect m_work_area;
int m_content_height = 360;
int m_content_width = 330;
bool m_positioning = false;
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
};
} // namespace Slic3r::GUI
+2 -5
View File
@@ -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>>;
+23 -160
View File
@@ -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++)
@@ -947,21 +866,6 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
}
}
}
// Give every live device extruder an external virtual tray slot (non-Bambu) so a plugin agent
// that reaches the model only through this parser also gets the shared external-slot topology.
if (obj)
obj->ensure_external_virtual_tray_slots();
// Diagnostic: summarize the AMS state this production parse produced, so the agent boundary
// (submit_ams_payload) box/tray counts can be correlated with what actually landed in the model.
if (obj && system) {
size_t parsed_trays = 0;
for (const auto& kv : system->amsList)
parsed_trays += kv.second->GetTrays().size();
BOOST_LOG_TRIVIAL(info) << "DevFilaSystemParser::ParseV1_0: parsed units " << system->amsList.size()
<< " trays " << parsed_trays;
}
}
static DevAms::AmsType ams_type_from_string(const std::string& s)
@@ -972,23 +876,6 @@ static DevAms::AmsType ams_type_from_string(const std::string& s)
return DevAms::AMS; // default
}
void grow_virtual_tray_slots(std::vector<DevAmsTray>& slots, std::size_t tray_id)
{
// Address a slot by its virtual tray id, never by a raw vector index: the slot for `tray_id`
// lives at index VIRTUAL_TRAY_MAIN_ID - tray_id and placeholder entries carry
// id = VIRTUAL_TRAY_MAIN_ID - index. Append-only, so a slot that already holds reported
// metadata keeps it and a sparse/out-of-order report cannot shift the id <-> extruder mapping.
if (tray_id > static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID))
return;
const std::size_t slot_index = static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID) - tray_id;
while (slots.size() <= slot_index) {
slots.emplace_back(std::to_string(VIRTUAL_TRAY_MAIN_ID - static_cast<int>(slots.size())));
// A filler only makes the slot addressable; it is topology, not a reported spool. Callers
// overwrite the target entry with reported metadata, which clears this marker.
slots.back().is_slot_placeholder = true;
}
}
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
{
if (!system || !data.is_object())
@@ -1026,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);
@@ -1110,21 +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());
@@ -1133,26 +1013,9 @@ 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));
}
// Place each reported spool at its own index (VIRTUAL_TRAY_MAIN_ID - extruder_id) instead of
// appending in report order: a sparse or out-of-order report must not compact the vector and
// shift the slot -> extruder association.
obj->vt_slot.clear();
for (const auto& entry : external_slots)
{
// entry.first is the extruder id; its virtual tray id is VIRTUAL_TRAY_MAIN_ID - extruder_id
// and the slot lives at vector index extruder_id (the id <-> index mapping grow applies).
grow_virtual_tray_slots(obj->vt_slot, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - entry.first));
obj->vt_slot[entry.first] = entry.second;
obj->vt_slot.push_back(tray);
}
}
// Fill any extruder still missing a slot so the model always has one external slot per live
// extruder, even when the agent reported no external data at all. Preserves reported metadata.
if (obj)
obj->ensure_external_virtual_tray_slots();
}
}
+3 -14
View File
@@ -7,7 +7,6 @@
#include "DevFilaSwitch.h"
#include "DevUtil.h"
#include <cstddef>
#include <map>
#include <set>
#include <string>
@@ -83,7 +82,7 @@ public:
wxColour wx_color;
bool is_bbl;
bool is_exists = false;
bool is_slot_placeholder = false; // Orca: True for synthetic topology slots or empty reported trays
bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
int hold_count = 0;
int remain = 0; // filament remain: 0 ~ 100
@@ -115,16 +114,6 @@ public:
std::optional<DevFilamentDryingPreset> get_ams_drying_preset() const;
};
/**
* Ensure the virtual-tray vector can address the slot for a virtual tray id. The slot's index is
* `VIRTUAL_TRAY_MAIN_ID - tray_id` and placeholder entries carry id = VIRTUAL_TRAY_MAIN_ID - index.
* Existing entries are left untouched, so a slot already populated with reported filament data
* keeps it; callers then assign the reported tray at index `VIRTUAL_TRAY_MAIN_ID - tray_id`.
* Appended placeholders are marked `is_slot_placeholder`, so consumers can tell a synthetic
* topology filler from a spool the printer actually reported (overwriting the entry clears it).
*/
void grow_virtual_tray_slots(std::vector<DevAmsTray>& slots, std::size_t tray_id);
/**
* DevAms - Represents a single AMS (Automatic Material System) unit.
*
@@ -359,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();
@@ -444,4 +433,4 @@ struct DevFilamentDryingPreset
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
};
}// namespace Slic3r
}// namespace Slic3r
+17 -167
View File
@@ -9,7 +9,6 @@
#include <vector>
#include <string>
#include <utility>
#include <wx/app.h>
#include <wx/colour.h>
#include <set>
#include "slic3r/GUI/DeviceCore/DevDefs.h"
@@ -19,80 +18,14 @@
// TODO: remove this include
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GuiColor.hpp"
namespace Slic3r
{
// Bambu keeps its fixed external pair (255 main/right, 254 deputy/left) and its two
// left/right flags. Every other printer addresses one external virtual tray per live
// extruder, so ids 255, 254, 253, ... are extruders 0, 1, 2, ... .
static bool _is_bbl_vendor_printer()
{
return wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
}
// Extruder a virtual (external spool) tray id feeds: VIRTUAL_TRAY_MAIN_ID - tray_id.
// Returns -1 for an empty, non-numeric or out-of-range id.
static int _virtual_tray_extruder_id(const std::string& tray_id)
{
if (tray_id.empty())
return -1;
for (char c : tray_id)
{
if (c < '0' || c > '9')
return -1;
}
try
{
const int vt_id = std::stoi(tray_id);
if (vt_id < 0 || vt_id > VIRTUAL_TRAY_MAIN_ID)
return -1;
return VIRTUAL_TRAY_MAIN_ID - vt_id;
}
catch (...)
{
return -1;
}
}
// True when obj->vt_slot actually carries this virtual tray id as a reported spool. A synthetic
// topology filler (is_slot_placeholder) only makes the slot addressable and does not count.
static bool _has_virtual_tray_slot(const MachineObject* obj, const std::string& tray_id)
{
if (obj == nullptr || tray_id.empty())
return false;
for (const auto& tray : obj->vt_slot)
{
if (!tray.is_slot_placeholder && tray.id == tray_id)
return true;
}
return false;
}
// A generic printer may expose one AMS unit / external slot per extruder. When both legacy
// flags of a category are enabled (the sidebar sync call uses both AMS and both external flags)
// every extruder participates; when only one side is enabled (SelectMachine's left/right split)
// keep to that side's legacy extruder (right = main = 0, left = deputy = 1), so the N slots do
// not leak across sides.
static bool _generic_extruder_in_selected_side(int extruder_id, bool use_right, bool use_left)
{
if (use_right && use_left)
return true;
if (use_right)
return extruder_id == MAIN_EXTRUDER_ID;
if (use_left)
return extruder_id == DEPUTY_EXTRUDER_ID;
return false;
}
bool DevMappingUtil::is_valid_mapping_result(const MachineObject* obj, std::vector<FilamentInfo>& result, bool check_empty_slot)
{
if (result.empty()) return false;
const bool is_bbl_vendor = _is_bbl_vendor_printer();
for (int i = 0; i < result.size(); i++)
{
// invalid mapping result
@@ -109,13 +42,8 @@ namespace Slic3r
auto ams_item = obj->GetFilaSystem()->GetAmsById(result[i].ams_id);
if (ams_item == nullptr)
{
// A virtual (external spool) tray is not an AMS unit. Bambu keeps its fixed pair;
// a generic printer accepts whichever per-extruder slots it actually reported.
const bool is_virtual_tray = is_bbl_vendor
? (result[i].ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) ||
result[i].ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
: _has_virtual_tray_slot(obj, result[i].ams_id);
if (!is_virtual_tray)
if ((result[i].ams_id != std::to_string(VIRTUAL_TRAY_MAIN_ID)) &&
(result[i].ams_id != std::to_string(VIRTUAL_TRAY_DEPUTY_ID)))
{
result[i].tray_id = -1;
return false;
@@ -166,11 +94,8 @@ namespace Slic3r
result.ams_id = std::to_string(ams_id);
result.slot_id = std::to_string(slot_id);
if (type == DevAms::EXT_SPOOL || ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID)
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID)
{
// An external spool is addressed by its virtual tray id (255 -> extruder 0, 254 -> 1,
// ...); the id is the tray's identity, so it must not fall through to the AMS
// ams_id * 4 + slot_id indexing.
result.tray_id = atoi(tray.id.c_str());
result.id = atoi(tray.id.c_str());
}
@@ -201,25 +126,11 @@ namespace Slic3r
std::map<int, FilamentInfo> tray_filaments; // tray_index : tray_color
bool left_nozzle_has_ams = false, right_nozzle_has_ams = false;
const bool is_bbl_vendor = _is_bbl_vendor_printer();
// Extruders whose AMS filament participates in this mapping. On a generic printer this
// carries the per-extruder "nozzle already has AMS" state that the legacy right/left flags
// express for Bambu, so the matching external spool can be suppressed.
std::set<int> generic_extruders_with_ams;
const auto& ams_list = obj->GetFilaSystem()->GetAmsList();
for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++)
{
std::string ams_id = ams->second->GetAmsId();
auto ams_type = ams->second->GetAmsType();
const int ams_id_int = atoi(ams->first.c_str());
int generic_tray_base = 0;
if (!is_bbl_vendor) {
for (const auto& other : ams_list) {
if (atoi(other.first.c_str()) < ams_id_int)
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
}
}
// GetAmsType() maps mixed -> AMS_LITE; recover the mixed type so N9 trays index at 24+slot.
if (ams_type == DevAms::AMS_LITE && ams->second->IsAmsLiteMixed())
{
@@ -227,16 +138,10 @@ namespace Slic3r
}
for (auto tray = ams->second->GetTrays().begin(); tray != ams->second->GetTrays().end(); tray++)
{
int ams_id = ams_id_int;
int ams_id = atoi(ams->first.c_str());
int tray_id = atoi(tray->first.c_str());
int tray_index = 0;
if (!is_bbl_vendor)
{
// Generic units can have different lane counts; index all lanes cumulatively,
// matching the shared tray-index map and sidebar list builder.
tray_index = generic_tray_base + tray_id;
}
else if (ams_type == DevAms::AMS || ams_type == DevAms::AMS_LITE || ams_type == DevAms::N3F)
if (ams_type == DevAms::AMS || ams_type == DevAms::AMS_LITE || ams_type == DevAms::N3F)
{
tray_index = ams_id * 4 + tray_id;
}
@@ -264,18 +169,14 @@ namespace Slic3r
if (tray->second->is_tray_info_ready())
{
_parse_tray_info(ams_id, tray_id, ams_type, *(tray->second), info);
// Generic unit lanes use one cumulative index across variable-width units;
// ams_id * 4 + slot_id is only valid for Bambu's fixed four-slot units.
if (!is_bbl_vendor)
info.id = tray_index;
}
bool ams_valid = false;
if (is_bbl_vendor)
//first: left,nozzle=1,map=1 second: right,nozzle=0,map=2
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
bool left_ams_valid = (ams->second->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_LEFT_AMS];
if (right_ams_valid || left_ams_valid)
{
//first: left,nozzle=1,map=1 second: right,nozzle=0,map=2
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
bool left_ams_valid = (ams->second->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_LEFT_AMS];
tray_filaments.emplace(std::make_pair(tray_index, info));
if (right_ams_valid)
{
right_nozzle_has_ams = true;
@@ -284,25 +185,6 @@ namespace Slic3r
{
left_nozzle_has_ams = true;
}
ams_valid = right_ams_valid || left_ams_valid;
}
else
{
// A generic unit is bound to its own extruder, so it takes part whenever the
// request covers that extruder.
for (int extruder_id : ams->second->GetBindedExtruderSet())
{
if (_generic_extruder_in_selected_side(extruder_id, map_opt[MappingOption::USE_RIGHT_AMS], map_opt[MappingOption::USE_LEFT_AMS]))
{
ams_valid = true;
generic_extruders_with_ams.insert(extruder_id);
}
}
}
if (ams_valid)
{
tray_filaments.emplace(std::make_pair(tray_index, info));
}
}
}
@@ -311,21 +193,10 @@ namespace Slic3r
{
for (auto tray : obj->vt_slot)
{
// A synthetic topology filler is not a reported external spool, so it is never a
// mapping candidate. Bambu keeps its fixed reported pair.
if (!is_bbl_vendor && tray.is_slot_placeholder)
bool right_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID) && map_opt[MappingOption::USE_RIGHT_EXT]);
bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
if (right_ext_valid || left_ext_valid)
{
continue;
}
if (is_bbl_vendor)
{
// Bambu keeps its fixed pair and the two left/right external flags.
const bool right_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID) && map_opt[MappingOption::USE_RIGHT_EXT]);
const bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
if (!right_ext_valid && !left_ext_valid)
{
continue;
}
if (nozzle_has_ams_then_ignore_ext)
{
if (right_ext_valid && right_nozzle_has_ams)
@@ -337,31 +208,10 @@ namespace Slic3r
continue;
}
}
FilamentInfo info;
_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
tray_filaments.emplace(std::make_pair(info.tray_id, info));
}
else
{
// Generic: every present virtual tray is the external spool of the extruder it
// addresses (VIRTUAL_TRAY_MAIN_ID - tray.id): 255 -> 0, 254 -> 1, ... . Keep to
// the selected side when only one legacy flag is enabled, and drop the spool when
// its extruder already has AMS filament in this mapping and suppression is on.
const int extruder_id = _virtual_tray_extruder_id(tray.id);
if (extruder_id < 0)
{
continue;
}
if (!_generic_extruder_in_selected_side(extruder_id, map_opt[MappingOption::USE_RIGHT_EXT], map_opt[MappingOption::USE_LEFT_EXT]))
{
continue;
}
if (nozzle_has_ams_then_ignore_ext && generic_extruders_with_ams.count(extruder_id) != 0)
{
continue;
}
}
FilamentInfo info;
_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
tray_filaments.emplace(std::make_pair(info.tray_id, info));
}
}
@@ -562,4 +412,4 @@ namespace Slic3r
return 0;
}
}
}
+18 -38
View File
@@ -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
+47 -99
View File
@@ -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,25 +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);
grow_virtual_tray_slots(vt_slot, static_cast<std::size_t>(tray_id));
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
vt_slot[index] = vslot;
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);
}
}
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);
}
}
}
@@ -4170,13 +4163,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
try {
parse_new_info(jj);
} catch (...) {}
// Give every live device extruder an external virtual tray even when the payload
// carried no vir_slot/vt_tray data (non-Bambu), so the UI always has a slot per
// extruder. Runs after parse_new_info(), which refreshes the extruder count, and
// after the vir_slot/vt_tray handling above, which may have cleared the support
// flag and would otherwise hide these synthetic slots.
ensure_external_virtual_tray_slots();
#pragma endregion
} else if (jj["command"].get<std::string>() == "gcode_line") {
//ack of gcode_line
@@ -5282,50 +5268,18 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
return vt_tray;
}
void MachineObject::ensure_external_virtual_tray_slots()
{
// Without a preset bundle the vendor layout is unknown, and Bambu owns its own fixed pair
// (255, 254) reported through vir_slot/vt_tray; in both cases leave vt_slot untouched.
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr ||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
return;
}
const int extruder_count = m_extder_system != nullptr ? m_extder_system->GetTotalExtderCount() : 0;
if (extruder_count <= 0) {
return;
}
// One slot per live extruder, addressed by VIRTUAL_TRAY_MAIN_ID - extruder_id. Grow with
// placeholders and only fill slots that do not exist yet, so a slot already populated from a
// reported tray keeps its metadata and the slot -> extruder mapping never shifts.
for (int extruder_id = 0; extruder_id < extruder_count; ++extruder_id) {
const int tray_id = VIRTUAL_TRAY_MAIN_ID - extruder_id;
grow_virtual_tray_slots(vt_slot, static_cast<std::vector<DevAmsTray>::size_type>(tray_id));
auto& tray = vt_slot[extruder_id];
// The constructor seeds id 255 without a placeholder flag. Mark any empty generic slot as
// synthetic, but leave a slot carrying reported filament data eligible for sync/mapping.
tray.is_slot_placeholder = !tray.is_exists && tray.setting_id.empty() && tray.m_fila_type.empty();
}
// A printer with an external-slot layout supports virtual trays even when the payload carried
// no vir_slot/vt_tray data, which would otherwise clear this flag and hide the synthetic slots.
ams_support_virtual_tray = true;
}
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
@@ -5335,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; }
}
}
-10
View File
@@ -950,16 +950,6 @@ public:
std::vector<DevAmsTray> vt_slot;
DevAmsTray parse_vt_tray(nlohmann::json vtray);
/**
* Give every live device extruder an external virtual tray slot (non-Bambu printers only).
*
* Slots are addressed by VIRTUAL_TRAY_MAIN_ID - extruder_id, so a slot already reported for an
* extruder keeps its tray metadata and sparse/out-of-order reports never shift the slot ->
* extruder association. Only missing topology slots are added, so the call is idempotent across
* refreshes. Bambu keeps its existing fixed pair reported through vir_slot/vt_tray.
*/
void ensure_external_virtual_tray_slots();
/*get ams slot info*/
bool contains_tray(const std::string &ams_id, const std::string &tray_id) const;
DevAmsTray get_tray(const std::string &ams_id, const std::string &tray_id) const;/*use contains_tray() check first*/
+166 -75
View File
@@ -2,7 +2,6 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Field.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "wxExtensions.hpp"
#include "Plater.hpp"
@@ -46,11 +45,7 @@
#include <wx/arrstr.h>
#include <wx/anybutton.h>
#include <wx/colour.h>
#include <wx/button.h>
#include <wx/brush.h>
#include <wx/pen.h>
#include <optional>
#include <variant>
#include <wx/clrpicker.h>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/dc.h>
@@ -2623,41 +2618,92 @@ void PluginConfigField::msw_rescale()
void ColourPicker::BUILD()
{
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
// field the same height as other parameter buttons before drawing its swatch.
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
window = button;
button->SetBitmapMargins(0, 0);
// Validate the color
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk()) {
clr = wxTransparentColour;
}
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
if (parent_is_custom_ctrl && m_opt.height < 0)
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
button->SetFont(wxGetApp().normal_font());
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
set_value(boost::any(clr_str));
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
apply_user_color(wxTransparentColour);
event.Skip();
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
convert_to_picker_widget(temp);
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
// // recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
#ifdef __WXMSW__
temp,
#endif
this](wxCommandEvent e) {
#ifdef __WXMSW__
draw_bmp_btn(temp, temp->GetColour());
#endif
on_change_field();
}), temp->GetId());
// ORCA reset value to default on right click. previously no way to switch back on windows
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
#ifdef __WXMSW__
temp->SetColour(wxTransparentColour);
draw_bmp_btn(temp, wxTransparentColour);
#else
set_undef_value(temp);
#endif
on_change_field();
e.Skip();
});
// Native layouts may assign the final field size after construction.
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
draw_bmp_btn();
event.Skip();
});
button->SetToolTip(get_tooltip_text(clr_str));
temp->SetToolTip(get_tooltip_text(clr_str));
}
void ColourPicker::draw_bmp_btn()
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
{
if (!window) return;
auto* btn = static_cast<wxButton*>(window);
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
field->SetColour(wxTransparentColour);
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
wxImage image(btn->GetBitmap().GetSize());
image.InitAlpha();
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return;
#ifdef __WXMSW__
wxGCDC dc2(dc);
#else
wxDC &dc2(dc);
#endif
dc2.SetPen(wxPen("#F1754E", 1));
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
dc.SelectObject(wxNullBitmap);
btn->SetBitmapLabel(bmp);
}
// ORCA match style with button on windows
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
{
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
btn->SetBackgroundColour(*wxWHITE);
wxGetApp().UpdateDarkUI(btn);
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
wxSize btn_sz = btn->GetSize();
wxImage image(btn_sz);
@@ -2666,11 +2712,11 @@ void ColourPicker::draw_bmp_btn()
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return bmp;
wxGCDC dc2(dc);
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
int padding = btn->FromDIP(5);
dc2.SetPen(*wxTRANSPARENT_PEN);
@@ -2689,72 +2735,117 @@ void ColourPicker::draw_bmp_btn()
return bmp;
};
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
}
void ColourPicker::set_value(const std::string& value, bool change_event)
void ColourPicker::set_value(const boost::any& value, bool change_event)
{
set_value(boost::any(wxString::FromUTF8(value)), change_event);
}
m_disable_change_event = !change_event;
const wxString clr_str(boost::any_cast<wxString>(value));
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
{
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
const wxString clr_str = boost::any_cast<wxString>(value);
const wxColour parsed(clr_str);
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
draw_bmp_btn();
#ifdef __WXMSW__
const wxColour parsed_clr(clr_str);
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
field->SetColour(clr);
draw_bmp_btn(field, clr);
#else
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk())
set_undef_value(field);
else
field->SetColour(clr);
#endif
m_disable_change_event = false;
}
boost::any& ColourPicker::get_value()
{
m_value = m_colour == wxTransparentColour ? std::string() :
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
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
View File
@@ -22,10 +22,10 @@
#include <boost/any.hpp>
#include "I18N.hpp"
#include <wx/colour.h>
#include <wx/colourdata.h>
#include <wx/spinctrl.h>
#include <wx/bmpcbox.h>
#include <optional>
#include <wx/clrpicker.h>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp"
@@ -610,27 +610,41 @@ private:
};
class ColourPicker : public Field {
using Field::Field;
void set_undef_value(wxColourPickerCtrl* field);
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
public:
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~ColourPicker() {}
wxWindow* window{ nullptr };
void BUILD() override;
void set_value(const std::string& value, bool change_event = false);
void set_value(const std::string& value, bool change_event = false) {
m_disable_change_event = !change_event;
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
m_disable_change_event = false;
}
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void msw_rescale() override;
void sys_color_changed() override;
void enable() override { if (window) window->Enable(); }
void disable() override{ if (window) window->Disable(); }
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
wxWindow* getWindow() override { return window; }
private:
// An absent result is cancellation; transparent color is the undefined field.
void apply_user_color(const std::optional<wxColour>& color);
void draw_bmp_btn();
void on_button_click(wxCommandEvent& event);
wxColour m_colour = wxTransparentColour;
void convert_to_picker_widget(wxColourPickerCtrl *widget);
void on_button_click(wxCommandEvent &WXUNUSED(ev));
void save_colors_to_config();
private:
#if !defined(__linux__) && !defined(__LINUX__)
wxColourData* m_clrData{nullptr};
wxColourPickerWidget* m_picker_widget{nullptr};
#endif
};
class PointCtrl : public Field {
+30 -8
View File
@@ -23,6 +23,7 @@
#include <string>
#include <tuple>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
@@ -277,6 +278,19 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
return bdc.bitmap;
}
// Sort colors by HSV values (primarily by hue, then saturation, then value)
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
if (colors.size() < 2) return;
std::sort(colors.begin(), colors.end(),
[](const wxColour& a, const wxColour& b) {
ColourHSV ha = wxColourToHSV(a);
ColourHSV hb = wxColourToHSV(b);
if (ha.h != hb.h) return ha.h < hb.h;
if (ha.s != hb.s) return ha.s < hb.s;
return ha.v < hb.v;
});
}
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
{
// Check if color is transparent
@@ -416,16 +430,24 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
{
if (colors.empty()) return wxNullBitmap;
if (force_gradient && colors.size() >= 2) {
return create_gradient_filament_bitmap(colors, size);
// Make a copy to sort without modifying original
std::vector<wxColour> sorted_colors = colors;
// Sort colors by HSV when there are 2 or more colors
if (sorted_colors.size() >= 2) {
sort_colors_by_hsv(sorted_colors);
}
switch (colors.size()) {
case 1: return create_single_filament_bitmap(colors[0], size);
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
case 3: return create_triple_filament_bitmap(colors, size);
case 4: return create_quadruple_filament_bitmap(colors, size);
default: return create_gradient_filament_bitmap(colors, size);
if (force_gradient && sorted_colors.size() >= 2) {
return create_gradient_filament_bitmap(sorted_colors, size);
}
switch (sorted_colors.size()) {
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
case 3: return create_triple_filament_bitmap(sorted_colors, size);
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
default: return create_gradient_filament_bitmap(sorted_colors, size);
}
}
+36 -48
View File
@@ -1,8 +1,4 @@
#include "FilamentPickerDialog.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <utility>
#include "GUI.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -83,8 +79,7 @@ void FilamentPickerDialog::StartFlashing()
m_flash_timer->Start(50);
}
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
std::optional<FilamentColorPickerValue> ordered_color)
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
: DPIDialog(parent ? parent : wxGetApp().mainframe,
wxID_ANY,
_L("Select Filament"),
@@ -97,7 +92,6 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
m_color_query = new FilamentColorCodeQuery();
m_is_data_loaded = LoadFilamentData(fila_id);
m_cur_filament_color = fila_color;
m_ordered_color = std::move(ordered_color);
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
m_cur_color_name = new wxString(color_name);
@@ -447,9 +441,6 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
// Bind click
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
m_selection_changed = true;
m_ordered_color.reset();
m_custom_selection.reset();
m_cur_filament_color = color_code->GetFilaColor();
UpdatePreview(*color_code);
UpdateButtonStates(btn);
@@ -520,27 +511,24 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
Layout();
}
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
{
std::vector<wxColour> wx_colors = {custom_color};
// Update preview bitmap
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
if (bmp.IsOk()) {
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
m_color_demo->SetBitmap(bmp);
m_color_demo->SetBackgroundColour(wx_colors.front());
m_color_demo->SetBackgroundColour(custom_color);
m_color_demo->Refresh();
} else {
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
}
// Update preview labels for custom color
wxString label;
for (const auto& color : wx_colors) {
if (!label.empty()) label += " / ";
label += color.GetAsString(wxC2S_HTML_SYNTAX);
}
m_label_preview_color->SetLabel(label);
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
m_label_preview_idx->SetLabel("");
Layout();
}
@@ -569,6 +557,16 @@ void FilamentPickerDialog::CreateMoreInfoButton()
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
}
wxColourData FilamentPickerDialog::GetSingleColorData()
{
wxColourData data;
data.SetChooseFull(true);
if (m_cur_filament_color.ColorCount() > 0) {
data.SetColour(*m_cur_filament_color.m_colors.begin());
}
return data;
}
void FilamentPickerDialog::BindEvents()
{
// Bind mouse events for window dragging
@@ -590,37 +588,27 @@ void FilamentPickerDialog::BindEvents()
// Pause click detection while color picker is open
StopClickDetection();
FilamentColorPickerValue current;
if (m_ordered_color) {
current = *m_ordered_color;
} else {
for (const auto& color : m_cur_filament_color.m_colors)
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
}
const auto initial = filament_color_picker_initial(current);
std::optional<ColorSelection> selected;
{
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
if (dialog.ShowModal() == wxID_OK)
selected = dialog.selection();
}
StartClickDetection();
if (!filament_color_picker_changed(initial, selected))
return;
wxColourData original_data = GetSingleColorData();
wxColourData result = show_sys_picker_dialog(this, original_data);
m_custom_selection = selected;
m_ordered_color = filament_color_picker_result(*selected);
m_selection_changed = true;
std::vector<wxColour> colors;
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);
});
}
+4 -10
View File
@@ -5,10 +5,9 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "FilamentBitmapUtils.hpp"
#include "ColorPickerDialog.hpp"
#include <optional>
#include "Widgets/Button.hpp"
#include "EncodedFilament.hpp"
#include <wx/colourdata.h>
#include <wx/dialog.h>
#include <wx/string.h>
#include <wx/event.h>
@@ -26,13 +25,10 @@ namespace Slic3r { namespace GUI {
class FilamentPickerDialog : public DPIDialog
{
public:
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
virtual ~FilamentPickerDialog();
// Public interface methods
bool HasSelectionChanged() const { return m_selection_changed; }
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
bool IsDataLoaded() const { return m_is_data_loaded; }
wxColour GetSelectedColour() const;
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
@@ -72,7 +68,7 @@ private:
// UI update methods
void UpdatePreview(const FilamentColorCode& filament);
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
void UpdateCustomColorPreview(const wxColour& custom_color);
void UpdateButtonStates(wxBitmapButton* selected_btn);
// Shaped window methods
@@ -81,6 +77,7 @@ private:
// Data loading
bool LoadFilamentData(const wxString& fila_id);
wxColourData GetSingleColorData();
// Flash effect
void StartFlashing();
@@ -101,9 +98,6 @@ private:
FilamentColorCodes* m_cur_color_codes{nullptr};
wxBitmapButton* m_cur_selected_btn{nullptr};
FilamentColor m_cur_filament_color;
std::optional<FilamentColorPickerValue> m_ordered_color;
std::optional<ColorSelection> m_custom_selection;
bool m_selection_changed = false;
// Shaped window members
wxBitmap m_shape_bmp;
+29 -195
View File
@@ -20,7 +20,6 @@
#include <clocale>
#include <cstddef>
#include <algorithm>
#include <cctype>
#include <ios>
#include <iomanip>
#include <cstring>
@@ -2362,18 +2361,13 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
return false;
}
const Preset &selected_preset = preset_bundle->printers.get_selected_preset();
const Preset *parent_preset = preset_bundle->printers.get_preset_parent(selected_preset);
const bool inherits_system_preset = selected_preset.is_user() && parent_preset && parent_preset->is_system;
if (!inherits_system_preset) {
if (!this->plater)
return false;
if (!this->plater)
return false;
this->plater->update_objects_position_when_select_preset([machine_preset]() {
Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER);
printer_tab->select_preset(machine_preset->name);
});
}
this->plater->update_objects_position_when_select_preset([machine_preset]() {
Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER);
printer_tab->select_preset(machine_preset->name);
});
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list";
const Preset &cur_preset = preset_bundle->printers.get_selected_preset();
@@ -2385,19 +2379,7 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
assert(physical_extruder_map->values.size() == extruder_nums);
extruder_map = physical_extruder_map->values;
}
const int device_extruder_count = obj->GetExtderSystem()->GetTotalExtderCount();
if (device_extruder_count != extruder_nums) {
MessageDialog dlg(
this->plater,
format_wxstr(
_L("The connected printer reports %1% extruder(s), but the selected printer preset has %2%. "
"Update the preset to match the printer before syncing."),
device_extruder_count, extruder_nums),
_L("Sync printer information"),
wxOK | wxICON_WARNING);
dlg.ShowModal();
return false;
}
assert(obj->GetExtderSystem()->GetTotalExtderCount() == extruder_nums);
// Multi-nozzle: resolve the nozzle option (pops MultiNozzleSyncDialog when is_manual). No-op for every
// existing printer — support_multi_nozzle is false unless some extruder_max_nozzle_count entry is a real
@@ -2476,60 +2458,26 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
int main_index = obj->is_main_extruder_on_left() ? 0 : 1;
int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0;
// Nozzle diameters are not changed by device sync. If the device reports a different or unavailable
// diameter, warn and leave the preset and controls alone while syncing AMS and other printer info.
const ConfigOptionFloats *preset_nozzle_diameter =
preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter");
bool nozzle_diameters_match = false;
if (preset_nozzle_diameter != nullptr && preset_nozzle_diameter->values.size() == nozzle_diameters.size()) {
nozzle_diameters_match = true;
for (size_t index = 0; index < nozzle_diameters.size(); ++index) {
// A diameter the device does not report (missing, zero, negative or not a finite number) never matches.
const double device_diameter = nozzle_diameters[index];
const double preset_diameter = preset_nozzle_diameter->values[index];
if (!std::isfinite(device_diameter) || device_diameter <= 0.0 ||
!std::isfinite(preset_diameter) || preset_diameter <= 0.0 ||
std::fabs(device_diameter - preset_diameter) > EPSILON) {
nozzle_diameters_match = false;
break;
}
}
}
if (!nozzle_diameters_match) {
if (is_manual) {
MessageDialog dlg(
this->plater,
_L("The nozzle diameter reported by the connected printer does not match the current printer preset. "
"The nozzle diameter and the printer preset will not be synchronized. "
"Select the printer preset that matches the nozzle installed on the printer, then sync again."),
_L("Sync printer information"), wxICON_WARNING | wxOK);
dlg.ShowModal();
} else {
// An automatic sync runs on every device update, so log the mismatch instead of popping a dialog.
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << __LINE__
<< " nozzle diameter mismatch between connected printer and printer preset,"
" skip syncing the nozzle diameter";
}
}
if (extruder_nums > 1) {
if (nozzle_diameters_match) {
int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1]));
assert(left_index != -1 && right_index != -1);
left_extruder->combo_diameter->SetSelection(left_index);
right_extruder->combo_diameter->SetSelection(right_index);
}
int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1]));
assert(left_index != -1 && right_index != -1);
left_extruder->combo_diameter->SetSelection(left_index);
right_extruder->combo_diameter->SetSelection(right_index);
is_switching_diameter = true;
switch_diameter(false);
is_switching_diameter = false;
AMSCountPopupWindow::SetAMSCount(deputy_index, deputy_4, deputy_1);
AMSCountPopupWindow::SetAMSCount(main_index, main_4, main_1);
AMSCountPopupWindow::UpdateAMSCount(0, left_extruder);
AMSCountPopupWindow::UpdateAMSCount(1, right_extruder);
} else {
if (nozzle_diameters_match) {
int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
assert(index != -1);
single_extruder->combo_diameter->SetSelection(index);
}
int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
assert(index != -1);
single_extruder->combo_diameter->SetSelection(index);
is_switching_diameter = true;
switch_diameter(true);
is_switching_diameter = false;
}
// set nozzle volume type after switching prset, so this value can override the old value stored in conf
@@ -6121,12 +6069,8 @@ void Sidebar::on_bed_type_change(BedType bed_type)
*
* Return Value:
* - Map key encoding:
* - Virtual trays: 0x10000 + vt_tray.id for the main extruder's slot, or just vt_tray.id for the
* others. Bambu's fixed pair is 255 (main) / 254 (secondary); a generic printer identifies each
* slot by its own id (extruder = VIRTUAL_TRAY_MAIN_ID - id: 255 -> 0, 254 -> 1, ...)
* - 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.)
*
@@ -6146,13 +6090,7 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
}
}
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
// Diagnostic: keys produced from virtual (external) trays, so the final summary can label each
// output entry's origin as virtual vs AMS.
std::set<int> virtual_slot_keys;
auto build_tray_config = [is_bbl_vendor](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id, int extruder_id) {
auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) {
BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%")
% name % tray.setting_id % tray.m_fila_type % tray.color;
DynamicPrintConfig tray_config;
@@ -6167,12 +6105,6 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)});
tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists});
tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder});
// Transient (never persisted) per-extruder association of a generic slot, so the sync and
// the slicing per-extruder grouping keep the real extruder instead of the legacy left/right
// buckets. Bambu entries omit it and keep the legacy key-only convention.
if (!is_bbl_vendor) {
tray_config.set_key_value("device_tray_extruder_id", new ConfigOptionInt(extruder_id));
}
std::optional<FilamentBaseInfo> info;
if (wxGetApp().preset_bundle) {
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
@@ -6185,44 +6117,10 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
};
if (obj->ams_support_virtual_tray) {
for (size_t index = 0; index < obj->vt_slot.size(); ++index) {
auto & vt_tray = obj->vt_slot[index];
int vt_id = 0;
if (vt_tray.id.empty()) continue;
if (!std::all_of(vt_tray.id.begin(), vt_tray.id.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) continue;
try { vt_id = std::stoi(vt_tray.id); } catch (...) { continue; }
if (vt_id < 0 || vt_id > VIRTUAL_TRAY_MAIN_ID) continue;
// Enumerate the extruder ordinal (the vt_slot index) and validate the expected tray id
// instead of trusting the vector size: a stray filler or a stale id for a non-live
// extruder must never surface as a real tray. Bambu keeps its fixed pair untouched.
if (!is_bbl_vendor) {
const int expected_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(index);
if (vt_id != expected_id || !devPrinterUtil::IsVirtualSlot(vt_id)) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip vt_slot index %1% id %2% expected %3%") % index % vt_id % expected_id;
continue;
}
}
// Bambu keeps its fixed pair with the leading (main/right) slot marked 0x10000. On a
// generic printer every external slot is identified by its own virtual tray id
// (extruder = VIRTUAL_TRAY_MAIN_ID - id: 255 -> 0, 254 -> 1, ...), so mark the main
// extruder's slot by identity rather than by position in vt_slot. The map key then
// keeps each slot bound to its extruder across refreshes and sparse reports.
const bool is_main_slot = is_bbl_vendor ? (&vt_tray == &obj->vt_slot.front())
: (vt_id == VIRTUAL_TRAY_MAIN_ID);
const int key = (is_main_slot ? 0x10000 : 0) + vt_id;
const auto inserted = filament_ams_list.emplace(key,
build_tray_config(vt_tray, "Ext", vt_tray.id, "0", VIRTUAL_TRAY_MAIN_ID - vt_id));
virtual_slot_keys.insert(key);
if (!inserted.second) {
const auto &existing = inserted.first->second;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": virtual insert collision key %1% incoming_id %2% existing_origin %3% existing_ams %4% existing_slot %5% existing_type %6% existing_id %7%")
% key % vt_id
% (virtual_slot_keys.count(key) ? "virtual" : "ams")
% existing.opt_string("ams_id", 0u)
% existing.opt_string("slot_id", 0u)
% existing.opt_string("filament_type", 0u)
% existing.opt_string("filament_id", 0u);
}
int extruder = 0x10000; // Main (first) extruder at right
for (auto & vt_tray : obj->vt_slot) {
filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255
extruder = 0;
}
}
@@ -6242,61 +6140,11 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
for (auto ams : list) {
int ams_id = std::stoi(ams.first);
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS unit %1% extruder %2% trays %3%")
% ams_id % ams.second->GetExtruderId() % ams.second->GetTrays().size();
// 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());
}
}
size_t inserted_trays = 0;
size_t rejected_trays = 0;
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;
const int key = extruder + tray_index;
auto inserted = filament_ams_list.emplace(key,
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id),
ams.second->GetExtruderId()));
if (inserted.second) {
++inserted_trays;
} else {
++rejected_trays;
const auto &existing = inserted.first->second;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS insert collision key %1% incoming_ams %2% incoming_slot %3% incoming_extruder %4% existing_origin %5% existing_ams %6% existing_slot %7% existing_extruder %8% existing_type %9% existing_id %10%")
% key % ams_id % slot_id % ams.second->GetExtruderId()
% (virtual_slot_keys.count(key) ? "virtual" : "ams")
% existing.opt_string("ams_id", 0u)
% existing.opt_string("slot_id", 0u)
% (existing.has("device_tray_extruder_id") ? existing.opt_int("device_tray_extruder_id") : -1)
% existing.opt_string("filament_type", 0u)
% existing.opt_string("filament_id", 0u);
}
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)));
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS unit %1% tray inserts accepted %2% rejected %3%")
% ams_id % inserted_trays % rejected_trays;
}
// Diagnostic: dump the final output map (origin, key, identity/data flags) so parser output can
// be correlated with the entries logged at the sync boundary.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": output entries %1%") % filament_ams_list.size();
for (const auto &entry : filament_ams_list) {
const auto &cfg = entry.second;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": out %1% key %2% ams_id %3% slot_id %4% extruder %5% type %6% id %7% exist %8% placeholder %9%")
% (virtual_slot_keys.count(entry.first) ? "virtual" : "ams")
% entry.first
% cfg.opt_string("ams_id", 0u)
% cfg.opt_string("slot_id", 0u)
% (cfg.has("device_tray_extruder_id") ? cfg.opt_int("device_tray_extruder_id") : -1)
% cfg.opt_string("filament_type", 0u)
% cfg.opt_string("filament_id", 0u)
% cfg.opt_bool("filament_exist", 0u)
% cfg.opt_bool("filament_slot_placeholder", 0u);
}
return filament_ams_list;
}
@@ -6533,20 +6381,6 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
std::vector<std::pair<DynamicPrintConfig *,std::string>> unknowns;
auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams");
auto sync_color_only = wxGetApp().app_config->get("sync_ams_filament_mode") == "1";
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": sync_color_only %1% enable_append %2% direct_sync %3% effective_use_map %4% filament_ams_list %5%")
% sync_color_only % enable_append % sync_result.direct_sync % (!sync_result.direct_sync) % list.size();
for (const auto &entry : list) {
const auto &tray = entry.second;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": tray %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% filament_exist %7% placeholder %8%")
% entry.first
% tray.opt_string("ams_id", 0u)
% tray.opt_string("slot_id", 0u)
% tray.opt_string("filament_id", 0u)
% tray.opt_string("filament_type", 0u)
% tray.opt_string("filament_colour", 0u)
% (tray.has("filament_exist") && tray.opt_bool("filament_exist", 0u))
% (tray.has("filament_slot_placeholder") && tray.opt_bool("filament_slot_placeholder", 0u));
}
auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, sync_color_only);
wxString detail;
for (auto & uk : unknowns) {
+59 -55
View File
@@ -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);
}
}
+2
View File
@@ -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; }
+2 -8
View File
@@ -37,7 +37,6 @@
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
#include "DeviceCore/DevStorage.h"
#include <wx/app.h>
#include <wx/event.h>
#include <string>
#include "libslic3r/PrintConfig.hpp"
@@ -1156,7 +1155,7 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res
wxString ams_id;
wxColour ams_col;
if (devPrinterUtil::IsVirtualSlot(f->ams_id))
if (f->tray_id == VIRTUAL_TRAY_MAIN_ID || f->tray_id == VIRTUAL_TRAY_DEPUTY_ID)
{
ams_id = _L("Ext");
}else if (f->tray_id >= 0) {
@@ -1348,8 +1347,6 @@ bool SelectMachineDialog::get_ams_mapping_result(std::string &mapping_array_str,
if (m_ams_mapping_result.empty())
return false;
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
bool valid_mapping_result = true;
int invalid_count = 0;
for (int i = 0; i < m_ams_mapping_result.size(); i++) {
@@ -1405,10 +1402,7 @@ bool SelectMachineDialog::get_ams_mapping_result(std::string &mapping_array_str,
// convert #RRGGBB to RRGGBBAA
mapping_item["sourceColor"] = m_filaments[k].color;
mapping_item["targetColor"] = m_ams_mapping_result[k].color;
// The legacy v0 mapping array encodes an external spool as -1, a BBL-firmware
// contract. A generic printer keeps the virtual tray id (255 - extruder) so
// the payload still says which extruder's external slot was mapped.
if (is_bbl_vendor && (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID)) { tray_id = -1; }
if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) { tray_id = -1; }
/*new ams mapping data*/
try {
File diff suppressed because it is too large. Load diff
+4 -36
View File
@@ -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);
+2 -8
View File
@@ -37,7 +37,6 @@
#include <vector>
#include <wx/anybutton.h>
#include <wx/animate.h>
#include <wx/app.h>
#include <wx/busycursor.h>
#include <wx/chartype.h>
#include <wx/arrstr.h>
@@ -1246,7 +1245,7 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul
wxString ams_id;
wxColour ams_col;
if (devPrinterUtil::IsVirtualSlot(f->ams_id)) {
if (f->tray_id == VIRTUAL_TRAY_MAIN_ID || f->tray_id == VIRTUAL_TRAY_DEPUTY_ID) {
ams_id = _L("Ext");
}
@@ -1394,8 +1393,6 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
if (m_ams_mapping_result.empty())
return false;
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
bool valid_mapping_result = true;
int invalid_count = 0;
for (int i = 0; i < m_ams_mapping_result.size(); i++) {
@@ -1452,10 +1449,7 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
// convert #RRGGBB to RRGGBBAA
mapping_item["sourceColor"] = m_filaments[k].color;
mapping_item["targetColor"] = m_ams_mapping_result[k].color;
// The legacy v0 mapping array encodes an external spool as -1, a BBL-firmware
// contract. A generic printer keeps the virtual tray id (255 - extruder) so the
// payload still says which extruder's external slot was mapped.
if (is_bbl_vendor && (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID)) { tray_id = -1; }
if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) { tray_id = -1; }
/*new ams mapping data*/
try {
+42 -19
View File
@@ -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);
+21 -502
View File
@@ -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);
-27
View File
@@ -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;
};
+54 -317
View File
@@ -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()
-10
View File
@@ -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 };
+3 -18
View File
@@ -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);
}
}
-3
View File
@@ -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);
}
+38 -1
View File
@@ -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){
+3
View File
@@ -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 {
+92 -617
View File
@@ -10,7 +10,6 @@
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceCore/DevDefs.h"
#include "../GUI/DeviceCore/DevStorage.h"
#include "../GUI/DeviceCore/DevFirmware.h"
#include "nlohmann/json.hpp"
@@ -39,7 +38,6 @@
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cctype>
#include <ios>
@@ -63,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;
@@ -481,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
@@ -489,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);
@@ -507,147 +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;
}
size_t has_filament_count = 0;
for (const auto& tray : trays) {
if (tray.has_filament) ++has_filament_count;
}
std::ostringstream box_summary;
bool first_box = true;
for (const auto& [extruder_id, lanes] : boxes) {
if (!first_box) box_summary << ", ";
box_summary << "ext" << extruder_id << ":" << lanes.size();
first_box = false;
}
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: input trays " << trays.size()
<< " extruders " << boxes.size() << " has_filament " << has_filament_count
<< " boxes [" << box_summary.str() << "]";
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();
@@ -656,42 +528,9 @@ void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array,
nlohmann::json print_json = nlohmann::json::object();
print_json["ams"] = ams_json;
// Diagnostic: log the AMS-box boundary input (box count and per-box tray counts) so what the
// parser produces can be compared against what the agent actually submitted.
std::ostringstream box_trays;
for (size_t i = 0; i < ams_array.size(); ++i) {
if (i > 0) box_trays << ", ";
box_trays << "box" << ams_array[i].value("id", std::string()) << ":"
<< ((ams_array[i].contains("tray") && ams_array[i]["tray"].is_array()) ? ams_array[i]["tray"].size() : 0);
}
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::submit_ams_payload: boundary boxes " << ams_array.size()
<< " trays [" << box_trays.str() << "]";
// Call the parser to populate DevFilaSystem
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
// ParseV1_0 handles only the AMS boxes, so grow the external-spool slots directly here too:
// pull mode bypasses MachineObject::parse_json(), the only place the push-status vir_slot is
// applied. Each Moonraker extruder gets one empty native virtual tray (id = 255 - extruder_id),
// kept at its id-derived index so a non-contiguous extruder set does not shift slots. Slots that
// already carry reported spool metadata are left untouched (only missing slots are added).
const auto extruder_status = collect_extruder_status();
for (const auto& entry : extruder_status) {
const int extruder_id = entry.first;
if (extruder_id < 0 || extruder_id > VIRTUAL_TRAY_MAIN_ID) {
continue;
}
// Address the slot by its virtual tray id (VIRTUAL_TRAY_MAIN_ID - extruder_id), which places
// it at vector index extruder_id. Append-only, so a reported spool and the extruder
// association survive refreshes.
grow_virtual_tray_slots(obj->vt_slot, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - extruder_id));
auto& tray = obj->vt_slot[extruder_id];
tray.is_slot_placeholder = !tray.is_exists && tray.setting_id.empty() && tray.m_fila_type.empty();
}
if (!extruder_status.empty()) {
// A native virtual tray implies external-spool support, matching parse_vt_tray().
obj->ams_support_virtual_tray = true;
}
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.
@@ -731,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;
}
@@ -845,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())
@@ -971,60 +788,29 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
const auto& value = json["result"]["value"];
trays.clear();
max_lane_index = 0;
size_t skipped_records = 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);
// Happy Hare also writes lane_data, where the lane number is the gate index. Its mmu_machine
// status maps gates to units, and each unit's config maps to an extruder. Probe that mapping
// only when a lane has no AFC/extruder field, preserving the existing AFC/generic behavior.
std::map<int, int> hh_gate_extruder;
bool hh_probed = false;
auto hh_extruder_for_lane = [&](int lane) {
if (!hh_probed) {
hh_probed = true;
fetch_hh_gate_extruders(hh_gate_extruder);
}
const auto it = hh_gate_extruder.find(lane);
return it != hh_gate_extruder.end() ? it->second : 0;
};
for (const auto& [lane_key, lane_obj] : value.items()) {
if (!lane_obj.is_object()) {
++skipped_records;
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) {
++skipped_records;
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, then to the Happy
// Hare unit layout, and only then to the main extruder (best effort).
const int source_extruder = safe_json_int_flexible(lane_obj, "extruder_index",
safe_json_int_flexible(lane_obj, "extruder_id", -1));
const char* source_extruder_field = lane_obj.contains("extruder_index") ? "extruder_index"
: lane_obj.contains("extruder_id") ? "extruder_id" : "none";
tray.extruder_id = source_extruder;
if (tray.extruder_id < 0) {
const auto it = lane_extruder.find(lane_key);
tray.extruder_id = it != lane_extruder.end() ? it->second : hh_extruder_for_lane(lane_index);
}
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");
@@ -1037,18 +823,8 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
max_lane_index = std::max(max_lane_index, lane_index);
trays.push_back(tray);
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane " << lane_index
<< " src_extruder " << source_extruder_field << "=" << source_extruder
<< " mapped_extruder " << tray.extruder_id
<< " material " << tray.tray_type
<< " color " << tray.tray_color
<< " preset_resolved " << (!tray.tray_info_idx.empty());
}
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane_data records " << value.size()
<< " parsed " << trays.size() << " skipped " << skipped_records
<< " max_lane_index " << max_lane_index;
if (trays.empty()) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found";
return false;
@@ -1139,234 +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. Each gate is grouped by the toolhead its
// mmu_machine unit feeds; when the unit layout is unavailable every gate keeps the previous
// fallback of feed group 0.
std::map<int, int> gate_extruder;
fetch_hh_gate_extruders(gate_extruder);
// 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;
const auto extruder_it = gate_extruder.find(gate_idx);
tray.extruder_id = extruder_it != gate_extruder.end() ? extruder_it->second : 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;
}
// GET a Moonraker path (relative to the device base URL) with the device's API key and parse the
// response body. Returns false on a non-200 status or a body that is not valid JSON.
bool MoonrakerPrinterAgent::http_get_json(const std::string& path, nlohmann::json& out) const
{
auto http = Http::get(join_url(device_info.base_url, path));
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();
}
// 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)
{
nlohmann::json afc_json;
if (!http_get_json("/printer/objects/query?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 (!http_get_json("/printer/objects/query?" + 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();
}
// Happy Hare maps its gates to toolheads through mmu_machine units: each unit entry carries
// {name, first_gate, num_gates}, and the matching [mmu_unit <name>] config section names the
// extruder the unit is attached to. Only Happy Hare publishes mmu_machine, so a generic
// lane_data / AFC printer is never reassigned from here. Best effort: returns false and leaves the
// map empty when Happy Hare or its unit data is not available.
bool MoonrakerPrinterAgent::fetch_hh_gate_extruders(std::map<int, int>& gate_extruder)
{
gate_extruder.clear();
nlohmann::json machine_json;
if (!http_get_json("/printer/objects/query?mmu_machine", machine_json) ||
!machine_json.contains("result") || !machine_json["result"].contains("status") ||
!machine_json["result"]["status"].contains("mmu_machine") ||
!machine_json["result"]["status"]["mmu_machine"].is_object()) {
return false;
}
const auto& machine = machine_json["result"]["status"]["mmu_machine"];
if (machine.empty()) {
return false;
}
// Happy Hare publishes its config through the configfile object; read every unit's section from
// one fetch. An absent or unreadable configfile leaves every unit at the main extruder.
nlohmann::json sections;
nlohmann::json config_json;
if (http_get_json("/printer/objects/query?configfile", config_json) &&
config_json.contains("result") && config_json["result"].contains("status") &&
config_json["result"]["status"].contains("configfile") &&
config_json["result"]["status"]["configfile"].is_object()) {
const auto& configfile = config_json["result"]["status"]["configfile"];
if (configfile.contains("config") && configfile["config"].is_object()) {
sections = configfile["config"];
}
}
// One gate range -> extruder resolution per unit, reused for every gate the unit carries.
std::map<std::string, int> unit_extruder;
for (const auto& [unit_key, unit_obj] : machine.items()) {
// A unit entry is the one carrying a gate range; skip scalars such as num_units.
if (!unit_obj.is_object() || !unit_obj.contains("first_gate") || !unit_obj.contains("num_gates")) {
continue;
}
const int first_gate = safe_json_int_flexible(unit_obj, "first_gate", -1);
const int num_gates = safe_json_int_flexible(unit_obj, "num_gates", -1);
if (first_gate < 0 || num_gates <= 0) {
continue;
}
// If status omits the unit name, use the unit key as a best-effort config section suffix.
std::string unit_name = safe_json_string(unit_obj, "name");
if (unit_name.empty() && unit_key.rfind("unit_", 0) == 0) {
unit_name = unit_key;
}
if (unit_name.empty()) {
continue;
}
auto it = unit_extruder.find(unit_name);
if (it == unit_extruder.end()) {
it = unit_extruder.emplace(unit_name, hh_unit_extruder_from_config(sections, unit_name)).first;
}
for (int gate = first_gate; gate < first_gate + num_gates; ++gate) {
gate_extruder[gate] = it->second;
}
}
if (gate_extruder.empty()) {
return false;
}
BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_gate_extruders: Mapped "
<< gate_extruder.size() << " Happy Hare gate(s) across "
<< unit_extruder.size() << " unit(s)";
return true;
}
// Read the [mmu_unit <name>] section of a configfile.config map and map its extruder_name to a
// toolhead index ("extruder" -> 0, "extruderN" -> N). Happy Hare v4 defaults a missing
// extruder_name to the main extruder, so an absent setting and an absent section both fall back to
// 0 rather than dropping the unit.
int MoonrakerPrinterAgent::hh_unit_extruder_from_config(const nlohmann::json& sections, const std::string& unit_name)
{
constexpr int default_extruder = 0;
if (!sections.is_object()) {
return default_extruder;
}
// Happy Hare names its unit sections "mmu_unit <name>" (e.g. "mmu_unit unit0").
const auto it = sections.find("mmu_unit " + unit_name);
if (it == sections.end() || !it->is_object()) {
return default_extruder;
}
const std::string extruder_name = safe_json_string(*it, "extruder_name");
if (extruder_name.empty()) {
return default_extruder;
}
const int extruder_index = extruder_index_from_name(extruder_name);
return extruder_index >= 0 ? extruder_index : default_extruder;
}
int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str)
{
auto json = nlohmann::json::parse(json_str, nullptr, false);
@@ -1465,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;
@@ -1580,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;
@@ -1988,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 {
@@ -2207,31 +1781,6 @@ void MoonrakerPrinterAgent::update_status_cache(const nlohmann::json& updates)
}
}
// Snapshot Moonraker toolhead status objects ("extruder", "extruder1", ...) keyed by extruder id.
// An id is the suffix of the object name, so a non-contiguous set keeps its real ids.
std::map<int, nlohmann::json> MoonrakerPrinterAgent::collect_extruder_status() const
{
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
std::map<int, 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());
}
}
return extruder_status;
}
nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
{
nlohmann::json payload;
@@ -2314,80 +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.
const auto extruder_status = collect_extruder_status();
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"] = "";
// External-spool placeholders. Moonraker reports the extruders but no external-spool
// filament, so emit one empty native virtual tray per extruder (id = 255 - extruder_id).
// Emitting vir_slot makes the parser grow vt_slot and set ams_support_virtual_tray, giving
// every extruder a load slot instead of only the main one. Ids come from the extruder_status
// map, so a non-contiguous extruder set keeps its id mapping (the parser fills the holes
// with placeholders) rather than shifting every slot down.
auto& vir_slot = payload["print"]["vir_slot"];
vir_slot = nlohmann::json::array();
for (const auto& entry : extruder_status) {
if (entry.first < 0 || entry.first > VIRTUAL_TRAY_MAIN_ID) {
continue;
}
vir_slot.push_back({{"id", std::to_string(VIRTUAL_TRAY_MAIN_ID - entry.first)}});
}
}
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;
@@ -145,8 +139,6 @@ private:
void handle_ws_message(const std::string& dev_id, const std::string& payload);
void update_status_cache(const nlohmann::json& updates);
nlohmann::json build_print_payload_locked() const;
// Snapshot Moonraker "extruder"/"extruderN" status objects from the cache, keyed by extruder id.
std::map<int, nlohmann::json> collect_extruder_status() const;
// Print control helpers
int pause_print(const std::string& dev_id);
@@ -171,31 +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);
// Gate -> toolhead index, read from the Happy Hare mmu_machine units (gate ranges) and each
// unit's configured extruder_name. Returns false when Happy Hare is not present.
bool fetch_hh_gate_extruders(std::map<int, int>& gate_extruder);
// Read the extruder_name of the [mmu_unit <name>] section from a configfile.config map and map
// it to a toolhead index. Missing section/setting falls back to the main extruder (Happy Hare's
// own default).
static int hh_unit_extruder_from_config(const nlohmann::json& sections, const std::string& unit_name);
// GET a Moonraker path with the device's API key and parse the JSON body.
bool http_get_json(const std::string& path, nlohmann::json& out) const;
// 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);
@@ -2304,105 +2304,6 @@ TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament
}
}
namespace {
// One system "Generic PLA" the sync can resolve a reported "PLA" tray to, with a real
// filament_id so a tray that does report one still maps through the known-id path.
void add_generic_pla(PresetBundle &bundle)
{
Preset &pla = add_inmemory_preset(bundle.filaments, "Generic PLA");
pla.is_system = true;
pla.is_compatible = true;
pla.filament_id = "GFL99";
}
// A device tray as build_filament_ams_list builds it: a reported type, color and loaded flag,
// an optional preset id, and the placeholder flag pull-mode agents set on empty slots.
DynamicPrintConfig ams_tray(const std::string &filament_id, const std::string &filament_type, bool exist, bool placeholder)
{
DynamicPrintConfig tray;
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
tray.set_key_value("filament_colour", new ConfigOptionStrings{"#FF0000"});
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
tray.set_key_value("filament_exist", new ConfigOptionBools{exist});
tray.set_key_value("filament_slot_placeholder", new ConfigOptionBools{placeholder});
tray.set_key_value("ams_id", new ConfigOptionStrings{"0"});
tray.set_key_value("slot_id", new ConfigOptionStrings{"0"});
return tray;
}
// Run the mapped sync over the bundle's single tray and return the filament count it reports,
// which is what the sidebar uses to rebuild the Prepare filament list.
unsigned int sync_one_tray(PresetBundle &bundle, bool enable_append)
{
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
std::map<int, AMSMapInfo> maps;
MergeFilamentInfo merge;
return bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, enable_append, merge, /*color_only=*/false);
}
} // namespace
// A generic device reports type/color/loaded for a spool but no preset id. The Device tab shows
// that spool, so the Prepare sidebar has to count it: the sync appends the generic preset its
// reported type resolves to. A tray that only carried topology (no type, not loaded) must not.
TEST_CASE("A data-bearing AMS tray without a preset id appends a generic filament on sync", "[Preset][Bundle]")
{
PresetBundle bundle;
add_generic_pla(bundle);
bundle.set_num_filaments(1, std::string("#00FF00"));
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, "PLA", /*exist=*/true, /*placeholder=*/false);
const size_t before = bundle.filament_presets.size();
const auto count = sync_one_tray(bundle, /*enable_append=*/true);
CHECK(count == before + 1);
CHECK(bundle.filament_presets.back() == "Generic PLA");
}
TEST_CASE("An empty synthetic AMS tray does not append a filament on sync", "[Preset][Bundle]")
{
PresetBundle bundle;
add_generic_pla(bundle);
bundle.set_num_filaments(1, std::string("#00FF00"));
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, /*filament_type=*/{}, /*exist=*/false, /*placeholder=*/false);
const size_t before = bundle.filament_presets.size();
const auto count = sync_one_tray(bundle, /*enable_append=*/true);
CHECK(count == before);
}
TEST_CASE("An empty AMS placeholder does not append a filament on sync", "[Preset][Bundle]")
{
PresetBundle bundle;
add_generic_pla(bundle);
bundle.set_num_filaments(1, std::string("#00FF00"));
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, /*filament_type=*/{}, /*exist=*/false, /*placeholder=*/true);
const size_t before = bundle.filament_presets.size();
const auto count = sync_one_tray(bundle, /*enable_append=*/true);
CHECK(count == before);
}
// The append preference still governs a data-bearing tray: with append off the sidebar count is
// unchanged, exactly as for a mapped-but-unlisted tray.
TEST_CASE("The append preference gates a data-bearing AMS tray without a preset id", "[Preset][Bundle]")
{
PresetBundle bundle;
add_generic_pla(bundle);
bundle.set_num_filaments(1, std::string("#00FF00"));
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, "PLA", /*exist=*/true, /*placeholder=*/false);
const size_t before = bundle.filament_presets.size();
const auto count = sync_one_tray(bundle, /*enable_append=*/false);
CHECK(count == before);
}
// A mix is described by 1-based indices into the project's filament list, which Orca rebuilds
// from the selected printer's snapshot (filament_%02u / filament_colors) at startup and on every
// printer selection. Held anywhere but that same per-printer snapshot, the mixed arrays end up
-3
View File
@@ -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
@@ -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);
}
}
+1 -380
View File
@@ -13,15 +13,10 @@
#include <Windows.h>
#endif
#include <cstddef>
#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>
@@ -70,209 +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("External virtual tray slots cover each extruder without shifting existing data", "[DevFilaSystem]")
{
std::vector<DevAmsTray> slots;
slots.emplace_back("255");
slots[0].setting_id = "main-spool";
// Ensure slots for three extruders, preserving the main spool's reported metadata.
for (int extruder_id = 0; extruder_id < 3; ++extruder_id) {
grow_virtual_tray_slots(slots, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - extruder_id));
}
REQUIRE(slots.size() == 3);
CHECK(slots[0].id == "255");
CHECK(slots[1].id == "254");
CHECK(slots[2].id == "253");
CHECK(slots[0].setting_id == "main-spool");
// Repeating the topology update is idempotent and leaves existing tray data intact.
slots[2].setting_id = "third-spool";
for (int extruder_id = 0; extruder_id < 3; ++extruder_id) {
grow_virtual_tray_slots(slots, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - extruder_id));
}
REQUIRE(slots.size() == 3);
CHECK(slots[2].setting_id == "third-spool");
}
TEST_CASE("Sparse external spool reports retain their extruder slot IDs", "[DevFilaSystem]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
const json filament = {
{"units", json::array()},
{"external", json::array({
{{"extruder", 2}, {"loaded", true}, {"material", "PETG"}, {"preset_id", "third-spool"}},
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"preset_id", "main-spool"}}
})}
};
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
REQUIRE(obj.vt_slot.size() == 3);
CHECK(obj.vt_slot[0].id == "255");
CHECK(obj.vt_slot[0].setting_id == "main-spool");
CHECK(obj.vt_slot[1].id == "254");
CHECK(obj.vt_slot[2].id == "253");
CHECK(obj.vt_slot[2].setting_id == "third-spool");
}
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");
@@ -440,174 +232,3 @@ TEST_CASE("Switch-bound AMS with an invalid track is excluded from mapping", "[D
DevFilaSystemParser::ParseV1_0(print_push, &obj_no_switch, obj_no_switch.GetFilaSystem().get(), false);
REQUIRE(obj_no_switch.GetFilaSystem()->GetAmsList().count("0") == 0);
}
TEST_CASE("Generic external spools map by their virtual tray id", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// One external spool per extruder: 255 -> extruder 0, 254 -> 1, 253 -> 2, 252 -> 3.
const json filament = {
{"units", json::array()},
{"external", json::array({
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"color", "FF5733FF"}, {"preset_id", "P0"}},
{{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"color", "33C1FFFF"}, {"preset_id", "P1"}},
{{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"color", "8D33FFFF"}, {"preset_id", "P2"}},
{{"extruder", 3}, {"loaded", true}, {"material", "PETG"}, {"color", "44AA22FF"}, {"preset_id", "P3"}}
})}
};
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
REQUIRE(obj.vt_slot.size() == 4);
REQUIRE(obj.vt_slot[2].id == "253");
// Matches only extruder 2's external spool (ABS / 8D33FFFF).
FilamentInfo fila;
fila.id = 0;
fila.type = "ABS";
fila.color = "8D33FFFF";
std::vector<FilamentInfo> result;
std::vector<bool> map_opt(4, false);
map_opt[MappingOption::USE_LEFT_EXT] = true;
map_opt[MappingOption::USE_RIGHT_EXT] = true;
DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false);
REQUIRE(result.size() == 1);
// The external spool keeps its virtual tray id (253), never the AMS ams_id * 4 + slot_id form.
CHECK(result[0].tray_id == 253);
CHECK(result[0].ams_id == "253");
}
TEST_CASE("Mapping validation accepts only reported generic virtual slots", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
const json filament = {
{"units", json::array()},
{"external", json::array({
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"preset_id", "P0"}},
{{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"preset_id", "P1"}},
{{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"preset_id", "P2"}}
})}
};
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
REQUIRE(obj.vt_slot.size() == 3);
FilamentInfo reported;
reported.id = 0;
reported.tray_id = 253;
reported.ams_id = "253"; // extruder 2's external slot, present in vt_slot
reported.slot_id = "0";
{
std::vector<FilamentInfo> result{reported};
CHECK(DevMappingUtil::is_valid_mapping_result(&obj, result, false));
}
// A virtual id the printer never reported is rejected and reset.
FilamentInfo missing;
missing.id = 1;
missing.tray_id = 252;
missing.ams_id = "252";
missing.slot_id = "0";
{
std::vector<FilamentInfo> result{missing};
CHECK_FALSE(DevMappingUtil::is_valid_mapping_result(&obj, result, false));
CHECK(result[0].tray_id == -1);
}
}
TEST_CASE("Generic AMS units on every extruder join the mapping", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
const std::vector<std::string> materials = {"PLA", "ASA", "ABS", "PETG", "TPU"};
const std::vector<std::string> colors = {"FF5733FF", "33C1FFFF", "8D33FFFF", "44AA22FF", "FFAA00FF"};
json filament = { {"units", json::array()}, {"external", json::array()} };
for (int extruder = 0; extruder < 5; ++extruder) {
json unit = { {"id", std::to_string(extruder)}, {"extruder", extruder}, {"slots", json::array()} };
unit["slots"].push_back({{"index", 0}, {"loaded", true}, {"material", materials[extruder]}, {"color", colors[extruder]}});
filament["units"].push_back(std::move(unit));
}
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 5);
// PETG / 44AA22FF is extruder 3's AMS slot, with cumulative generic tray index 3.
FilamentInfo fila;
fila.id = 0;
fila.type = "PETG";
fila.color = "44AA22FF";
// The sidebar sync enables both legacy AMS flags, so every extruder participates.
std::vector<FilamentInfo> result;
std::vector<bool> map_opt(4, false);
map_opt[MappingOption::USE_LEFT_AMS] = true;
map_opt[MappingOption::USE_RIGHT_AMS] = true;
DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false);
REQUIRE(result.size() == 1);
CHECK(result[0].ams_id == "3");
CHECK(result[0].tray_id == 3);
// SelectMachine's right-side split passes only the right flag: extruder 3's unit stays out and
// nothing in extruder 0's unit matches PETG, so the filament is left unmapped.
std::vector<FilamentInfo> split_result;
map_opt[MappingOption::USE_LEFT_AMS] = false;
DevMappingUtil::ams_filament_mapping(&obj, {fila}, split_result, map_opt, {}, false);
REQUIRE(split_result.size() == 1);
CHECK(split_result[0].tray_id == -1);
}
TEST_CASE("Generic external spool is suppressed when its extruder has AMS filament", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// One AMS unit on extruder 2 (red ABS) plus one external spool per extruder; extruder 2's
// external spool is green ABS, which is what the sliced filament matches exactly.
const json filament = {
{"units", json::array({
{{"id", "2"}, {"extruder", 2}, {"slots", json::array({
{{"index", 0}, {"loaded", true}, {"material", "ABS"}, {"color", "FF0000FF"}}
})}}
})},
{"external", json::array({
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"color", "FF5733FF"}},
{{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"color", "33C1FFFF"}},
{{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"color", "00FF00FF"}},
{{"extruder", 3}, {"loaded", true}, {"material", "PETG"}, {"color", "44AA22FF"}},
{{"extruder", 4}, {"loaded", true}, {"material", "TPU"}, {"color", "FFAA00FF"}}
})}
};
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
REQUIRE(obj.vt_slot.size() == 5);
REQUIRE(obj.vt_slot[2].id == "253");
FilamentInfo fila;
fila.id = 0;
fila.type = "ABS";
// FilamentInfo.color carries the "#RRGGBBAA" form the GUI builds for a sliced filament
// (SelectMachine/SyncAmsInfoDialog); tray colors are the bare RRGGBBAA DevAmsTray::decode_color
// consumes. A bare string here is not parsed by wxColour's hex branch and reads as black.
fila.color = "#00FF00FF";
std::vector<bool> map_opt(4, false);
map_opt[MappingOption::USE_LEFT_AMS] = true;
map_opt[MappingOption::USE_RIGHT_AMS] = true;
map_opt[MappingOption::USE_LEFT_EXT] = true;
map_opt[MappingOption::USE_RIGHT_EXT] = true;
// With suppression off the exact-match external spool (253) wins over the red AMS tray.
std::vector<FilamentInfo> result;
DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false);
REQUIRE(result.size() == 1);
CHECK(result[0].tray_id == 253);
CHECK(result[0].ams_id == "253");
// With suppression on, extruder 2 already has AMS filament in this mapping, so its external
// spool is dropped and the AMS tray is the only candidate left. This fixture has only that
// single generic unit, so its cumulative tray index is 0.
std::vector<FilamentInfo> suppressed;
DevMappingUtil::ams_filament_mapping(&obj, {fila}, suppressed, map_opt, {}, true);
REQUIRE(suppressed.size() == 1);
CHECK(suppressed[0].ams_id == "2");
CHECK(suppressed[0].tray_id == 0);
}
@@ -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);
}