Fix AMS auto-matching when multiple trays have same tray_info_idx

The tray_info_idx field is a filament TYPE identifier (e.g., "GFA00" for
generic PLA), not unique per spool. When multiple AMS trays are loaded
with the same filament type, the previous code used find() which always
returned the first match regardless of color.

Now checks if tray_info_idx is unique among available trays:
- If unique: use that tray as definitive match (existing behavior)
- If not unique: fall back to color matching among matching trays

Fixed in both backend (print_scheduler.py) and frontend (useFilamentMapping.ts).

Closes #245
This commit is contained in:
maziggy
2026-02-04 14:03:50 +01:00
parent 81d18cb8bd
commit 3571cd1a42
7 changed files with 309 additions and 205 deletions
+111 -70
View File
@@ -62,12 +62,13 @@ export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined):
* Compute AMS mapping for a printer given filament requirements and printer status.
* This is a non-hook version that can be called imperatively (e.g., in a loop for multiple printers).
*
* Priority: tray_info_idx match > exact color match > similar color match > type-only match
* Priority: unique tray_info_idx match > exact color match > similar color match > type-only match
*
* The tray_info_idx is a unique spool identifier stored in the 3MF file when the user
* slices in Bambu Studio. If the same spool is still loaded in the printer (same
* tray_info_idx), we use that tray. This ensures the exact spool selected during
* slicing is used, even if multiple trays have the same type and color.
* The tray_info_idx is a filament type identifier stored in the 3MF file when the user
* slices (e.g., "GFA00" for generic PLA, "P4d64437" for custom presets). If the same
* tray_info_idx appears in only ONE available tray, we use that tray. If multiple trays
* have the same tray_info_idx (e.g., two spools of generic PLA), we fall back to color
* matching among those trays.
*
* @param filamentReqs - Required filaments from the 3MF file
* @param printerStatus - Current printer status with AMS information
@@ -88,43 +89,63 @@ export function computeAmsMapping(
const comparisons = filamentReqs.filaments.map((req) => {
const reqTrayInfoIdx = req.tray_info_idx || '';
// First priority: match by tray_info_idx (exact spool identity)
// Only match if both have non-empty tray_info_idx
const idxMatch = reqTrayInfoIdx
? loadedFilaments.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.trayInfoIdx &&
f.trayInfoIdx === reqTrayInfoIdx
)
: undefined;
// Get available trays (not already used)
const available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
// Fall back to type+color matching
const exactMatch =
!idxMatch &&
loadedFilaments.find(
let idxMatch: LoadedFilament | undefined;
let exactMatch: LoadedFilament | undefined;
let similarMatch: LoadedFilament | undefined;
let typeOnlyMatch: LoadedFilament | undefined;
// Check if tray_info_idx is unique among available trays
if (reqTrayInfoIdx) {
const idxMatches = available.filter((f) => f.trayInfoIdx === reqTrayInfoIdx);
if (idxMatches.length === 1) {
// Unique tray_info_idx - use it as definitive match
idxMatch = idxMatches[0];
} else if (idxMatches.length > 1) {
// Multiple trays with same tray_info_idx - use color matching among them
exactMatch = idxMatches.find(
(f) =>
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
if (!exactMatch) {
similarMatch = idxMatches.find(
(f) =>
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
}
if (!exactMatch && !similarMatch) {
typeOnlyMatch = idxMatches.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
);
}
}
}
// If no idx match, do standard type/color matching on all available trays
if (!idxMatch && !exactMatch && !similarMatch && !typeOnlyMatch) {
exactMatch = available.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
const similarMatch =
!idxMatch &&
!exactMatch &&
loadedFilaments.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
const typeOnlyMatch =
!idxMatch &&
!exactMatch &&
!similarMatch &&
loadedFilaments.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
);
if (!exactMatch) {
similarMatch = available.find(
(f) =>
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
}
if (!exactMatch && !similarMatch) {
typeOnlyMatch = available.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
);
}
}
const loaded = idxMatch || exactMatch || similarMatch || typeOnlyMatch || undefined;
// Mark this tray as used so it won't be assigned to another slot
@@ -329,47 +350,67 @@ export function useFilamentMapping(
}
// Auto-match: Find a loaded filament
// Priority: tray_info_idx match > exact color match > similar color match > type-only match
// Priority: unique tray_info_idx match > exact color match > similar color match > type-only match
// IMPORTANT: Exclude trays that are already assigned (manually or auto)
const reqTrayInfoIdx = req.tray_info_idx || '';
// First priority: match by tray_info_idx (exact spool identity)
// This ensures the exact spool selected during slicing is used
const idxMatch = reqTrayInfoIdx
? loadedFilaments.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.trayInfoIdx &&
f.trayInfoIdx === reqTrayInfoIdx
)
: undefined;
// Get available trays (not already used)
const available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
// Fall back to type+color matching
const exactMatch =
!idxMatch &&
loadedFilaments.find(
let idxMatch: LoadedFilament | undefined;
let exactMatch: LoadedFilament | undefined;
let similarMatch: LoadedFilament | undefined;
let typeOnlyMatch: LoadedFilament | undefined;
// Check if tray_info_idx is unique among available trays
if (reqTrayInfoIdx) {
const idxMatches = available.filter((f) => f.trayInfoIdx === reqTrayInfoIdx);
if (idxMatches.length === 1) {
// Unique tray_info_idx - use it as definitive match
idxMatch = idxMatches[0];
} else if (idxMatches.length > 1) {
// Multiple trays with same tray_info_idx - use color matching among them
exactMatch = idxMatches.find(
(f) =>
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
if (!exactMatch) {
similarMatch = idxMatches.find(
(f) =>
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
}
if (!exactMatch && !similarMatch) {
typeOnlyMatch = idxMatches.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
);
}
}
}
// If no idx match, do standard type/color matching on all available trays
if (!idxMatch && !exactMatch && !similarMatch && !typeOnlyMatch) {
exactMatch = available.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
const similarMatch =
!idxMatch &&
!exactMatch &&
loadedFilaments.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
const typeOnlyMatch =
!idxMatch &&
!exactMatch &&
!similarMatch &&
loadedFilaments.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
);
if (!exactMatch) {
similarMatch = available.find(
(f) =>
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
}
if (!exactMatch && !similarMatch) {
typeOnlyMatch = available.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
);
}
}
const loaded = idxMatch || exactMatch || similarMatch || typeOnlyMatch || undefined;
// Mark this tray as used so it won't be assigned to another slot