mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 01:57:41 +02:00
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user