fix(vp): gate the slicer's AMS pick behind the toggle and scope its badges (#2700)

Round-3 review of the "Save AMS mapping" PR.

The queue item's ams_mapping was set unconditionally, on the reasoning that
honouring the slicer's own pick is a correctness fix rather than a feature.
It is both. Storing a resolved mapping makes _ensure_ams_mapping return
early, so _compute_ams_mapping_for_printer never runs — and that function is
where prefer_lowest_filament lives, along with the AMS-filament-backup gate
that qualifies it (#1766), the inventory-remain overrides, and the per-slot
force-colour overrides. Every existing queue-mode VP pointed at a printer
would have quietly lost all of it on upgrade, without a setting to turn it
back on.

So save_ams_mapping now gates the queue item too, not just the archive
persistence. Off is exactly the old behaviour. The correctness case the PR
was written for — two spools of the same red PLA, and the slot the user
picked in the slicer thrown away — is still fixed, for anyone who asks for
it.

Force color match wins over it when both are on. Its only effect on a
fixed-printer item is the filament_overrides written onto the queue item,
and those are read inside the function a stored mapping skips, so the two
toggles sitting next to each other on the same card silently cancelled. The
dispatch now matches strictly, as asked, while the slicer's pick is still
saved onto the archive — that is what the toggle's name promises, and a
later reprint is a separate decision from this print. The queue-add fallback
applies the same rule to a request that carries force-colour overrides.

A mapping shorter than a plate's highest slot id cannot address that plate's
own slots, and _ensure_ams_mapping would have kept it anyway, since it only
rejects an all-unresolved one. Each plate now checks the length it needs and
falls back to a computed mapping if the array does not reach. Bambu Studio
sends a file-global array, so this normally never fires; it also means a
multi-plate Send All degrades safely if that ever stops being true.

The badges claimed more than they delivered. Both rendered whenever a saved
mapping existed, ignoring which printer it belonged to, while the tooltips
promised the reprint would reuse those exact spools — true only on the
printer the trays were resolved against. The queue row's flag is now
computed against that row's own printer, which is precisely when dispatch
reuses the mapping, and the archive card names the printer instead of
implying any of them will do. It hides itself when that printer no longer
exists. Retranslated in all 13 locales.

Frontend tests, which the PR had none of. The printer-scoping rule is now a
pure function rather than an inline expression, covered for the mismatched
printer, the no-printer-selected case that would otherwise compare undefined
against undefined, and malformed extra_data. The toggle's undo bookkeeping
is covered for unresolved slots, short mappings, and hand-made picks —
preserved when the toggle never wrote that slot, replaced when it did, which
is behaviour worth pinning either way.

Also reverts all three queue-mode switches when a save fails, not just the
new one; without it the card shows a setting the server rejected.
This commit is contained in:
maziggy
2026-07-31 16:16:39 +02:00
parent 4b4cb18a64
commit 4f2c073a34
28 changed files with 1098 additions and 219 deletions
@@ -0,0 +1,46 @@
/**
* Reading the archive's saved slicer AMS-slot pick back out of `extra_data`.
*
* A virtual printer with "Save AMS mapping" on stores the slicer's own
* live-resolved tray choice on the archive as
* `extra_data.slicer_ams_mapping = { mapping, printer_id }` (written by
* `ArchiveService.archive_print`). The tray IDs in `mapping` are global tray
* IDs, which only mean something against the AMS layout of the one printer
* they were resolved against — slot 3 on another printer can hold a completely
* different spool. `printer_id` records which printer that was.
*
* Lives here rather than inline in the modal so the printer-scoping rule can
* be tested on its own: it is the only thing standing between a saved mapping
* and the wrong physical spool.
*/
/** Shape of `extra_data.slicer_ams_mapping`. Every field optional — this is
* free-form JSON off the wire, and older archives predate the key entirely. */
export interface SavedSlicerAmsMapping {
mapping?: number[];
printer_id?: number;
}
/**
* The saved mapping, but only when it is safe to apply to `printerId`.
*
* Returns `undefined` — meaning "no saved mapping in scope, behave as before" —
* when the archive has none, when the stored value is malformed, when no
* printer is selected yet, or when the selected printer is not the one the
* mapping was resolved against.
*/
export function resolveArchiveSlicerAmsMapping(
extraData: Record<string, unknown> | null | undefined,
printerId: number | null | undefined,
): number[] | undefined {
// No printer selected means there is nothing to compare against. Bailing
// here also stops `undefined === undefined` from reading as a match below.
if (printerId == null) return undefined;
const saved = extraData?.slicer_ams_mapping as SavedSlicerAmsMapping | undefined;
if (!saved || typeof saved !== 'object') return undefined;
if (saved.printer_id !== printerId) return undefined;
if (!Array.isArray(saved.mapping) || saved.mapping.length === 0) return undefined;
return saved.mapping;
}
+11 -15
View File
@@ -22,6 +22,7 @@ import { getCurrencySymbol } from '../../utils/currency';
import { getBedTypeInfo } from '../../utils/bedType';
import { toDateTimeLocalValue, parseUTCDate } from '../../utils/date';
import { getGlobalTrayId, isPlaceholderDate, effectivePreferLowest } from '../../utils/amsHelpers';
import { resolveArchiveSlicerAmsMapping } from './archiveAmsMapping';
import { FilamentMapping } from './FilamentMapping';
import { FilamentOverride } from './FilamentOverride';
import { PlateSelector } from './PlateSelector';
@@ -327,21 +328,16 @@ export function PrintModal({
// The archive's own saved AMS-slot pick from the slicer (see the "Save AMS
// mapping" virtual-printer setting) — undefined for library files or
// archives that predate the feature / had it off at print time. Stored as
// `{mapping, printer_id}`: the tray IDs are only meaningful against the
// specific printer's AMS layout they were resolved from, so only surface
// them when reprinting on that same printer — a different printer's slot 3
// can hold a completely different spool.
const archiveSlicerAmsMappingRaw = !isLibraryFile
? (archiveDetails?.extra_data?.slicer_ams_mapping as
| { mapping?: number[]; printer_id?: number }
| undefined)
: undefined;
const archiveSlicerAmsMapping =
archiveSlicerAmsMappingRaw?.printer_id === effectivePrinterId &&
Array.isArray(archiveSlicerAmsMappingRaw.mapping)
? archiveSlicerAmsMappingRaw.mapping
: undefined;
// archives that predate the feature / had it off at print time, and
// deliberately undefined unless the selected printer is the one the mapping
// was resolved against. See `resolveArchiveSlicerAmsMapping`.
const archiveSlicerAmsMapping = useMemo(
() =>
isLibraryFile
? undefined
: resolveArchiveSlicerAmsMapping(archiveDetails?.extra_data, effectivePrinterId),
[isLibraryFile, archiveDetails?.extra_data, effectivePrinterId],
);
// Fetch plates for archives
const { data: archivePlatesData, isError: archivePlatesError } = useQuery({