mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
feat(vp): per-VP "Save AMS mapping" toggle + reprint auto-apply
Lets a reprint reuse the AMS slot the slicer itself picked, instead of re-deriving one from the file's static type/color. When a Print Queue VP has "Save AMS mapping" on, the slicer's own live-resolved ams_mapping (from the project_file MQTT command) is persisted onto the archive as extra_data.slicer_ams_mapping. A later reprint can reuse it via a new "Mapping" button in the filament-mapping panel — one click snaps every slot to the saved pick, click again reverts to auto-match. Archive cards and queue rows get an "AMS mapping saved" badge so it's visible beforehand. add_to_queue also falls back to the saved mapping automatically when the caller sends no explicit ams_mapping (e.g. a plain reprint with no per-slot edits). The queue item's own ams_mapping (used for that dispatch) is still captured unconditionally whenever the slicer provides it — that part is a correctness fix, not gated behind the toggle. Only the archive persistence for future reprints is opt-in. Split out from the original combined PR per review: this half is genuinely opt-in and low-risk (#2684). The dispatch-time validation gate that keeps a stored mapping honest (#1308) changes behaviour for every existing user and will land as its own PR. Review fixes applied: - _extract_slicer_ams_mapping_json: dropped the unreachable `v is None` arm and rejected bool explicitly (isinstance(v, int) accepts bool). - Translated the Russian docstring text to English. - save_ams_mapping's model comment moved to a trailing comment on the column line, matching the file's convention. - usingArchiveMapping now resets when the plate or archive changes, so the Mapping button can't read ON against a mapping it never applied. - Translated "Click to change slot assignment" and "Re-read". - add_to_queue's fallback is now called out explicitly in code comments and covered by three new integration tests (fallback fires, explicit mapping wins, unrelated extra_data doesn't false-trigger). Closes #2684
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Circle, Check, AlertTriangle, RefreshCw, ChevronDown, ChevronUp, Palette } from 'lucide-react';
|
||||
@@ -24,11 +24,50 @@ export function FilamentMapping({
|
||||
forceColorMatch,
|
||||
onForceColorMatchChange,
|
||||
plateLabel,
|
||||
archiveAmsMapping,
|
||||
}: FilamentMappingProps & { defaultExpanded?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
// "Mapping" toggle (only shown when the archive has a saved slicer pick):
|
||||
// ON selects every slot straight from `archiveAmsMapping`, bypassing the
|
||||
// type/color auto-match entirely — same mechanism as a manual per-slot
|
||||
// pick (`manualMappings`), just applied to every required slot at once.
|
||||
// OFF removes exactly those overrides so the panel falls back to its
|
||||
// normal auto-match, without touching any *other* manual picks the user
|
||||
// made by hand.
|
||||
const [usingArchiveMapping, setUsingArchiveMapping] = useState(false);
|
||||
|
||||
// Reset the toggle whenever the saved mapping it would apply changes — a
|
||||
// different plate selection or a different archive entirely. Without this
|
||||
// the button can read ON (green) from a previous archive/plate even though
|
||||
// it was never pressed against the mapping currently in scope.
|
||||
useEffect(() => {
|
||||
setUsingArchiveMapping(false);
|
||||
}, [archiveAmsMapping, plateLabel]);
|
||||
|
||||
const toggleArchiveMapping = () => {
|
||||
if (!archiveAmsMapping || !filamentReqs?.filaments) return;
|
||||
if (usingArchiveMapping) {
|
||||
const next = { ...manualMappings };
|
||||
for (const req of filamentReqs.filaments) {
|
||||
if (req.slot_id > 0) delete next[req.slot_id];
|
||||
}
|
||||
onManualMappingChange(next);
|
||||
setUsingArchiveMapping(false);
|
||||
return;
|
||||
}
|
||||
const next = { ...manualMappings };
|
||||
for (const req of filamentReqs.filaments) {
|
||||
const idx = req.slot_id - 1;
|
||||
if (req.slot_id > 0 && idx >= 0 && idx < archiveAmsMapping.length && archiveAmsMapping[idx] >= 0) {
|
||||
next[req.slot_id] = archiveAmsMapping[idx];
|
||||
}
|
||||
}
|
||||
onManualMappingChange(next);
|
||||
setUsingArchiveMapping(true);
|
||||
};
|
||||
|
||||
// Fetch printer status
|
||||
const { data: printerStatus } = useQuery({
|
||||
@@ -211,16 +250,33 @@ export function FilamentMapping({
|
||||
{isExpanded && (
|
||||
<div className="mt-2 bg-bambu-dark rounded-lg p-3 space-y-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-bambu-gray">Click to change slot assignment</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-bambu-gray/30 hover:border-bambu-gray hover:bg-bambu-dark-tertiary transition-colors text-bambu-gray hover:text-white"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
<span>Re-read</span>
|
||||
</button>
|
||||
<span className="text-xs text-bambu-gray">{t('printModal.clickToChangeSlot')}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{archiveAmsMapping && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleArchiveMapping}
|
||||
title={t('printModal.useArchiveMappingTooltip')}
|
||||
className={`flex items-center gap-1 px-2 py-0.5 text-xs rounded border transition-colors ${
|
||||
usingArchiveMapping
|
||||
? 'border-bambu-green bg-bambu-green/10 text-bambu-green'
|
||||
: 'border-bambu-gray/30 hover:border-bambu-gray hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
<span>{t('printModal.useArchiveMapping')}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-bambu-gray/30 hover:border-bambu-gray hover:bg-bambu-dark-tertiary transition-colors text-bambu-gray hover:text-white"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
<span>{t('printModal.reRead')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{filamentComparison.map((item, idx) => {
|
||||
// #1717: surface the same per-slot force-color-match checkbox here
|
||||
|
||||
@@ -325,6 +325,13 @@ export function PrintModal({
|
||||
// Get sliced_for_model from archive or library file
|
||||
const slicedForModel = archiveDetails?.sliced_for_model || libraryFileDetails?.sliced_for_model || null;
|
||||
|
||||
// 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.
|
||||
const archiveSlicerAmsMapping = !isLibraryFile
|
||||
? (archiveDetails?.extra_data?.slicer_ams_mapping as number[] | undefined)
|
||||
: undefined;
|
||||
|
||||
// Fetch plates for archives
|
||||
const { data: archivePlatesData, isError: archivePlatesError } = useQuery({
|
||||
queryKey: ['archive-plates', archiveId],
|
||||
@@ -1407,6 +1414,7 @@ export function PrintModal({
|
||||
onForceColorMatchChange={(slotId, value) =>
|
||||
setForceColorMatch((prev) => ({ ...prev, [slotId]: value }))
|
||||
}
|
||||
archiveAmsMapping={archiveSlicerAmsMapping}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -224,6 +224,14 @@ export interface FilamentMappingProps {
|
||||
* plate. Each plate prints its own subset of the file's slots and gets its
|
||||
* own AMS mapping, so the panels have to be told apart. */
|
||||
plateLabel?: string;
|
||||
/** The archive's own saved AMS-slot pick from the slicer
|
||||
* (`extra_data.slicer_ams_mapping`, written when the source virtual
|
||||
* printer has "Save AMS mapping" enabled) — position = slot_id-1, value =
|
||||
* global tray ID. When present, a "Mapping" toggle next to "Re-read" lets
|
||||
* the user select every slot from this array instead of the type/color
|
||||
* auto-match. Undefined/omitted when the archive has no saved mapping —
|
||||
* the toggle is hidden and behaviour is unchanged. */
|
||||
archiveAmsMapping?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user