import { useState } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Loader2, Monitor, AlertCircle, Box, Maximize2 } from 'lucide-react'; import { api } from '../api/client'; import { useToast } from '../contexts/ToastContext'; import { useAuth } from '../contexts/AuthContext'; import { ConfirmModal } from './ConfirmModal'; // Custom Skip Objects icon - arrow jumping over boxes export const SkipObjectsIcon = ({ className }: { className?: string }) => ( {/* Three boxes at the bottom */} {/* Curved arrow jumping over first box */} ); interface SkipObjectsModalProps { printerId: number; isOpen: boolean; onClose: () => void; } export function SkipObjectsModal({ printerId, isOpen, onClose }: SkipObjectsModalProps) { const { t } = useTranslation(); const { showToast } = useToast(); const { hasPermission } = useAuth(); const [pendingSkip, setPendingSkip] = useState<{ id: number; name: string } | null>(null); const [enlarged, setEnlarged] = useState(false); const { data: status } = useQuery({ queryKey: ['printerStatus', printerId], queryFn: () => api.getPrinterStatus(printerId), refetchInterval: 30000, enabled: isOpen, }); const { data: objectsData, refetch: refetchObjects } = useQuery({ queryKey: ['printableObjects', printerId], queryFn: () => api.getPrintableObjects(printerId), enabled: isOpen, refetchInterval: isOpen ? 5000 : false, }); const skipObjectsMutation = useMutation({ mutationFn: (objectIds: number[]) => api.skipObjects(printerId, objectIds), onSuccess: (data) => { showToast(data.message || t('printers.skipObjects.objectsSkipped')); setPendingSkip(null); refetchObjects(); }, onError: (error: Error) => showToast(error.message || t('printers.toast.failedToSkipObjects'), 'error'), }); if (!isOpen) return null; return ( <>
{ if (e.key === 'Escape') { if (enlarged) setEnlarged(false); else onClose(); } }} tabIndex={-1} ref={(el) => el?.focus()} > {/* Backdrop */}
{/* Modal */}
e.stopPropagation()} > {/* Header */}
{t('printers.skipObjects.title')}
{!objectsData ? (
) : objectsData.objects.length === 0 ? (

{t('printers.noObjectsFound')}

{t('printers.objectsLoadedOnPrintStart')}

) : (
{/* Info Banner */}

{t('printers.skipObjects.matchIdsInfo')}

{t('printers.skipObjects.printerShowsIds')}

{objectsData.skipped_count}/{objectsData.total} {t('printers.skipObjects.skipped')}
{/* Layer Warning */} {(status?.layer_num ?? 0) <= 1 && (

{t('printers.skipObjects.waitForLayer', { layer: status?.layer_num ?? 0 })}

)} {/* Content: Image + List side by side */}
{/* Left: Preview Image with object markers */}
setEnlarged(true)}> {status?.cover_url ? ( {t('printers.printPreview')} ) : (
)} {/* Enlarge hint */}
{/* Object ID markers overlay - positioned based on object data */} {objectsData.objects.length > 0 && (
{objectsData.objects.map((obj, idx) => { let x: number, y: number; // Use position data if available, otherwise fall back to grid if (obj.x != null && obj.y != null && objectsData.bbox_all) { // bbox_all defines the visible area in the top_N.png image // Format: [x_min, y_min, x_max, y_max] in mm const [xMin, yMin, xMax, yMax] = objectsData.bbox_all; const bboxWidth = xMax - xMin; const bboxHeight = yMax - yMin; // The image shows bbox_all area with some padding (~5-10%) const padding = 8; const contentArea = 100 - (padding * 2); // Map object position to image percentage x = padding + ((obj.x - xMin) / bboxWidth) * contentArea; // Y axis: image Y increases downward, but 3D Y increases toward back y = padding + ((yMax - obj.y) / bboxHeight) * contentArea; // Clamp to valid range x = Math.max(5, Math.min(95, x)); y = Math.max(5, Math.min(95, y)); } else if (obj.x != null && obj.y != null) { // Fallback: use full build plate (256mm) const buildPlate = 256; x = (obj.x / buildPlate) * 100; y = 100 - (obj.y / buildPlate) * 100; x = Math.max(5, Math.min(95, x)); y = Math.max(5, Math.min(95, y)); } else { // Fallback: arrange in a grid pattern over the build plate area const cols = Math.ceil(Math.sqrt(objectsData.objects.length)); const row = Math.floor(idx / cols); const col = idx % cols; const rows = Math.ceil(objectsData.objects.length / cols); x = 15 + (col * (70 / cols)) + (35 / cols); y = 15 + (row * (70 / rows)) + (35 / rows); } return (
{obj.id}
); })}
)} {/* Object count overlay */}
{t('printers.skipObjects.activeCount', { count: objectsData.objects.filter(o => !o.skipped).length })}
{/* Right: Object List with prominent IDs */}
{objectsData.objects.map((obj) => (
{/* Large prominent ID badge */}
{obj.id} ID
{/* Object name and status */}
{obj.name} {obj.skipped && ( {t('printers.willBeSkipped')} )}
{/* Skip button */} {!obj.skipped ? ( ) : ( {t('printers.skipObjects.skipped')} )}
))}
)}
{pendingSkip && ( skipObjectsMutation.mutate([pendingSkip.id])} onCancel={() => setPendingSkip(null)} /> )} {/* Enlarged lightbox overlay */} {enlarged && objectsData && (
setEnlarged(false)} >
e.stopPropagation()} > {status?.cover_url ? ( {t('printers.printPreview')} ) : (
)} {/* Object ID markers overlay */} {objectsData.objects.length > 0 && (
{objectsData.objects.map((obj, idx) => { let x: number, y: number; if (obj.x != null && obj.y != null && objectsData.bbox_all) { const [xMin, yMin, xMax, yMax] = objectsData.bbox_all; const bboxWidth = xMax - xMin; const bboxHeight = yMax - yMin; const padding = 8; const contentArea = 100 - (padding * 2); x = padding + ((obj.x - xMin) / bboxWidth) * contentArea; y = padding + ((yMax - obj.y) / bboxHeight) * contentArea; x = Math.max(5, Math.min(95, x)); y = Math.max(5, Math.min(95, y)); } else if (obj.x != null && obj.y != null) { const buildPlate = 256; x = (obj.x / buildPlate) * 100; y = 100 - (obj.y / buildPlate) * 100; x = Math.max(5, Math.min(95, x)); y = Math.max(5, Math.min(95, y)); } else { const cols = Math.ceil(Math.sqrt(objectsData.objects.length)); const row = Math.floor(idx / cols); const col = idx % cols; const rows = Math.ceil(objectsData.objects.length / cols); x = 15 + (col * (70 / cols)) + (35 / cols); y = 15 + (row * (70 / rows)) + (35 / rows); } return (
{obj.id}
); })}
)} {/* Active count badge */}
{t('printers.skipObjects.activeCount', { count: objectsData.objects.filter(o => !o.skipped).length })}
)} ); }