From bdf9518a723007cc53bca7934f68f48bce30b2b0 Mon Sep 17 00:00:00 2001 From: maziggy Date: Mon, 12 Jan 2026 08:26:29 +0100 Subject: [PATCH] - Created shared color utility (frontend/src/utils/colors.ts): - Moved BAMBU_HEX_COLORS mapping and getColorName()/hexToColorName() functions to a shared location - Exported functions for use across components - Updated modals to use shared colors: - ReprintModal.tsx - removed ~100 lines of duplicate color code, now imports from utils/colors - AddToQueueModal.tsx - same cleanup - EditQueueItemModal.tsx - same cleanup - Added AMS mapping to EditQueueItemModal: - Added state for showFilamentMapping, isRefreshing, manualMappings - Added queries for filament requirements and printer status - Added loadedFilaments, filamentComparison, amsMapping memos - Added collapsible filament mapping UI section (same as other modals) - Initializes manualMappings from existing item.ams_mapping - Submits ams_mapping when saving changes - Backend changes: - Added ams_mapping field to PrintQueueItemUpdate schema - Updated update_queue_item route to serialize ams_mapping to JSON - Frontend API client: - Added ams_mapping to PrintQueueItemUpdate interface - Changed "Print" to "Reprint" on archive card button (ArchivesPage.tsx:690) - Bumped service worker to v22 --- backend/app/api/routes/print_queue.py | 4 + backend/app/schemas/print_queue.py | 1 + frontend/public/sw.js | 4 +- frontend/src/api/client.ts | 1 + frontend/src/components/AddToQueueModal.tsx | 106 +-- .../src/components/EditQueueItemModal.tsx | 363 +++++++- frontend/src/components/ReprintModal.tsx | 106 +-- frontend/src/pages/ArchivesPage.tsx | 2 +- frontend/src/utils/colors.ts | 107 +++ .../{index-BQZgdf1x.js => index-rSeEpV8h.js} | 834 +++++++++--------- static/index.html | 2 +- 11 files changed, 896 insertions(+), 634 deletions(-) create mode 100644 frontend/src/utils/colors.ts rename static/assets/{index-BQZgdf1x.js => index-rSeEpV8h.js} (66%) diff --git a/backend/app/api/routes/print_queue.py b/backend/app/api/routes/print_queue.py index d0f167270..d76f9b30e 100644 --- a/backend/app/api/routes/print_queue.py +++ b/backend/app/api/routes/print_queue.py @@ -168,6 +168,10 @@ async def update_queue_item( if not result.scalar_one_or_none(): raise HTTPException(400, "Printer not found") + # Serialize ams_mapping to JSON for TEXT column storage + if "ams_mapping" in update_data: + update_data["ams_mapping"] = json.dumps(update_data["ams_mapping"]) if update_data["ams_mapping"] else None + for field, value in update_data.items(): setattr(item, field, value) diff --git a/backend/app/schemas/print_queue.py b/backend/app/schemas/print_queue.py index b902bbdce..c9e7a2e23 100644 --- a/backend/app/schemas/print_queue.py +++ b/backend/app/schemas/print_queue.py @@ -34,6 +34,7 @@ class PrintQueueItemUpdate(BaseModel): require_previous_success: bool | None = None auto_off_after: bool | None = None manual_start: bool | None = None + ams_mapping: list[int] | None = None class PrintQueueItemResponse(BaseModel): diff --git a/frontend/public/sw.js b/frontend/public/sw.js index fb5ca0434..8566e9730 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,6 +1,6 @@ // Bambuddy Service Worker -const CACHE_NAME = 'bambuddy-v21'; -const STATIC_CACHE = 'bambuddy-static-v21'; +const CACHE_NAME = 'bambuddy-v22'; +const STATIC_CACHE = 'bambuddy-static-v22'; // Static assets to cache on install const STATIC_ASSETS = [ diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 984897ea6..742bdd5d8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -825,6 +825,7 @@ export interface PrintQueueItemUpdate { require_previous_success?: boolean; auto_off_after?: boolean; manual_start?: boolean; + ams_mapping?: number[]; } // MQTT Logging types diff --git a/frontend/src/components/AddToQueueModal.tsx b/frontend/src/components/AddToQueueModal.tsx index 57db2782c..1b0357a51 100644 --- a/frontend/src/components/AddToQueueModal.tsx +++ b/frontend/src/components/AddToQueueModal.tsx @@ -6,111 +6,7 @@ import type { PrintQueueItemCreate } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { useToast } from '../contexts/ToastContext'; - -// Bambu Lab filament hex color to name mapping (from bambu-color-names.csv) -const BAMBU_HEX_COLORS: Record = { - '000000': 'Black', '001489': 'Blue', '002e96': 'Blue', '0047bb': 'Blue', '00482b': 'Pine Green', - '004ea8': 'Blue', '0056b8': 'Cobalt Blue', '0069b1': 'Lake Blue', '0072ce': 'Blue', '0078bf': 'Marine Blue', - '0085ad': 'Light Blue', '0086d6': 'Cyan', '008bda': 'Blue', '009639': 'Green', '009bd8': 'Cyan', - '009fa1': 'Teal', '00a6a0': 'Green', '00ae42': 'Bambu Green', '00b1b7': 'Turquoise', '00bb31': 'Green', - '018814': 'Candy Green', '042f56': 'Dark Blue', '0a2989': 'Blue', '0a2ca5': 'Blue', '0c2340': 'Navy Blue', - '0c3b95': 'Blue', '101820': 'Black', '147bd1': 'Blue', '164b35': 'Green', '16b08e': 'Malachite Green', - '1d7c6a': 'Oxide Green Metallic', '1f79e5': 'Lake Blue', '2140b4': 'Blue', '25282a': 'Black', '2842ad': 'Royal Blue', - '2d2b28': 'Onyx Black Sparkle', '324585': 'Indigo Blue', '353533': 'Gray', '39541a': 'Forest Green', - '39699e': 'Cobalt Blue Metallic', '3b665e': 'Green', '3f5443': 'Alpine Green Sparkle', '3f8e43': 'Mistletoe Green', - '424379': 'Nebulae', '43403d': 'Iron Gray Metallic', '482960': 'Indigo Purple', '483d8b': 'Royal Purple Sparkle', - '489fdf': 'Azure', '4c241c': 'Rosewood', '4ce4a0': 'Green', '4d3324': 'Dark Chocolate', '4d5054': 'Lava Gray', - '4dafda': 'Cyan', '4f3f24': 'Black Walnut', '515151': 'Dark Gray', '515a6c': 'Gray', '545454': 'Dark Gray', - '565656': 'Titan Gray', '56b7e6': 'Sky Blue', '583061': 'Violet Purple', '5898dd': 'Blue', '594177': 'Purple', - '5b492f': 'Brown', '5b6579': 'Blue Gray', '5c9748': 'Matcha Green', '5e43b7': 'Purple', '5e4b3c': 'Copper', - '5f6367': 'Titan Gray', '61b0ff': 'Translucent Light Blue', '61bf36': 'Green', '61c680': 'Grass Green', - '6667ab': 'Lavender Blue', '684a43': 'Brown', '686865': 'Black', '68724d': 'Dark Green', '688197': 'Blue Gray', - '69398e': 'Iris Purple', '6e88bc': 'Jeans Blue', '6ee53c': 'Lime Green', '6f5034': 'Cocoa Brown', '7248bd': 'Lavender', - '748c45': 'Translucent Olive', '757575': 'Nardo Gray', '75aed8': 'Blue', '77edd7': 'Translucent Teal', '789d4a': 'Olive', - '792b36': 'Crimson Red Sparkle', '7ac0e9': 'Glow Blue', '7ae1bf': 'Mint', '7cd82b': 'Lime Green', '7d6556': 'Dark Brown', - '8344b0': 'Purple', '847d48': 'Bronze', '854ce4': 'Purple', '8671cb': 'Purple', '875718': 'Peanut Brown', - '87909a': 'Silver', '898d8d': 'Gray', '8a949e': 'Gray', '8e8e8e': 'Translucent Gray', '8e9089': 'Gray', - '90ff1a': 'Neon Green', '918669': 'Classic Birch', '939393': 'Gray', '950051': 'Plum', '951e23': 'Burgundy Red', - '959698': 'Silver', '96d8af': 'Light Jade', '96dcb9': 'Mint', '995f11': 'Clay Brown', '999d9d': 'Gray', - '9b9ea0': 'Ash Gray', '9d2235': 'Maroon Red', '9d432c': 'Brown', '9e007e': 'Purple', '9ea2a2': 'Gray', - '9f332a': 'Brick Red', 'a1ffac': 'Glow Green', 'a3d8e1': 'Ice Blue', 'a6a9aa': 'Silver', 'a8a8aa': 'Gray', - 'a8c6ee': 'Baby Blue', 'aa6443': 'Copper Brown Metallic', 'ad4e38': 'Red Granite', 'adb1b2': 'Gray', - 'ae835b': 'Caramel', 'ae96d4': 'Lilac Purple', 'af1685': 'Purple', 'afb1ae': 'Gray', 'b15533': 'Terracotta', - 'b28b33': 'Gold', 'b39b84': 'Iridium Gold Metallic', 'b50011': 'Red', 'b8acd6': 'Lavender', 'b8cde9': 'Ice Blue', - 'ba9594': 'Rose Gold', 'bb3d43': 'Dark Red', 'bc0900': 'Red', 'becf00': 'Bright Green', 'c0df16': 'Green', - 'c12e1f': 'Red', 'c2e189': 'Apple Green', 'c3e2d6': 'Light Cyan', 'c5ed48': 'Lime', 'c6001a': 'Red', - 'c6c6c6': 'Gray', 'c8102e': 'Red', 'c8c8c8': 'Silver', 'c98935': 'Ochre Yellow', 'c9a381': 'Translucent Brown', - 'cbc6b8': 'Bone White', 'cdceca': 'Gray', 'cea629': 'Classic Gold Sparkle', 'd02727': 'Candy Red', - 'd1d3d5': 'Light Gray', 'd32941': 'Red', 'd3b7a7': 'Latte Brown', 'd6001c': 'Red', 'd6abff': 'Translucent Purple', - 'd6cca3': 'White Oak', 'dc3a27': 'Orange', 'dd3c22': 'Vermilion Red', 'de4343': 'Scarlet Red', 'dfd1a7': 'Beige', - 'e02928': 'Red', 'e4bd68': 'Gold', 'e5b03d': 'Gold', 'e83100': 'Red', 'e8afcf': 'Sakura Pink', 'e8dbb7': 'Desert Tan', - 'eaeae4': 'White', 'eaeceb': 'Silver', 'ec008c': 'Magenta', 'ed0000': 'Red', 'eeb1c1': 'Pink', 'efe255': 'Yellow', - 'f0f1a8': 'Clear', 'f17b8f': 'Glow Pink', 'f3cfb2': 'Champagne', 'f3e600': 'Yellow', 'f48438': 'Orange', - 'f4a925': 'Gold', 'f4d53f': 'Yellow', 'f4ee2a': 'Yellow', 'f5547c': 'Hot Pink', 'f55a74': 'Pink', - 'f5b6cd': 'Cherry Pink', 'f5dbab': 'Mellow Yellow', 'f5f1dd': 'White', 'f68b1b': 'Neon Orange', 'f74e02': 'Orange', - 'f75403': 'Orange', 'f7ada6': 'Pink', 'f7d959': 'Lemon Yellow', 'f7e6de': 'Beige', 'f7f3f0': 'White Marble', - 'f8ff80': 'Glow Yellow', 'f99963': 'Mandarin Orange', 'f9c1bd': 'Translucent Pink', 'f9dfb9': 'Cream', - 'f9ef41': 'Yellow', 'f9f7f2': 'Nature', 'f9f7f4': 'White', 'fce300': 'Yellow', 'fce900': 'Yellow', - 'fec600': 'Sunflower Yellow', 'fedb00': 'Yellow', 'ff4800': 'Orange', 'ff671f': 'Orange', 'ff6a13': 'Orange', - 'ff7f41': 'Orange', 'ff9016': 'Pumpkin Orange', 'ff911a': 'Translucent Orange', 'ff9d5b': 'Glow Orange', - 'ffb549': 'Sunflower Yellow', 'ffc72c': 'Tangerine Yellow', 'ffce00': 'Yellow', 'ffd00b': 'Yellow', - 'ffe133': 'Yellow', 'fffaf2': 'White', 'ffffff': 'White', -}; - -// Get color name from hex color (lookup Bambu database, then fallback to HSL-based name) -function getColorName(hexColor: string): string { - // Normalize hex: lowercase, strip # and alpha channel - const hex = hexColor.replace('#', '').toLowerCase().substring(0, 6); - // Try Bambu color lookup - if (BAMBU_HEX_COLORS[hex]) { - return BAMBU_HEX_COLORS[hex]; - } - // Fall back to HSL-based name - return hexToColorName(hexColor); -} - -// Convert hex color to basic color name using HSL -function hexToColorName(hex: string | null | undefined): string { - if (!hex || hex.length < 6) return 'Unknown'; - const cleanHex = hex.replace('#', ''); - const r = parseInt(cleanHex.substring(0, 2), 16); - const g = parseInt(cleanHex.substring(2, 4), 16); - const b = parseInt(cleanHex.substring(4, 6), 16); - - const max = Math.max(r, g, b) / 255; - const min = Math.min(r, g, b) / 255; - const l = (max + min) / 2; - - let h = 0; - let s = 0; - - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255; - if (max === rNorm) h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6; - else if (max === gNorm) h = ((bNorm - rNorm) / d + 2) / 6; - else h = ((rNorm - gNorm) / d + 4) / 6; - } - h = h * 360; - - if (l < 0.15) return 'Black'; - if (l > 0.85) return 'White'; - if (s < 0.15) { - if (l < 0.4) return 'Dark Gray'; - if (l > 0.6) return 'Light Gray'; - return 'Gray'; - } - if (h < 15 || h >= 345) return 'Red'; - if (h < 45) return 'Orange'; - if (h < 70) return 'Yellow'; - if (h < 150) return 'Green'; - if (h < 200) return 'Cyan'; - if (h < 260) return 'Blue'; - if (h < 290) return 'Purple'; - if (h < 345) return 'Pink'; - return 'Unknown'; -} +import { getColorName } from '../utils/colors'; interface AddToQueueModalProps { archiveId: number; diff --git a/frontend/src/components/EditQueueItemModal.tsx b/frontend/src/components/EditQueueItemModal.tsx index f6e7d26e4..c1eff404e 100644 --- a/frontend/src/components/EditQueueItemModal.tsx +++ b/frontend/src/components/EditQueueItemModal.tsx @@ -1,11 +1,12 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Calendar, Clock, X, AlertCircle, Power, Pencil, Hand } from 'lucide-react'; +import { Calendar, Clock, X, AlertCircle, Power, Pencil, Hand, Check, AlertTriangle, Circle, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react'; import { api } from '../api/client'; import type { PrintQueueItem, PrintQueueItemUpdate } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { useToast } from '../contexts/ToastContext'; +import { getColorName } from '../utils/colors'; interface EditQueueItemModalProps { item: PrintQueueItem; @@ -37,12 +38,49 @@ export function EditQueueItemModal({ item, onClose }: EditQueueItemModalProps) { }); const [requirePreviousSuccess, setRequirePreviousSuccess] = useState(item.require_previous_success); const [autoOffAfter, setAutoOffAfter] = useState(item.auto_off_after); + const [showFilamentMapping, setShowFilamentMapping] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + // Manual slot overrides: slot_id (1-indexed) -> globalTrayId + // Initialize from existing ams_mapping if present + const [manualMappings, setManualMappings] = useState>(() => { + if (item.ams_mapping && Array.isArray(item.ams_mapping)) { + const mappings: Record = {}; + item.ams_mapping.forEach((globalTrayId, idx) => { + if (globalTrayId !== -1) { + mappings[idx + 1] = globalTrayId; + } + }); + return mappings; + } + return {}; + }); const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: () => api.getPrinters(), }); + // Fetch filament requirements from the archived 3MF + const { data: filamentReqs } = useQuery({ + queryKey: ['archive-filaments', item.archive_id], + queryFn: () => api.getArchiveFilamentRequirements(item.archive_id), + }); + + // Fetch printer status when a printer is selected + const { data: printerStatus } = useQuery({ + queryKey: ['printer-status', printerId], + queryFn: () => api.getPrinterStatus(printerId), + enabled: !!printerId, + }); + + // Clear manual mappings when printer changes (but not on initial load) + const [initialPrinterId] = useState(item.printer_id); + useEffect(() => { + if (printerId !== initialPrinterId) { + setManualMappings({}); + } + }, [printerId, initialPrinterId]); + // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -52,6 +90,207 @@ export function EditQueueItemModal({ item, onClose }: EditQueueItemModalProps) { return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); + // Helper to normalize color format (API returns "RRGGBBAA", 3MF uses "#RRGGBB") + const normalizeColor = (color: string | null | undefined): string => { + if (!color) return '#808080'; + const hex = color.replace('#', '').substring(0, 6); + return `#${hex}`; + }; + + // Helper to format slot label for display + const formatSlotLabel = (amsId: number, trayId: number, isHt: boolean, isExternal: boolean): string => { + if (isExternal) return 'External'; + const letter = String.fromCharCode(65 + (amsId >= 128 ? amsId - 128 : amsId)); + if (isHt) return `HT-${letter}`; + return `AMS-${letter} Slot ${trayId + 1}`; + }; + + // Calculate global tray ID for MQTT command + 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 + const loadedFilaments = useMemo(() => { + const filaments: Array<{ + type: string; + color: string; + colorName: string; + amsId: number; + trayId: number; + isHt: boolean; + isExternal: boolean; + label: string; + globalTrayId: number; + }> = []; + + printerStatus?.ams?.forEach((amsUnit) => { + const isHt = amsUnit.tray.length === 1; + amsUnit.tray.forEach((tray) => { + if (tray.tray_type) { + const color = normalizeColor(tray.tray_color); + filaments.push({ + type: tray.tray_type, + color, + colorName: getColorName(color), + amsId: amsUnit.id, + trayId: tray.id, + isHt, + isExternal: false, + label: formatSlotLabel(amsUnit.id, tray.id, isHt, false), + globalTrayId: getGlobalTrayId(amsUnit.id, tray.id, false), + }); + } + }); + }); + + if (printerStatus?.vt_tray?.tray_type) { + const color = normalizeColor(printerStatus.vt_tray.tray_color); + filaments.push({ + type: printerStatus.vt_tray.tray_type, + color, + colorName: getColorName(color), + amsId: -1, + trayId: 0, + isHt: false, + isExternal: true, + label: 'External', + globalTrayId: 254, + }); + } + + return filaments; + }, [printerStatus]); + + // Compare required filaments with loaded filaments + const filamentComparison = useMemo(() => { + if (!filamentReqs?.filaments || filamentReqs.filaments.length === 0) return []; + + const normalizeColorForCompare = (color: string | undefined): string => { + if (!color) return ''; + return color.replace('#', '').toLowerCase().substring(0, 6); + }; + + const colorsAreSimilar = (color1: string | undefined, color2: string | undefined, threshold = 40): boolean => { + const hex1 = normalizeColorForCompare(color1); + const hex2 = normalizeColorForCompare(color2); + if (!hex1 || !hex2 || hex1.length < 6 || hex2.length < 6) return false; + + const r1 = parseInt(hex1.substring(0, 2), 16); + const g1 = parseInt(hex1.substring(2, 4), 16); + const b1 = parseInt(hex1.substring(4, 6), 16); + const r2 = parseInt(hex2.substring(0, 2), 16); + const g2 = parseInt(hex2.substring(2, 4), 16); + const b2 = parseInt(hex2.substring(4, 6), 16); + + return Math.abs(r1 - r2) <= threshold && + Math.abs(g1 - g2) <= threshold && + Math.abs(b1 - b2) <= threshold; + }; + + const usedTrayIds = new Set(Object.values(manualMappings)); + + return filamentReqs.filaments.map((req) => { + const slotId = req.slot_id || 0; + + // Check if there's a manual override for this slot + if (slotId > 0 && manualMappings[slotId] !== undefined) { + const manualTrayId = manualMappings[slotId]; + const manualLoaded = loadedFilaments.find((f) => f.globalTrayId === manualTrayId); + + if (manualLoaded) { + const typeMatch = manualLoaded.type?.toUpperCase() === req.type?.toUpperCase(); + const colorMatch = normalizeColorForCompare(manualLoaded.color) === normalizeColorForCompare(req.color) || + colorsAreSimilar(manualLoaded.color, req.color); + + let status: 'match' | 'type_only' | 'mismatch' | 'empty'; + if (typeMatch && colorMatch) { + status = 'match'; + } else if (typeMatch) { + status = 'type_only'; + } else { + status = 'mismatch'; + } + + return { + ...req, + loaded: manualLoaded, + hasFilament: true, + typeMatch, + colorMatch, + status, + isManual: true, + }; + } + } + + // Auto-match + const exactMatch = loadedFilaments.find( + (f) => !usedTrayIds.has(f.globalTrayId) && + f.type?.toUpperCase() === req.type?.toUpperCase() && + normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color) + ); + const similarMatch = !exactMatch && loadedFilaments.find( + (f) => !usedTrayIds.has(f.globalTrayId) && + f.type?.toUpperCase() === req.type?.toUpperCase() && + colorsAreSimilar(f.color, req.color) + ); + const typeOnlyMatch = !exactMatch && !similarMatch && loadedFilaments.find( + (f) => !usedTrayIds.has(f.globalTrayId) && + f.type?.toUpperCase() === req.type?.toUpperCase() + ); + const loaded = exactMatch || similarMatch || typeOnlyMatch || undefined; + + if (loaded) { + usedTrayIds.add(loaded.globalTrayId); + } + + const hasFilament = !!loaded; + const typeMatch = hasFilament; + const colorMatch = !!exactMatch || !!similarMatch; + + let status: 'match' | 'type_only' | 'mismatch' | 'empty'; + if (exactMatch || similarMatch) { + status = 'match'; + } else if (typeOnlyMatch) { + status = 'type_only'; + } else { + status = 'mismatch'; + } + + return { + ...req, + loaded, + hasFilament, + typeMatch, + colorMatch, + status, + isManual: false, + }; + }); + }, [filamentReqs, loadedFilaments, manualMappings]); + + // Build AMS mapping array + const amsMapping = useMemo(() => { + if (filamentComparison.length === 0) return undefined; + + const maxSlotId = Math.max(...filamentComparison.map((f) => f.slot_id || 0)); + if (maxSlotId <= 0) return undefined; + + const mapping = new Array(maxSlotId).fill(-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 hasFilamentReqs = filamentReqs?.filaments && filamentReqs.filaments.length > 0; + const updateMutation = useMutation({ mutationFn: (data: PrintQueueItemUpdate) => api.updateQueueItem(item.id, data), onSuccess: () => { @@ -72,6 +311,7 @@ export function EditQueueItemModal({ item, onClose }: EditQueueItemModalProps) { require_previous_success: requirePreviousSuccess, auto_off_after: autoOffAfter, manual_start: scheduleType === 'manual', + ams_mapping: amsMapping, }; if (scheduleType === 'scheduled' && scheduledTime) { @@ -95,7 +335,7 @@ export function EditQueueItemModal({ item, onClose }: EditQueueItemModalProps) { className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose} > - e.stopPropagation()}> + e.stopPropagation()}> {/* Header */}
@@ -143,6 +383,123 @@ export function EditQueueItemModal({ item, onClose }: EditQueueItemModalProps) { )}
+ {/* Filament Mapping Section */} + {printerId && hasFilamentReqs && ( +
+ + + {showFilamentMapping && ( +
+
+ Click to change slot assignment + +
+ {filamentComparison.map((item, idx) => ( +
+ + + + + {item.type} ({item.used_grams}g) + + → + + {item.status === 'match' ? ( + + ) : item.status === 'type_only' ? ( + + + + ) : ( + + + + )} +
+ ))} +
+ )} +
+ )} + {/* Schedule type */}
diff --git a/frontend/src/components/ReprintModal.tsx b/frontend/src/components/ReprintModal.tsx index 88343f4c0..27218688a 100644 --- a/frontend/src/components/ReprintModal.tsx +++ b/frontend/src/components/ReprintModal.tsx @@ -4,6 +4,7 @@ import { X, Printer, Loader2, AlertTriangle, Check, Circle, RefreshCw, ChevronDo import { api } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; +import { getColorName } from '../utils/colors'; interface ReprintModalProps { archiveId: number; @@ -29,111 +30,6 @@ const DEFAULT_PRINT_OPTIONS: PrintOptions = { timelapse: false, }; -// Bambu Lab filament hex color to name mapping (from bambu-color-names.csv) -const BAMBU_HEX_COLORS: Record = { - '000000': 'Black', '001489': 'Blue', '002e96': 'Blue', '0047bb': 'Blue', '00482b': 'Pine Green', - '004ea8': 'Blue', '0056b8': 'Cobalt Blue', '0069b1': 'Lake Blue', '0072ce': 'Blue', '0078bf': 'Marine Blue', - '0085ad': 'Light Blue', '0086d6': 'Cyan', '008bda': 'Blue', '009639': 'Green', '009bd8': 'Cyan', - '009fa1': 'Teal', '00a6a0': 'Green', '00ae42': 'Bambu Green', '00b1b7': 'Turquoise', '00bb31': 'Green', - '018814': 'Candy Green', '042f56': 'Dark Blue', '0a2989': 'Blue', '0a2ca5': 'Blue', '0c2340': 'Navy Blue', - '0c3b95': 'Blue', '101820': 'Black', '147bd1': 'Blue', '164b35': 'Green', '16b08e': 'Malachite Green', - '1d7c6a': 'Oxide Green Metallic', '1f79e5': 'Lake Blue', '2140b4': 'Blue', '25282a': 'Black', '2842ad': 'Royal Blue', - '2d2b28': 'Onyx Black Sparkle', '324585': 'Indigo Blue', '353533': 'Gray', '39541a': 'Forest Green', - '39699e': 'Cobalt Blue Metallic', '3b665e': 'Green', '3f5443': 'Alpine Green Sparkle', '3f8e43': 'Mistletoe Green', - '424379': 'Nebulae', '43403d': 'Iron Gray Metallic', '482960': 'Indigo Purple', '483d8b': 'Royal Purple Sparkle', - '489fdf': 'Azure', '4c241c': 'Rosewood', '4ce4a0': 'Green', '4d3324': 'Dark Chocolate', '4d5054': 'Lava Gray', - '4dafda': 'Cyan', '4f3f24': 'Black Walnut', '515151': 'Dark Gray', '515a6c': 'Gray', '545454': 'Dark Gray', - '565656': 'Titan Gray', '56b7e6': 'Sky Blue', '583061': 'Violet Purple', '5898dd': 'Blue', '594177': 'Purple', - '5b492f': 'Brown', '5b6579': 'Blue Gray', '5c9748': 'Matcha Green', '5e43b7': 'Purple', '5e4b3c': 'Copper', - '5f6367': 'Titan Gray', '61b0ff': 'Translucent Light Blue', '61bf36': 'Green', '61c680': 'Grass Green', - '6667ab': 'Lavender Blue', '684a43': 'Brown', '686865': 'Black', '68724d': 'Dark Green', '688197': 'Blue Gray', - '69398e': 'Iris Purple', '6e88bc': 'Jeans Blue', '6ee53c': 'Lime Green', '6f5034': 'Cocoa Brown', '7248bd': 'Lavender', - '748c45': 'Translucent Olive', '757575': 'Nardo Gray', '75aed8': 'Blue', '77edd7': 'Translucent Teal', '789d4a': 'Olive', - '792b36': 'Crimson Red Sparkle', '7ac0e9': 'Glow Blue', '7ae1bf': 'Mint', '7cd82b': 'Lime Green', '7d6556': 'Dark Brown', - '8344b0': 'Purple', '847d48': 'Bronze', '854ce4': 'Purple', '8671cb': 'Purple', '875718': 'Peanut Brown', - '87909a': 'Silver', '898d8d': 'Gray', '8a949e': 'Gray', '8e8e8e': 'Translucent Gray', '8e9089': 'Gray', - '90ff1a': 'Neon Green', '918669': 'Classic Birch', '939393': 'Gray', '950051': 'Plum', '951e23': 'Burgundy Red', - '959698': 'Silver', '96d8af': 'Light Jade', '96dcb9': 'Mint', '995f11': 'Clay Brown', '999d9d': 'Gray', - '9b9ea0': 'Ash Gray', '9d2235': 'Maroon Red', '9d432c': 'Brown', '9e007e': 'Purple', '9ea2a2': 'Gray', - '9f332a': 'Brick Red', 'a1ffac': 'Glow Green', 'a3d8e1': 'Ice Blue', 'a6a9aa': 'Silver', 'a8a8aa': 'Gray', - 'a8c6ee': 'Baby Blue', 'aa6443': 'Copper Brown Metallic', 'ad4e38': 'Red Granite', 'adb1b2': 'Gray', - 'ae835b': 'Caramel', 'ae96d4': 'Lilac Purple', 'af1685': 'Purple', 'afb1ae': 'Gray', 'b15533': 'Terracotta', - 'b28b33': 'Gold', 'b39b84': 'Iridium Gold Metallic', 'b50011': 'Red', 'b8acd6': 'Lavender', 'b8cde9': 'Ice Blue', - 'ba9594': 'Rose Gold', 'bb3d43': 'Dark Red', 'bc0900': 'Red', 'becf00': 'Bright Green', 'c0df16': 'Green', - 'c12e1f': 'Red', 'c2e189': 'Apple Green', 'c3e2d6': 'Light Cyan', 'c5ed48': 'Lime', 'c6001a': 'Red', - 'c6c6c6': 'Gray', 'c8102e': 'Red', 'c8c8c8': 'Silver', 'c98935': 'Ochre Yellow', 'c9a381': 'Translucent Brown', - 'cbc6b8': 'Bone White', 'cdceca': 'Gray', 'cea629': 'Classic Gold Sparkle', 'd02727': 'Candy Red', - 'd1d3d5': 'Light Gray', 'd32941': 'Red', 'd3b7a7': 'Latte Brown', 'd6001c': 'Red', 'd6abff': 'Translucent Purple', - 'd6cca3': 'White Oak', 'dc3a27': 'Orange', 'dd3c22': 'Vermilion Red', 'de4343': 'Scarlet Red', 'dfd1a7': 'Beige', - 'e02928': 'Red', 'e4bd68': 'Gold', 'e5b03d': 'Gold', 'e83100': 'Red', 'e8afcf': 'Sakura Pink', 'e8dbb7': 'Desert Tan', - 'eaeae4': 'White', 'eaeceb': 'Silver', 'ec008c': 'Magenta', 'ed0000': 'Red', 'eeb1c1': 'Pink', 'efe255': 'Yellow', - 'f0f1a8': 'Clear', 'f17b8f': 'Glow Pink', 'f3cfb2': 'Champagne', 'f3e600': 'Yellow', 'f48438': 'Orange', - 'f4a925': 'Gold', 'f4d53f': 'Yellow', 'f4ee2a': 'Yellow', 'f5547c': 'Hot Pink', 'f55a74': 'Pink', - 'f5b6cd': 'Cherry Pink', 'f5dbab': 'Mellow Yellow', 'f5f1dd': 'White', 'f68b1b': 'Neon Orange', 'f74e02': 'Orange', - 'f75403': 'Orange', 'f7ada6': 'Pink', 'f7d959': 'Lemon Yellow', 'f7e6de': 'Beige', 'f7f3f0': 'White Marble', - 'f8ff80': 'Glow Yellow', 'f99963': 'Mandarin Orange', 'f9c1bd': 'Translucent Pink', 'f9dfb9': 'Cream', - 'f9ef41': 'Yellow', 'f9f7f2': 'Nature', 'f9f7f4': 'White', 'fce300': 'Yellow', 'fce900': 'Yellow', - 'fec600': 'Sunflower Yellow', 'fedb00': 'Yellow', 'ff4800': 'Orange', 'ff671f': 'Orange', 'ff6a13': 'Orange', - 'ff7f41': 'Orange', 'ff9016': 'Pumpkin Orange', 'ff911a': 'Translucent Orange', 'ff9d5b': 'Glow Orange', - 'ffb549': 'Sunflower Yellow', 'ffc72c': 'Tangerine Yellow', 'ffce00': 'Yellow', 'ffd00b': 'Yellow', - 'ffe133': 'Yellow', 'fffaf2': 'White', 'ffffff': 'White', -}; - -// Get color name from hex color (lookup Bambu database, then fallback to HSL-based name) -function getColorName(hexColor: string): string { - // Normalize hex: lowercase, strip # and alpha channel - const hex = hexColor.replace('#', '').toLowerCase().substring(0, 6); - // Try Bambu color lookup - if (BAMBU_HEX_COLORS[hex]) { - return BAMBU_HEX_COLORS[hex]; - } - // Fall back to HSL-based name - return hexToColorName(hexColor); -} - -// Convert hex color to basic color name using HSL -function hexToColorName(hex: string | null | undefined): string { - if (!hex || hex.length < 6) return 'Unknown'; - const cleanHex = hex.replace('#', ''); - const r = parseInt(cleanHex.substring(0, 2), 16); - const g = parseInt(cleanHex.substring(2, 4), 16); - const b = parseInt(cleanHex.substring(4, 6), 16); - - const max = Math.max(r, g, b) / 255; - const min = Math.min(r, g, b) / 255; - const l = (max + min) / 2; - - let h = 0; - let s = 0; - - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255; - if (max === rNorm) h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6; - else if (max === gNorm) h = ((bNorm - rNorm) / d + 2) / 6; - else h = ((rNorm - gNorm) / d + 4) / 6; - } - h = h * 360; - - if (l < 0.15) return 'Black'; - if (l > 0.85) return 'White'; - if (s < 0.15) { - if (l < 0.4) return 'Dark Gray'; - if (l > 0.6) return 'Light Gray'; - return 'Gray'; - } - if (h < 15 || h >= 345) return 'Red'; - if (h < 45) return 'Orange'; - if (h < 70) return 'Yellow'; - if (h < 150) return 'Green'; - if (h < 200) return 'Cyan'; - if (h < 260) return 'Blue'; - if (h < 290) return 'Purple'; - if (h < 345) return 'Pink'; - return 'Unknown'; -} - export function ReprintModal({ archiveId, archiveName, onClose, onSuccess }: ReprintModalProps) { const queryClient = useQueryClient(); const [selectedPrinter, setSelectedPrinter] = useState(null); diff --git a/frontend/src/pages/ArchivesPage.tsx b/frontend/src/pages/ArchivesPage.tsx index 240f65a1b..b02622728 100644 --- a/frontend/src/pages/ArchivesPage.tsx +++ b/frontend/src/pages/ArchivesPage.tsx @@ -687,7 +687,7 @@ function ArchiveCard({ onClick={() => setShowReprint(true)} > - Print + Reprint