Skip to content

Commit 1995bc1

Browse files
committed
feat: printer boards, unit-based filament storage, group selection
Printers view - The board holds identified OBJECTS, not one card per machine: placement, snapping, selection and dragging all key off a single `data-board-id`, so a new kind of widget is a case rather than a second copy of the board. - A connected machine's filament storage is shown as its UNITS — one named block per physical object (Ext., AMS 1, AMS 2…) in a movable widget beside the card, instead of one flat strip. Storage on the machine - `units` map on `printers/{brand}/devices/{id}`: one entry per physical unit, carrying its shape, a renamable label, a position on each board, and slots holding what the user assigned (`uids`) plus what the machine last reported (stamped `seenAt`). A map, so one unit is written without rewriting its neighbours; on the document, because units are few and always read with the machine. - Every brand describes its units from its own PROTOCOL.md, behind `getUnits` on the registry — alongside `getSlots`, which normalises six incompatible wire formats into one shape the rest of the app can read. - The seeder never overwrites what the user owns (label, shape, positions, assignments), and a unit that stops being reported is marked absent rather than deleted: unplugging an Elegoo Canvas hub must not discard what was in it. Both boards - Rubber-band selection and group move, one shared implementation so the two boards cannot drift apart. Panning moves to the wheel button, since the left button now draws a selection. - A move handle on every card: outside Arrange it is the only thing that moves a machine or a rack, so nothing slides because you reached past a title. - Held / selected / colliding written as one ordered block per board, so a collision warning can never be outranked by a lift. - A card dragged past the edge of its board is dropped where it last stood; it holds pointer capture, so nothing else told it the cursor had left. Groundwork - `printers/filament.js` resolves a TigerTag spool into what any of the six brands can be told, from the mobile app's proven behaviour rather than its code. Never refuses: an unknown brand or material becomes a Generic stand-in in the right colour at the right temperature. Docs - `docs/printer-storage-terms.md` — the agreed vocabulary (slot = one place for one spool, unit = the physical box grouping slots) and the stored shape, with the reasoning, so the decision is not re-litigated later.
1 parent be84d4a commit 1995bc1

28 files changed

Lines changed: 2329 additions & 303 deletions

‎ROADMAP.md‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,46 @@ Grouped by domain. Versions in parentheses are the release that landed the featu
125125

126126
## 🚧 Next up — concrete work
127127

128+
### One board for machines, AMS and racks
129+
130+
The printer view becomes the storage plan — same grid, same zoom, same panning,
131+
same free placement — and then the pieces join up:
132+
133+
1. **The printer view is a plan.** Grid, zoom, wheel, middle-drag pan, cards
134+
placed by hand. Sections are already gone; the plan machinery is the rack
135+
view's, made board-agnostic rather than copied.
136+
2. **An AMS is a rack assigned to a printer.** Derived automatically from the
137+
slots the machine reports when it connects — so it carries no shape of its
138+
own: it cannot be resized or deleted, only moved. Everything already built
139+
for racks (slots, drag-drop, locking, depth, the colour filter) then applies
140+
with nothing rewritten, and each unit can sit where it really stands next to
141+
its machine. **Deleting a printer deletes the AMS racks bound to it** —
142+
they describe hardware that is no longer there.
143+
2b. **The slots show on the machine's own card first.** Before any rack exists,
144+
a connected machine displays its feed slots as a row of colour chips under
145+
the progress bar, each naming its filament on hover (*R3D · PLA SnapSpeed*).
146+
It is the cheapest possible proof that the driver's slot data is correct and
147+
complete — and it is worth having on its own, so it ships before the rack.
148+
3. **One unified board** — racks, printers and AMS together, so a spool can be
149+
dragged off a shelf straight into a machine's slot.
150+
151+
The load-bearing idea is the second: treat an AMS as a rack with a printer id,
152+
and the third step nearly falls out of the first two.
153+
154+
**Binding a rack to a machine.** The rack carries `printer: { brand, id, unit }`
155+
— the printer's document id, plus which unit of it this is, because one machine
156+
can have several AMS/CFS units and each is its own rack, free to sit where that
157+
unit really stands. Deleting the printer deletes every rack that names it. A
158+
bound rack shows its machine on its card, so the pair reads as a pair.
159+
160+
**Not every machine has an AMS, and it does not matter.** A Snapmaker U1 has
161+
four tool-changer nozzles rather than a magazine; it still becomes one rack of
162+
one row × four slots. The rack does not model the hardware's kind — only how
163+
many places it offers and which machine they belong to. The slot count comes
164+
from the connection (`nozzleCount` for the U1), and it survives disconnection,
165+
because a machine being asleep does not remove its bays.
166+
167+
128168
Items where the spec is written and we know roughly how to do it. Ranked by ratio (impact / effort × risk).
129169

130170
> ### 🐿️🐿️ Sprint mode — days, not months
@@ -714,6 +754,77 @@ Today a spool lives in **one** place: its rack slot (`rack: { id, level, positio
714754
715755
> **Why dual-location and not a move?** Physically a spool is either in the rack or on the printer — but the user wants the home slot remembered so unmounting sends it back to the right place, and so the rack view doesn't "lose" a spool just because it's printing right now. The printer mount is therefore an **overlay**, never a relocation.
716756
757+
#### ⚠️ The hard part is not the transport — it is the material mapping
758+
759+
Writing to a machine is **already solved**: every brand's filament sheet pushes a
760+
set-filament command today (Bambu `ams_filament_setting` over MQTT, and the
761+
equivalent per brand). Assigning a spool to a slot therefore reuses a path that
762+
already exists and is proven.
763+
764+
What does **not** exist is the translation. Each brand ships its **own** vendor +
765+
material catalogue (`SNAP_FIL_VENDOR_MATERIALS`, `CRE_FIL_VENDOR_MATERIALS`,
766+
`FFG_FIL_VENDOR_MATERIALS`, Bambu's material list), and they are not the same
767+
shape:
768+
769+
- **Bambu needs an exact id** — `tray_info_idx` / `bambuID`, a number from its own
770+
catalogue. A string it has never heard of is not an option.
771+
- **The others take vendor + material strings**, falling back to a `Generic`
772+
vendor list when they do not know the brand.
773+
774+
So assigning *R3D · PLA Basic* means resolving a TigerTag identity into whatever
775+
that particular machine can be told. It is lossy — a printer that has never heard
776+
of R3D can still be told "Generic PLA" with the right colour and temperatures,
777+
which is the honest answer — and a payload a machine does not recognise is
778+
**refused outright**, so this is a decision per brand, not a lookup.
779+
780+
**This mapping already exists — in the MOBILE app.** Tiger NFC Connect
781+
(`TigerTag_RFID_Connect`) scans a spool and pushes it into the machine today, for
782+
all six brands. Studio must **port that logic, not re-derive it**: a second,
783+
independently-guessed mapping would drift and the two apps would set the same
784+
spool differently on the same printer.
785+
786+
**Port the LEGACY per-brand pages, not `lib/printers_link/`.** The newer
787+
`printers_link` drivers are the tidier code but they have regressed against the
788+
per-brand screens that were proven on hardware, in three places:
789+
790+
| Brand | `printers_link` does | the legacy page does — port THIS |
791+
|---|---|---|
792+
| Bambu | never passes the tag's own `onlineBambuID`, so it always resolves through the catalogue | `bambu_mqtt_page.dart` — priority chain: tag `onlineBambuID` (unless `'-'`) → catalogue `metadata.bambuID` → `'GFA00'` |
793+
| Elegoo | always `method 2003` + refresh `2005`; colour without the `#` | `elegoo_mqtt_page.dart` — `1055` when the Canvas hub is absent, `2003` when present (matching the slicer), colour as `#RRGGBB` |
794+
| Creality | hardcodes `name: 'Generic <type>'`, discarding the resolved label | `creality_websocket_page.dart` — uses the product's own Creality label when there is one |
795+
796+
**The catalogue: Studio is already better placed than mobile.** The mapping table
797+
is the material catalogue itself — `metadata.bambuID` (Bambu `tray_info_idx`) and
798+
`metadata.crealityID` (Creality `rfid`). Mobile ships a **truncated** copy of it,
799+
missing `material_type`, `filled_type` and `crealityPressureAdvance`, so its Bambu
800+
`tray_type` silently degrades to the raw label and its Creality pressure is always
801+
the default. Studio's bundled `assets/db/tigertag/id_material.json` is the
802+
**complete** 113-entry version (80 with `bambuID`, 67 with `crealityID`, 109 with
803+
`material_type`) and is already refreshed from the API. Read it from there; do not
804+
copy mobile's asset.
805+
806+
**The fallback policy to copy: never refuse, always substitute a Generic.**
807+
Unknown vendor → the literal `Generic`. Unknown material → `GFA00` (Bambu Generic
808+
PLA) / `'0'` (Creality) / `0x0000` (Elegoo) plus that brand's default temperatures.
809+
Colour and temperatures travel cleanly in every case; only the *identity* is lossy.
810+
811+
**One mobile defect NOT to copy:** an unrecognised material id resolves to the
812+
literal string `"Unknown Material"`, which is then pushed to the printer as the
813+
filament type. An unknown id must fall back to the Generic entry, not send a
814+
sentence as a material name.
815+
816+
**Temperatures come from the CATALOGUE, not from the chip.** Mobile deliberately
817+
uses `recommended.nozzleTempMin/Max` and ignores both the chip's own temperature
818+
bytes and the product API's. Match that, or the two apps will set different
819+
temperatures for the same spool.
820+
821+
**Post-write refresh is part of the write.** Bambu re-issues `pushall` on the ack;
822+
Elegoo refreshes at +1000 ms (a documented firmware processing window); Snapmaker
823+
queries `print_task_config` at +500 ms; FlashForge polls `/detail` at +1 s and
824+
deliberately does not await its POST. These delays are firmware tolerances, not
825+
cosmetics — a port that drops them looks frozen or shows stale slots.
826+
827+
717828
#### 🗃️ Data model
718829
719830
- **The PRINTER owns the occupancy — the slot stores the spool UID(s), not a field on the spool.** The printer knows what it contains, not the other way round (this is how an AMS itself reasons). Each slot on the printer doc carries a `uids` array: **empty = free, 1 UID = a plain spool, 2 UIDs = a twin pair** (a single physical spool with two linked tags — both stored so a scan of *either* tag resolves to the slot, without needing the twin link at read time). The spool doc gets **no `mount` field**; mount state is purely derived from the printers.

‎WORKLOG.md‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1-
# Worklog — v2.21.1 (in progress)
1+
# Worklog — v2.21.2 (in progress)
22

33
## Added
4+
- **A connected machine's filament storage is shown as its UNITS** — one named block per physical object (Ext., AMS 1, AMS 2…) in the widget beside its card, rather than one flat strip: an X1C with two AMS and a spool arm is three boxes standing in three places, and thirteen squares in a line said none of that. **The storage itself is stored on the machine**, as a `units` map on its own document — one entry per physical unit: an AMS, CFS or ACE box, a holder pair, the external arm. A map rather than an array so one unit can be written without rewriting its neighbours, and on the document rather than in a subcollection because the units are few, are always read with the machine, and would otherwise be invisible to anyone reading the printer document. Each carries its shape, a user-renamable label, a position on *each* board, and its slots: what the user assigned (`uids`) and what the machine last reported, stamped with `seenAt` so a reader knows how old it is. Every brand now describes its units, not just a flat slot list, from its own `PROTOCOL.md`. The seeder never overwrites what the user owns — label, shape, both positions and assignments — and a unit that stops being reported is marked absent rather than deleted, because unplugging an Elegoo's Canvas hub must not discard what was in it. The agreed vocabulary and shape: `docs/printer-storage-terms.md` — `renderer/inventory.js`, `renderer/printers/*/index.js`, `docs/printer-storage-terms.md`, `docs/firestore-schema.md`, `llms.txt`
5+
- Groundwork for assigning an inventory spool to a machine slot: one resolver (`renderer/printers/filament.js`) turns a TigerTag spool into the values any of the six brands can be told — Bambu's catalogue id and tray type, Creality's id and pressure, the family/fill split, the vendor token, the temperatures, and each brand's own colour format. Written from the mobile app's proven behaviour, not from its code. Never refuses: an unknown brand or material becomes a Generic stand-in in the right colour at the right temperature, and the result flags whether the identity is exact. Internal — no user-facing change yet — `renderer/printers/filament.js`
6+
- A connected machine's feed slots are their own movable object on the printer board, next to the machine rather than stuck under its card — placed, snapped and selected like anything else there. The board itself now keys everything off one `data-board-id` instead of assuming one object per machine, which is what the AMS racks will plug into. The widget shows the same coloured squares the machine's own panel draws, each carrying its material family (PLA, ABS, PETG…), at exactly a rack slot's size and scale in picture view (64 × 64, 6 px apart) — a machine's bay and a shelf's bay hold the same object, so a spool must not appear to change size when it moves between them — in balanced rows of at most five, the widget sizing itself to its bays. An unknown bay reads `?`, a bay with a colour but no declared material reads `—`. Hovering one names the bay, the filament's brand and its variant (*E1 — R3D · PLA Basic*); an empty bay is drawn as an outline rather than skipped, so the strip never shortens when a spool runs out. Each brand now normalises its own wire format — AMS trays, CFS materials, ACE slots, tool-changer nozzles — behind one `getSlots` on the brand registry, so the slots can be read anywhere instead of only inside each brand's own filament card — `renderer/inventory.js`, `renderer/printers/*/index.js`, `renderer/css/40-printers.css`
7+
- A move handle on every card, left of the ⋮, on both boards. Outside Arrange it is the only thing that moves a machine or a rack — the rest of a printer card opens its panel, and a rack no longer slides because you reached past its title — while in Arrange the whole card stays a handle. New `icon_move` glyph — `renderer/inventory.js`, `renderer/css/30-racks.css`, `renderer/css/40-printers.css`, `renderer/css/70-detail-misc.css`, `assets/svg/icons/icon_move.svg`
8+
- Rubber-band selection on both boards — sweep the empty board to select several machines (Printers) or racks (Storage, in Arrange mode only), shift to add to the selection, then drag any selected card to move the whole group at its existing offsets. One shared implementation (`wirePlanMarquee`) so the two boards cannot drift apart — `renderer/inventory.js`, `renderer/css/30-racks.css`, `renderer/css/40-printers.css`
49

510
## Changed
11+
- A printer card is a switch: clicking the machine whose panel is already open closes it, instead of leaving the ✕ as the only way back — `renderer/inventory.js`
12+
- Panning both boards is now the wheel button only; the left button on empty board draws a selection instead — `renderer/inventory.js`
13+
- Several racks can be selected at once, where it used to be one (Arrange mode, as before — outside it a press on the board still belongs to the racks and their slots) — `renderer/inventory.js`, `renderer/css/30-racks.css`
14+
- In a group move, each carried card answers for its own landing spot, so the red overlap edge names the machine or rack actually sitting on something. Still a warning — a drop is never refused — `renderer/inventory.js`
15+
- **The printer view is a board now, like Storage.** It used to split into "Connected" and "Offline" sections, so a machine losing its link changed place — you lost track of it at the moment you were looking for it, and a card you had put somewhere never stayed put. One flat list: the state is already a coloured dot on the card and did not also need to be a change of address. Each machine is then placed by hand, on a permanent grid, with the storage plan's own magnetism and guides — the geometry and the panning were made board-agnostic rather than duplicated, so one implementation serves both. Wheel to zoom, middle-drag to pan, buttons and a typed value from 30 % up, and the point under the cursor is held still so zooming can never be mistaken for something having moved. The board carries its own zoom (`tigertag.printer.planZoom`), since the two plans hold different things at different scales. The position lives on the printer as `plan: {x, y, z}`, so it follows the account; a machine never placed flows below the arrangement instead of landing on top of it, and the one moved last sits in front. **The whole of the old organisation went with it**: the sort drag-and-drop that rewrote `sortIndex` behind the arrangement, the state partition and its section headers, the full grid rebuild that fired whenever a machine blinked — a connect or a disconnect now touches one dot, because nothing about a card's position depends on its state any more — the grip that only ever existed for the sort drag, and the trailing "+ add a printer" card, which had no place on a board and duplicated the "+" in the search bar. The whole card is the handle now: a click still opens the panel, and the move begins after 4 px of travel. `sortIndex` survives as the order of the table view. — `renderer/inventory.js`, `renderer/css/40-printers.css`
16+
17+
618

719
## Fixed
8-
- **Every spool in a back row was unranked on launch** — a two-deep rack came back with only its front row filled, and the rest sitting in "Not stored". Storage runs a self-heal that finds two spools sharing one slot and evicts the extras, and its slot key was `rack | level | position` — **without the depth**. A spool behind therefore shared a key with the spool in front of it, so a full two-deep column read as a pile of duplicates and everything but the front one was written back to `rack: null`. Caught in the app's own log: `[racks] self-heal: 57 spool(s) in already-occupied slots → unranked`. The depth is part of the slot now: two rows deep is not a collision, it is the feature. It happened behind the "What's New" window, which opens 2.5 s after launch, so the damage was only discovered when that window was dismissed. — `renderer/inventory.js`
9-
- **One half of a twin pair was unranked on launch, at random.** A twin pair is ONE physical spool wearing two chips, so both halves legitimately share a slot — and the duplicate-slot heal knows it, grouping them before it counts. But only one of the two documents carries the field that links them (which is exactly why `_markTwinPairs` flags both sides rather than trusting it), and the lookup that resolves a twin only ever read that one direction. So the pair read as a legitimate couple or as two rivals depending on which half the heal happened to visit first — a coin toss, every launch. The lookup reads both ways now. It was not confined to the heal: **nine** places resolve a twin, including the slot write, which therefore reached only one of the two documents depending on the direction. Fixed at the source, in the one function. — `renderer/inventory.js`
10-
- **Two heuristics that write `rack: null` on real spools now refuse to run when they would clear a large share of the stock.** The duplicate-slot heal and the orphan-reference sweep both act on a guess — "you are on top of someone" / "your rack is gone" — and a guess that erases where a user put a quarter of their collection has misread something rather than found that much genuine mess. Both now step aside and say so in the log instead of writing the mistake to every document they can reach. The orphan sweep additionally waits for a racks snapshot **confirmed by the server**: a cold start is served from a local cache that may name fewer racks than exist, and acting on it unranks everything living in the racks it forgot. — `renderer/inventory.js`
20+
- Grabbing a card on either board now rings it in the accent colour. The only sign of a grab used to be a drop shadow, which on a dark board under a card that has barely moved is no sign at all — a grab that took and one that missed looked the same. The three states are written as one ordered block per board (held < selected < colliding) so a collision can never be outranked by a lift — `renderer/css/30-racks.css`, `renderer/css/40-printers.css`
21+
- A card dragged past the edge of its board is now dropped where it last stood, on both boards. The card holds pointer capture, so nothing told it the cursor had left — it kept following a pointer that was no longer over the map, and you had to travel back inside just to let go — `renderer/inventory.js`
22+
- Clicking the ⋮ on a printer card also opened the machine's side panel behind the menu: the press armed an open through the rebuild-safety fallback, which the click handler then never got to cancel — `renderer/inventory.js`
23+
- The closed-hand cursor now appears the moment you press a rack or a machine, instead of after the first pixels of travel, and stays closed wherever the pointer goes during the drag — `renderer/inventory.js`, `renderer/css/30-racks.css`
24+
- Overlap warning on the Storage board is a red outline again, and only that: it used to recolour an outline that Arrange mode alone declared (so nothing showed outside Arrange), and the red wash it painted over the card replaced the drag shadow as well — `renderer/css/30-racks.css`
1125

1226
## Removed
1327

1428
## i18n
29+
- Added: `actionMove`, `rackBoundToPrinter` — 9 locales

‎assets/svg/icons/icon_move.svg‎

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)