| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import { useMemo } from 'react';
- import { useQueries } from '@tanstack/react-query';
- import { api } from '../api/client';
- import type { PrinterStatus, Printer } from '../api/client';
- import {
- buildLoadedFilaments,
- computeAmsMapping,
- type LoadedFilament,
- type FilamentRequirement,
- } from './useFilamentMapping';
- import {
- normalizeColorForCompare,
- colorsAreSimilar,
- } from '../utils/amsHelpers';
- /**
- * Match status for a single printer's filament configuration.
- */
- export type PrinterMatchStatus = 'full' | 'partial' | 'missing';
- /**
- * Per-printer configuration for AMS mapping.
- */
- export interface PerPrinterConfig {
- /** Whether this printer uses the default mapping or has custom config */
- useDefault: boolean;
- /** Manual slot overrides for this printer (slot_id -> globalTrayId) */
- manualMappings: Record<number, number>;
- /** Whether this mapping was auto-configured */
- autoConfigured: boolean;
- }
- /**
- * Result of filament mapping for a single printer.
- */
- export interface PrinterMappingResult {
- printerId: number;
- printerName: string;
- /** Printer status data */
- status: PrinterStatus | undefined;
- /** Whether status is still loading */
- isLoading: boolean;
- /** List of loaded filaments in this printer */
- loadedFilaments: LoadedFilament[];
- /** Auto-computed AMS mapping for this printer */
- autoMapping: number[] | undefined;
- /** Final AMS mapping (considering manual overrides) */
- finalMapping: number[] | undefined;
- /** Match status: full (all exact), partial (some mismatches), missing (type not found) */
- matchStatus: PrinterMatchStatus;
- /** Number of slots with exact match (type + color) */
- exactMatches: number;
- /** Number of slots with type-only match */
- typeOnlyMatches: number;
- /** Number of slots with missing type */
- missingTypes: number;
- /** Total required slots */
- totalSlots: number;
- /** Per-printer config */
- config: PerPrinterConfig;
- }
- /**
- * Result of the useMultiPrinterFilamentMapping hook.
- */
- export interface UseMultiPrinterFilamentMappingResult {
- /** Results for each selected printer */
- printerResults: PrinterMappingResult[];
- /** Whether any printer data is still loading */
- isLoading: boolean;
- /** Per-printer configurations */
- perPrinterConfigs: Record<number, PerPrinterConfig>;
- /** Update config for a specific printer */
- updatePrinterConfig: (printerId: number, config: Partial<PerPrinterConfig>) => void;
- /** Auto-configure all printers based on their loaded filaments */
- autoConfigureAll: () => void;
- /** Auto-configure a specific printer */
- autoConfigurePrinter: (printerId: number) => void;
- /** Get final mapping for a specific printer (for submission) */
- getFinalMapping: (printerId: number) => number[] | undefined;
- /** Check if all printers have acceptable mappings */
- allPrintersReady: boolean;
- }
- /**
- * Compute match details for a printer given filament requirements and loaded filaments.
- */
- function computeMatchDetails(
- filamentReqs: FilamentRequirement[] | undefined,
- loadedFilaments: LoadedFilament[],
- manualMappings: Record<number, number>
- ): { exactMatches: number; typeOnlyMatches: number; missingTypes: number; totalSlots: number; status: PrinterMatchStatus } {
- if (!filamentReqs || filamentReqs.length === 0) {
- return { exactMatches: 0, typeOnlyMatches: 0, missingTypes: 0, totalSlots: 0, status: 'full' };
- }
- let exactMatches = 0;
- let typeOnlyMatches = 0;
- let missingTypes = 0;
- const usedTrayIds = new Set<number>(Object.values(manualMappings));
- for (const req of filamentReqs) {
- const slotId = req.slot_id || 0;
- // Check manual override first
- 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);
- if (typeMatch && colorMatch) {
- exactMatches++;
- } else if (typeMatch) {
- typeOnlyMatches++;
- } else {
- missingTypes++;
- }
- continue;
- }
- }
- // 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
- ? undefined
- : loadedFilaments.find(
- (f) =>
- !usedTrayIds.has(f.globalTrayId) &&
- f.type?.toUpperCase() === req.type?.toUpperCase() &&
- colorsAreSimilar(f.color, req.color)
- );
- const typeOnlyMatch =
- exactMatch || similarMatch
- ? undefined
- : loadedFilaments.find(
- (f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
- );
- const loaded = exactMatch ?? similarMatch ?? typeOnlyMatch;
- if (loaded) {
- usedTrayIds.add(loaded.globalTrayId);
- }
- if (exactMatch || similarMatch) {
- exactMatches++;
- } else if (typeOnlyMatch) {
- typeOnlyMatches++;
- } else {
- missingTypes++;
- }
- }
- const totalSlots = filamentReqs.length;
- let status: PrinterMatchStatus = 'full';
- if (missingTypes > 0) {
- status = 'missing';
- } else if (typeOnlyMatches > 0) {
- status = 'partial';
- }
- return { exactMatches, typeOnlyMatches, missingTypes, totalSlots, status };
- }
- /**
- * Compute AMS mapping with manual overrides applied.
- */
- function computeMappingWithOverrides(
- filamentReqs: { filaments: FilamentRequirement[] } | undefined,
- printerStatus: PrinterStatus | undefined,
- manualMappings: Record<number, number>
- ): number[] | undefined {
- if (!filamentReqs?.filaments || filamentReqs.filaments.length === 0) return undefined;
- const loadedFilaments = buildLoadedFilaments(printerStatus);
- if (loadedFilaments.length === 0) return undefined;
- const usedTrayIds = new Set<number>(Object.values(manualMappings));
- const comparisons: { slot_id: number; globalTrayId: number }[] = [];
- for (const req of filamentReqs.filaments) {
- const slotId = req.slot_id || 0;
- // Check manual override first
- if (slotId > 0 && manualMappings[slotId] !== undefined) {
- comparisons.push({ slot_id: slotId, globalTrayId: manualMappings[slotId] });
- continue;
- }
- // 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
- ? undefined
- : loadedFilaments.find(
- (f) =>
- !usedTrayIds.has(f.globalTrayId) &&
- f.type?.toUpperCase() === req.type?.toUpperCase() &&
- colorsAreSimilar(f.color, req.color)
- );
- const typeOnlyMatch =
- exactMatch || similarMatch
- ? undefined
- : loadedFilaments.find(
- (f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
- );
- const loaded = exactMatch ?? similarMatch ?? typeOnlyMatch;
- if (loaded) {
- usedTrayIds.add(loaded.globalTrayId);
- }
- comparisons.push({ slot_id: slotId, globalTrayId: loaded?.globalTrayId ?? -1 });
- }
- const maxSlotId = Math.max(...comparisons.map((f) => f.slot_id || 0));
- if (maxSlotId <= 0) return undefined;
- const mapping = new Array(maxSlotId).fill(-1);
- comparisons.forEach((f) => {
- if (f.slot_id && f.slot_id > 0) {
- mapping[f.slot_id - 1] = f.globalTrayId;
- }
- });
- return mapping;
- }
- /**
- * Default per-printer config (use default mapping).
- */
- const DEFAULT_PRINTER_CONFIG: PerPrinterConfig = {
- useDefault: true,
- manualMappings: {},
- autoConfigured: false,
- };
- /**
- * Hook to manage filament mapping for multiple printers.
- * Fetches printer status for all selected printers and computes per-printer mappings.
- */
- export function useMultiPrinterFilamentMapping(
- selectedPrinterIds: number[],
- printers: Printer[] | undefined,
- filamentReqs: { filaments: FilamentRequirement[] } | undefined,
- defaultMappings: Record<number, number>,
- perPrinterConfigs: Record<number, PerPrinterConfig>,
- setPerPrinterConfigs: React.Dispatch<React.SetStateAction<Record<number, PerPrinterConfig>>>
- ): UseMultiPrinterFilamentMappingResult {
- // Fetch printer status for all selected printers in parallel
- const statusQueries = useQueries({
- queries: selectedPrinterIds.map((printerId) => ({
- queryKey: ['printer-status', printerId],
- queryFn: () => api.getPrinterStatus(printerId),
- enabled: selectedPrinterIds.length > 0,
- staleTime: 5000, // Consider data fresh for 5 seconds
- })),
- });
- // Build results for each printer
- const printerResults = useMemo((): PrinterMappingResult[] => {
- return selectedPrinterIds.map((printerId, index) => {
- const query = statusQueries[index];
- const printerStatus = query?.data;
- const printer = printers?.find((p) => p.id === printerId);
- const printerName = printer?.name || `Printer ${printerId}`;
- const loadedFilaments = buildLoadedFilaments(printerStatus);
- const config = perPrinterConfigs[printerId] || DEFAULT_PRINTER_CONFIG;
- // Compute auto mapping for this printer
- const autoMapping = computeAmsMapping(filamentReqs, printerStatus);
- // Determine which mappings to use:
- // If printer has override (useDefault=false), use its custom mappings
- // Otherwise use the default mappings
- const effectiveMappings = !config.useDefault
- ? config.manualMappings
- : defaultMappings;
- // Compute final mapping with overrides
- const finalMapping = computeMappingWithOverrides(filamentReqs, printerStatus, effectiveMappings);
- // Compute match details
- const matchDetails = computeMatchDetails(
- filamentReqs?.filaments,
- loadedFilaments,
- effectiveMappings
- );
- return {
- printerId,
- printerName,
- status: printerStatus,
- isLoading: query?.isLoading ?? false,
- loadedFilaments,
- autoMapping,
- finalMapping,
- matchStatus: matchDetails.status,
- exactMatches: matchDetails.exactMatches,
- typeOnlyMatches: matchDetails.typeOnlyMatches,
- missingTypes: matchDetails.missingTypes,
- totalSlots: matchDetails.totalSlots,
- config,
- };
- });
- }, [selectedPrinterIds, statusQueries, printers, filamentReqs, perPrinterConfigs, defaultMappings]);
- const isLoading = statusQueries.some((q) => q.isLoading);
- // Update config for a specific printer
- const updatePrinterConfig = (printerId: number, updates: Partial<PerPrinterConfig>) => {
- setPerPrinterConfigs((prev) => ({
- ...prev,
- [printerId]: {
- ...(prev[printerId] || DEFAULT_PRINTER_CONFIG),
- ...updates,
- },
- }));
- };
- // Auto-configure a specific printer based on its loaded filaments
- const autoConfigurePrinter = (printerId: number) => {
- const result = printerResults.find((r) => r.printerId === printerId);
- if (!result || !result.status || !filamentReqs?.filaments) return;
- // Compute optimal mapping for this printer
- const autoMapping = computeAmsMapping(filamentReqs, result.status);
- if (!autoMapping) return;
- // Convert autoMapping array to manualMappings record
- const manualMappings: Record<number, number> = {};
- autoMapping.forEach((globalTrayId, index) => {
- if (globalTrayId !== -1) {
- manualMappings[index + 1] = globalTrayId;
- }
- });
- updatePrinterConfig(printerId, {
- useDefault: false,
- manualMappings,
- autoConfigured: true,
- });
- };
- // Auto-configure all printers
- const autoConfigureAll = () => {
- for (const printerId of selectedPrinterIds) {
- autoConfigurePrinter(printerId);
- }
- };
- // Get final mapping for a specific printer (for submission)
- const getFinalMapping = (printerId: number): number[] | undefined => {
- const result = printerResults.find((r) => r.printerId === printerId);
- return result?.finalMapping;
- };
- // Check if all printers have acceptable mappings (no missing types)
- const allPrintersReady = printerResults.every((r) => r.matchStatus !== 'missing');
- return {
- printerResults,
- isLoading,
- perPrinterConfigs,
- updatePrinterConfig,
- autoConfigureAll,
- autoConfigurePrinter,
- getFinalMapping,
- allPrintersReady,
- };
- }
|