|
|
@@ -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">
|