Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cb4854e
docs(ace-mmu): design the U1 printer panel, and how to draw an ACE an…
physicsG Aug 21, 2026
3bbb943
docs(ace-mmu): keep third-party reference imagery out of the repo
physicsG Aug 21, 2026
2867bc9
docs(ace-mmu): correct the plates paired with the wrong bed type enum
physicsG Aug 21, 2026
9ba7eb5
feat(config): give the printer preset an ACE mode and per-head ACE to…
physicsG Aug 21, 2026
2a2f2df
feat(sidebar): rebuild the U1 printer panel as three cards and a tool…
physicsG Aug 21, 2026
5b99906
feat(sidebar): make Sync info read the machine, and mark what agrees …
physicsG Aug 22, 2026
ba703cc
fix(sidebar): make the sync mark hold its check, and give the icons t…
physicsG Aug 22, 2026
3e012b3
feat(sidebar): put the ACE inventory into the filament sync
physicsG Aug 22, 2026
79cc24f
feat(sidebar): sync the filament list to the machine when the plate i…
physicsG Aug 22, 2026
3795c1d
feat(account): keep the Snapmaker login across restarts
physicsG Aug 22, 2026
6aa2f77
fix(filament sync): stop the matcher auto-assigning rows the user can…
physicsG Aug 22, 2026
4986c69
feat(sidebar): mark the synced filaments, and drop the mapping dialog…
physicsG Aug 22, 2026
3bcff63
refactor(sidebar): give the filament rows the same corner mark as the…
physicsG Aug 22, 2026
5929206
fix(account): restore the login early, then say it again once the web…
physicsG Aug 23, 2026
40ce3f0
refactor(sidebar): one size for the sync mark, everywhere it appears
physicsG Aug 23, 2026
58df06d
docs(ace-mmu): take the competitor's name out of the design record
physicsG Aug 23, 2026
37c617e
fix(profiles): bump the Snapmaker vendor version so this branch's pro…
physicsG Aug 24, 2026
88d9daa
fix(profiles): correct a filename case that made the whole Snapmaker …
physicsG Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ resources/profiles/user/default
deps_src/build/
.claude/
.hermes/
CLAUDE.md
CLAUDE.md
# UI reference snapshots — third-party imagery, design reference only (see the folder's README)
ui-snapshots-inspiration/
128 changes: 128 additions & 0 deletions docs/ace-mmu/15-printer-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# The U1 printer panel (Prepare / Preview)

Interactive mockup: [printer-panel-mockup.html](printer-panel-mockup.html) ·
Visual standard: [16-ace-visuals.md](16-ace-visuals.md)

> **Status: design only. No code has landed.** This branch carries the docs and mockups;
> every implementation step below is still to do. The ACE machinery referenced here —
> `ace_head_capacity` / `ace_head_unit`, `AceMmuProvider`, `sync_ace_topology`,
> `AceMmuPlan` — lives on `feat/ace-mmu-slicing` and does not exist on
> `develop/add-multiace-support` yet. Start from [NEXT.md](NEXT.md).

**Prepare and Preview share one sidebar.** The Printer section at the top of it is a
single widget seen in two tabs, so this is one change in two places — and anything
added to it is also on screen while reading a sliced preview, where vertical space is
contested.

## What is wrong today

Measured by running the app, not by reading it (`.claude/tools/start.sh headless`).

| # | Defect | Where |
|---|--------|-------|
| 1 | **Four tabs hold one control each.** `Nozzle 1..4`, each with a `Diameter` combo; changing any one writes all four and switches the whole preset, because the U1 refuses mixed diameters. Three tabs exist to be clicked and show nothing new. | `Sidebar::update_nozzle_settings`, Plater.cpp:8682 |
| 2 | **Three names for one thing.** Sidebar says *Nozzle 1–4*, Printer Settings says *Toolhead 1–4*, the assignment dialog says *T1–T4*. | Plater.cpp:8814, Tab.cpp:4808, `resources/web/aceplan` |
| 3 | **No topology.** The preset knows head 4 is fed by ACE 1 with four slots (`ace_head_capacity`, `ace_head_unit`); the panel never says so. | — |
| 4 | **No contents, and no way to ask.** The spool colours at each head are known when the printer is on the LAN — they already drive the filament sync — but never reach this panel. | `append_ace_filament_list`, Plater.cpp:773 |
| 5 | **Two half-syncs.** The sidebar glyph syncs nozzle diameters only; multiACE topology sync is a separate button buried in Printer Settings › Multimaterial. | Plater.cpp:2214, Tab.cpp:4652 |

## The shape: a mature multi-head panel, for four heads

Taken piece by piece from the reference slicer this panel is modelled on - a two-nozzle
machine's printer section, which solves most of the same problems already.

- **Three cards across the top** — printer (thumbnail over preset combo), plate
(texture swatch + ⓘ, absorbing today's `Bed type` row), and **Sync info**.
- **A green corner tick** on any card that agrees with the connected machine. On a head
box it means the ACE wiring matches what the printer reports — a claim we can make,
because `sync_ace_topology` already computes exactly that diff.
- **A bordered box per head**, the reference's Left/Right Nozzle panes wrapped **2×2**: an
`ACE` row (badge when a unit feeds it, `Stock feeder` otherwise, adjust button always)
and a `Diameter` row. No `Flow` row — this fork deleted the control and there is no
setting behind it.
- **One `Nozzle` row**, not four tabs. Per-head diameters return only if the U1 ever
allows mixed sets; a machine reporting differing diameters still routes to the
existing `NozzleDiameterSelectDialog`.
- **Sync in two steps** — *Successfully synchronized nozzle, ACE mode and ACE unit
information* → **[Continue to sync filaments] [Cancel]**, anchored over the viewport.
Step two opens the existing `Sidebar::show_sync_filament_dialog`.

## ACE mode — the printer's own switch

Verified against firmware (`/printer/gcode/help` on 192.168.2.242):

```
SET_ACE_MODE MODE=normal|multi|head [HEAD=n]
ACE_SET_HEAD_ACE HEAD=0..3 ACE=0..3 "each ACE head is wired to exactly one ACE"
ACE_SET_HEAD_FEEDER HEAD=0..3 ENABLE=0|1 "(head mode only)"
```

The panel mirrors it as a labelled dropdown — **Normal** / **Per toolhead** /
**Combined** — with the raw `SET_ACE_MODE MODE=…` in the tooltip. Only in **head** mode
does per-toolhead wiring mean anything, so the head boxes grey their ACE rows in Normal.
Sync info reads the mode back along with the wiring.

## The assign popover

A **choice**, not a count: the firmware offers exactly two macros, so the list has
exactly two kinds of row and a tick rather than a spinner.

```
Which ACE feeds Toolhead 4?
⬡ Stock feeder One spool, loaded at the head ( )
▤ ACE 1 · ACE 2 Pro 4 slots · connected · 39% RH (✓)
```

Units are named as the printer names them — `protocol: "v2"` → **ACE 2 Pro**, `"v1"` →
**ACE Pro** — the same mapping `resources/web/multiace/index.html` already uses. Between
them the rows write exactly `ace_head_capacity` and `ace_head_unit`.

**One unit may feed several heads.** `ACE_SET_HEAD_ACE` binds a head to one ACE; it says
nothing about an ACE feeding one head, and `head_ace` is a map from head to unit. Ticking
the same unit on a second head is therefore legal, each row then reads *also feeds
Toolhead N*, and a warning states the real capacity.

## Two defects this uncovered

1. **A shared unit double-counts capacity.** `AceMmuPlan.hpp` sums capacity per head
(`total_cap += cap[h]`) and enforces it per head (`if (++load[h] > cap[h])`). Two
heads on one 4-slot unit read as **8 places** when there are 4, so the
infeasible-plate refusal — whose whole purpose is catching this — would pass a plate
that cannot be laid out. Needs a per-unit pool constraint beside the per-head one.
2. **Combined mode cannot be emitted.** `ace_head_capacity` already offers *6 slots* and
*8 slots*, but `GCode.cpp`'s `unit_of_head()` returns the single `ace_head_unit[h]`
and the plan's slot is an index *within the head*. Slot 5 of an 8-slot head emits
`ACE=<first unit> SLOT=5` where the machine needs `ACE=<second unit> SLOT=1` — wrong
unit, wrong slot, wrong colour. Those enum values are unsafe until the emitter maps
slot → (unit, slot).

## Touch points

| Piece | Where | State |
|-------|-------|-------|
| The panel | `Plater.cpp:2199–2565` | Title bar, preset card, Bed type row, nozzle notebook — built once in the Sidebar constructor |
| The head boxes | `Sidebar::update_nozzle_settings`, `Plater.cpp:8682` | Rebuilds one page per `nozzle_diameter` entry; becomes a 2×2 grid |
| Sync, nozzles | `Plater.cpp:2214–2350` | Queries the machine, `NozzleDiameterSelectDialog` on mixed diameters. Keep; becomes half the press |
| Sync, topology | `TabPrinter::sync_ace_topology`, `Tab.cpp:4652` | Reads `/multiace/api/state`, diffs per head, reports. Lift out of `TabPrinter` so the sidebar can call it |
| Sync, filaments | `Sidebar::show_sync_filament_dialog`, `Plater.cpp:8467` | Already lists U1 toolheads *and* ACE slots. What *Continue to sync filaments* opens |
| Topology | `ace_head_capacity`, `ace_head_unit` | Per-head `coInts` in the printer preset — the panel works with the printer off |
| Live contents | `AceMmuProvider`, `AceSnapshot` | Units, slots, colours, humidity, per-head bindings. One `fetch_once()` |

**Before building:** the U1 connects as a `PrintHost` through the webview, not as a
`MachineObject`, so this panel cannot lean on `MachineObject::poll_ace_ams()`. It
resolves a host with `AceMmuProvider::resolve_connected_host()` and reads on demand —
the other reason press-to-sync fits the U1 better than a background poll.

## Shipping order

Each is one branch off `develop/add-multiace-support`, one squashed PR.

1. **Panel structure** — three cards, 2×2 head boxes, one Nozzle row, `Toolhead N`
naming, plate card absorbing Bed type. No ACE; no new config. Self-contained.
2. **Topology in the preset** — `ace_head_capacity` / `ace_head_unit`, the Multimaterial
settings page, `sync_ace_topology`.
3. **The panel's ACE row** — badge, assign popover, ACE mode dropdown, on top of 1 + 2.
4. **Sync info, two-step** — nozzle + topology in one press, chaining to the filament sync.
5. **Per-unit capacity pool** — the shared-unit fix. Before any user can share a unit.
6. **Combined mode** — the emitter's slot → (unit, slot) map. Until then the mode is
listed and disabled, with the reason on it.
115 changes: 115 additions & 0 deletions docs/ace-mmu/16-ace-visuals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# The ACE visual standard

Interactive sheet: [ace-visual-standard.html](ace-visual-standard.html)

An ACE was drawn four different ways. The device page gave it 60 px circular spools; the
assignment dialog 96 px cards in a teal-banded box; the printer panel 7×15 px bars; and
the filament-mapping popup — which reads the ACE through `amsList` — **Orca's real AMS
widgets**. Four languages for one object.

That last one is the way out. The ACE is already projected onto `Ams`/`AmsTray`, so the
AMS widgets are the standard, and **the geometry below is lifted from
`src/slic3r/GUI/Widgets/AMSItem.hpp` rather than invented** — which is what stops it
drifting again.

## The three forms

One treatment — **outlined chassis, solid bays, one stroke weight** — in three
proportions. Four bays is not a variable: `/api/state` returns `"slots": [/* exactly 4 */]`
and `SLOT_COUNT = 4` is a constant. There is no single-bay form; an AMS Lite has one
spool, an ACE never does.

| Form | Size | Where | Function |
|------|------|-------|----------|
| **Badge** | 44×26 fill | a head box | `ace_badge()` |
| **Glyph** | 44×26 line, stroke 1.6 | a popover row, a label | `ace_glyph()` |
| **Glyph, square** | 24×24 line, stroke 1.6 | a tab, a menu, a `ScalableButton` | `ace_glyph_square()` |

**Badge** — hood, four bays, base drawn *over* them; the base is slightly wider than the
hood, which is what makes it read as a cabinet rather than a bar chart. Bays are 5×14
capsules at x 6/15/24/33 — **padding 4 = gap 4**, matching the proportions of the reference's
own icon. Colour and emptiness are all that survive at this size, so the badge carries
colour only; an empty bay is white against the grey hood, with no outline. Trust and
staleness live wherever the badge is a control.

**Glyph** — the badge's own silhouette in line: one stepped path, hood shoulders on top,
base stepping out at the bottom, bays filled.

**Glyph, square** — body and four bays, no hood or base step. It is deliberately *not*
the same silhouette: the family is carried by the bay treatment and the stroke, because
the square has a third of the width to say the same thing in. If the two ever sit side by
side and the mismatch shows, the fallback is the wide drawing letterboxed into the square.

## The box the spools sit in

Orca's AMS already owns the neutrals, and uses them for exactly these roles:

- `AMS_CONTROL_DEF_BLOCK_BK_COLOUR` **#EEEEEE** — the band, and an empty tube
- `AMS_CONTROL_DEF_LIB_BK_COLOUR` **#F8F8F8** — the box the tubes stand in
- `AMS_CONTROL_BRAND_COLOUR` **#009688** — hover, 2 px
- `AMS_CONTROL_DISABLE_COLOUR` **#CECECE** — a unit configured but not answering

All go through `StateColor::darkModeColorFor`, so dark mode is not a second palette.

**The spool object is `AMSLib`** — 58×80 (`AMS_CAN_LIB_SIZE`), a well inset by 4, the
filament colour drawn **from the bottom up to how much is left**. Not a swatch on a card:
a level in a tube, so a row reads as an inventory. Selection is 2 px in *the filament's
own colour* (`AMSLib`'s rule); hover is the brand teal. Label ink follows the fill's
luminance, the same `< 0.6` test `AMSLib` uses for its badge.

## Moisture and temperature

`AMSHumidity`, unchanged: a pill (radius = half the height) on `#EEEEEE`, the
`hum_level1..5` droplet at 16 px, a 1 px `#C2C2C2` divider, then the dryer glyph
(`ams_drying` / `ams_is_drying`). `AMS_HUMIDITY_SIZE` 93×26 with a percentage,
`AMS_HUMIDITY_NO_PERCENT_SIZE` 60×26 without.

`AMSinfo` already handles the ACE's exact case: `humidity_raw = -1` selects the numbered
droplet, anything else the plain droplet plus the number. Bucket the raw percentage
1 = ≤20, 2 = ≤35, 3 = ≤50, 4 = ≤65, 5 = >65 to pick the glyph.

**Temperature is the one addition.** The AMS carries `current_temperature` but never
draws it here; the ACE reports `temp` per unit and it matters while drying. It goes in
the same pill behind a second divider — one chip, not three. Absent when unreported,
never zeroed.

## Where a level comes from

An ACE slot has **no remain field**: `/api/state`'s `slots[]` carries material, brand,
colour and source, and nothing about quantity. But a Spoolman-backed printer binds them:

```jsonc
"spool_mode": "spoolman",
"spool_binding": { "0_0": "15", "0_1": "10", "0_3": "16" },
"spools": { "15": { "weight_g": 500.1, "used_mm": 0.0, "density": 1.27, ... } }
```

So bound slots can drive the column honestly, and an unbound one is drawn full but
hatched and labelled *amount unknown* rather than pretending to be full.

**What is not available is a percentage.** Spoolman knows the initial weight; this
payload does not, so a column scaled to 1 kg would call an 843 g spool 84% when it may be
a full 850 g one. Show the grams and treat the column as a gauge — or fetch
`remaining_weight` from Spoolman directly and scale it properly, which is its own piece
of work. `AceSlot` parses none of the binding today; wiring it through is the
prerequisite for the column meaning anything.

## Adoption

| Surface | Draws now | Becomes |
|---------|-----------|---------|
| Filament mapping popup (`AmsMappingPopup.cpp`) | Orca's AMS widgets, via the `amsList` projection | **Nothing** — it is already the standard, and the reference |
| Device / AMS tab (`AMSControl`, `AmsItem`) | Orca's AMS widgets, fed by the projection | **Nothing**, beyond naming the unit *ACE 2 Pro* rather than *AMS* |
| Printer panel (`Plater.cpp` sidebar) | 7×15 px bars in an ad-hoc strip | **Badge** in the head box |
| Assignment dialog (`resources/web/aceplan`) | 96 px `.pos` cards in a teal `.acebox` | **Spool box** + `AMSLib` columns; keep the drag targets |
| U1 + multiACE page (`resources/web/multiace`) | 60 px circular spools; 36 px circular swatches | **Spool box** + `AMSLib` columns. The circles are the biggest departure and the one worth losing — nothing else in Orca draws filament round |

**Note on `AMSPreview`.** The 82×27 strip of 14×14 cubes is real, shipping code, but it
has only two call sites — `AMSControl.cpp:1173` (the unit selector) and
`CalibrationWizardPresetPage.cpp:617`. `AMS_ITEM_CUBE_SIZE` appears nowhere else: the
cube is internal to that widget and is never drawn alone. It is documented here so the
two native surfaces are not diverged from, not as a form to build with.

**One arithmetic snag** if `AMSPreview` is ever reused: padding 7 plus four 14 px cubes
plus three 5 px gaps is 85, not the 82 the constant states. Callers size the preview
themselves today. Fix it once rather than per surface.
Loading