Add print options to reprint modal and fix AMS mapping for multi-color prints

Features:
  - Add print options UI to reprint modal (bed leveling, flow calibration,
    vibration calibration, first layer inspection, timelapse)
  - Collapsible "Print Options" section with toggle switches
  - Default values: bed leveling=on, vibration_cali=on, others=off

  Fixes:
  - Fix AMS mapping format to match Bambu Studio exactly
    - Use slot_id from 3MF to position tray IDs correctly
    - Fill unused slots with -1 placeholder
    - Add ams_mapping2 with detailed {ams_id, slot_id} pairs
  - Fix print command format for proper printer recognition
    - Add auto_bed_leveling, cfg, extrude_cali_flag, nozzle_offset_cali
    - Use American spelling "bed_leveling" (not British "bed_levelling")
    - Match sequence_id format from official apps
  - Prevent duplicate tray assignment in filament matching

  This resolves the "stuck at filament change" issue on multi-color reprints
  affecting X1C and other multi-AMS printer configurations.
This commit is contained in:
maziggy
2026-01-08 16:27:29 +01:00
parent 63c08c8154
commit 000ef632d8
11 changed files with 416 additions and 153 deletions
+117 -5
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { X, Printer, Loader2, AlertTriangle, Check, Circle, RefreshCw } from 'lucide-react';
import { X, Printer, Loader2, AlertTriangle, Check, Circle, RefreshCw, ChevronDown, ChevronUp, Settings } from 'lucide-react';
import { api } from '../api/client';
import { Card, CardContent } from './Card';
import { Button } from './Button';
@@ -12,10 +12,29 @@ interface ReprintModalProps {
onSuccess: () => void;
}
// Print options with defaults
interface PrintOptions {
timelapse: boolean;
bed_levelling: boolean;
flow_cali: boolean;
vibration_cali: boolean;
layer_inspect: boolean;
}
const DEFAULT_PRINT_OPTIONS: PrintOptions = {
bed_levelling: true,
flow_cali: false,
vibration_cali: true,
layer_inspect: false,
timelapse: false,
};
export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: ReprintModalProps) {
const queryClient = useQueryClient();
const [selectedPrinter, setSelectedPrinter] = useState<number | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [showOptions, setShowOptions] = useState(false);
const [printOptions, setPrintOptions] = useState<PrintOptions>(DEFAULT_PRINT_OPTIONS);
// Close on Escape key
useEffect(() => {
@@ -47,7 +66,10 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
const reprintMutation = useMutation({
mutationFn: () => {
if (!selectedPrinter) throw new Error('No printer selected');
return api.reprintArchive(archiveId, selectedPrinter);
return api.reprintArchive(archiveId, selectedPrinter, {
ams_mapping: amsMapping,
...printOptions,
});
},
onSuccess: () => {
onSuccess();
@@ -73,6 +95,13 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
return `AMS-${letter} Slot ${trayId + 1}`;
};
// Calculate global tray ID for MQTT command
// Regular AMS: (ams_id * 4) + slot_id, External: 254
const getGlobalTrayId = (amsId: number, trayId: number, isExternal: boolean): number => {
if (isExternal) return 254;
return amsId * 4 + trayId;
};
// Build a list of all loaded filaments from printer's AMS/HT/External with location info
const loadedFilaments = useMemo(() => {
const filaments: Array<{
@@ -83,6 +112,7 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
isHt: boolean;
isExternal: boolean;
label: string;
globalTrayId: number;
}> = [];
// Add filaments from all AMS units (regular and HT)
@@ -98,6 +128,7 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
isHt,
isExternal: false,
label: formatSlotLabel(amsUnit.id, tray.id, isHt, false),
globalTrayId: getGlobalTrayId(amsUnit.id, tray.id, false),
});
}
});
@@ -113,6 +144,7 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
isHt: false,
isExternal: true,
label: 'External',
globalTrayId: 254,
});
}
@@ -149,22 +181,34 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
Math.abs(b1 - b2) <= threshold;
};
// Track which trays have been assigned to avoid duplicates
const usedTrayIds = new Set<number>();
return filamentReqs.filaments.map((req) => {
// Find a loaded filament that matches by TYPE (printer will auto-map the slot)
// Priority: exact color match > similar color match > type-only match
// IMPORTANT: Exclude trays that are already assigned to another slot
const exactMatch = loadedFilaments.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase() &&
(f) => !usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
const similarMatch = !exactMatch && loadedFilaments.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase() &&
(f) => !usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
const typeOnlyMatch = !exactMatch && !similarMatch && loadedFilaments.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
(f) => !usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase()
);
const loaded = exactMatch || similarMatch || typeOnlyMatch || undefined;
// Mark this tray as used so it won't be assigned to another slot
if (loaded) {
usedTrayIds.add(loaded.globalTrayId);
}
const hasFilament = !!loaded;
const typeMatch = hasFilament;
const colorMatch = !!exactMatch || !!similarMatch;
@@ -190,6 +234,30 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
});
}, [filamentReqs, loadedFilaments]);
// Build AMS mapping from auto-matched filaments
// Format: array matching 3MF filament slot structure
// Position = slot_id - 1 (0-indexed), value = global tray ID or -1 for unused
// e.g., slots 1 and 3 used with trays 5 and 2 → [5, -1, 2, -1]
const amsMapping = useMemo(() => {
if (filamentComparison.length === 0) return undefined;
// Find the max slot_id to determine array size
const maxSlotId = Math.max(...filamentComparison.map((f) => f.slot_id || 0));
if (maxSlotId <= 0) return undefined;
// Create array with -1 for all positions
const mapping = new Array(maxSlotId).fill(-1);
// Fill in tray IDs at correct positions (slot_id - 1)
filamentComparison.forEach((f) => {
if (f.slot_id && f.slot_id > 0) {
mapping[f.slot_id - 1] = f.loaded?.globalTrayId ?? -1;
}
});
return mapping;
}, [filamentComparison]);
const hasTypeMismatch = filamentComparison.some((f) => f.status === 'mismatch');
return (
@@ -366,6 +434,50 @@ export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: Rep
</div>
)}
{/* Print Options */}
{selectedPrinter && (
<div className="mb-4">
<button
onClick={() => setShowOptions(!showOptions)}
className="flex items-center gap-2 text-sm text-bambu-gray hover:text-white transition-colors w-full"
>
<Settings className="w-4 h-4" />
<span>Print Options</span>
{showOptions ? <ChevronUp className="w-4 h-4 ml-auto" /> : <ChevronDown className="w-4 h-4 ml-auto" />}
</button>
{showOptions && (
<div className="mt-2 bg-bambu-dark rounded-lg p-3 space-y-2">
{[
{ key: 'bed_levelling', label: 'Bed Levelling', desc: 'Auto-level bed before print' },
{ key: 'flow_cali', label: 'Flow Calibration', desc: 'Calibrate extrusion flow' },
{ key: 'vibration_cali', label: 'Vibration Calibration', desc: 'Reduce ringing artifacts' },
{ key: 'layer_inspect', label: 'First Layer Inspection', desc: 'AI inspection of first layer' },
{ key: 'timelapse', label: 'Timelapse', desc: 'Record timelapse video' },
].map(({ key, label, desc }) => (
<label key={key} className="flex items-center justify-between cursor-pointer group">
<div>
<span className="text-sm text-white">{label}</span>
<p className="text-xs text-bambu-gray">{desc}</p>
</div>
<div
className={`relative w-10 h-5 rounded-full transition-colors ${
printOptions[key as keyof PrintOptions] ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
}`}
onClick={() => setPrintOptions((prev) => ({ ...prev, [key]: !prev[key as keyof PrintOptions] }))}
>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
printOptions[key as keyof PrintOptions] ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</div>
</label>
))}
</div>
)}
</div>
)}
{/* Error message */}
{reprintMutation.isError && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">