import { useEffect, useRef, useState, useCallback } from 'react'; import { WebGLPreview, init } from 'gcode-preview'; import { Loader2, Layers, ChevronLeft, ChevronRight, FileWarning } from 'lucide-react'; interface BuildVolume { x: number; y: number; z: number; } interface GcodeViewerProps { gcodeUrl: string; buildVolume?: BuildVolume; filamentColors?: string[]; className?: string; } export function GcodeViewer({ gcodeUrl, buildVolume = { x: 256, y: 256, z: 256 }, filamentColors, className = '' }: GcodeViewerProps) { const canvasRef = useRef(null); const previewRef = useRef(null); const renderTimeoutRef = useRef(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [notSliced, setNotSliced] = useState(false); const [currentLayer, setCurrentLayer] = useState(0); const [totalLayers, setTotalLayers] = useState(0); useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; const hasColors = filamentColors && filamentColors.length > 0; const hasMultipleColors = filamentColors && filamentColors.length > 1; // First color or default bambu green const primaryColor = hasColors ? filamentColors[0] : '#00ae42'; // Initialize the preview // For multi-color: pass array of CSS color strings to extrusionColor // The library uses index to match tool number (T0, T1, T2...) const preview = init({ canvas, buildVolume: buildVolume, backgroundColor: 0x1a1a1a, travelColor: 0x444444, // Pass array for multi-color, single value for single color extrusionColor: hasMultipleColors ? filamentColors : primaryColor, // Disable topLayerColor for multi-color (it overrides per-tool colors) ...(hasMultipleColors ? {} : { topLayerColor: primaryColor }), // Disable gradient for multi-color to preserve actual filament colors ...(hasMultipleColors ? { disableGradient: true } : {}), lastSegmentColor: 0xffffff, lineWidth: 2, renderTravel: false, renderExtrusion: true, }); previewRef.current = preview; // Fetch and parse G-code setLoading(true); setError(null); setNotSliced(false); fetch(gcodeUrl) .then(async response => { if (!response.ok) { if (response.status === 404) { const data = await response.json().catch(() => ({})); if (data.detail?.includes('sliced')) { setNotSliced(true); throw new Error('not_sliced'); } } throw new Error('Failed to load G-code'); } return response.text(); }) .then(gcode => { let processedGcode = gcode; if (hasMultipleColors) { // Bambu G-code uses special T commands that confuse the parser: // T255, T1000, T1001, T65535, T65279 etc. are not real tool changes // Filter these out and keep only valid tool numbers (T0-T15) processedGcode = gcode .split('\n') .map(line => { const match = line.match(/^(\s*)T(\d+)(\s*;.*)?$/i); if (match) { const toolNum = parseInt(match[2], 10); // Keep only valid tool numbers (0-15), comment out others if (toolNum > 15) { return `${match[1]}; FILTERED: T${toolNum}${match[3] || ''}`; } } return line; }) .join('\n'); // Prepend T0 to ensure initial tool is set processedGcode = `T0\n${processedGcode}`; } // Parse G-code preview.processGCode(processedGcode); // Get layer count const layers = preview.layers?.length || 0; setTotalLayers(layers); setCurrentLayer(layers); // Render all layers initially preview.render(); setLoading(false); }) .catch(err => { setError(err.message); setLoading(false); }); // Handle resize const handleResize = () => { if (canvas.parentElement) { const rect = canvas.parentElement.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; preview.resize(); } }; handleResize(); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); if (renderTimeoutRef.current) { cancelAnimationFrame(renderTimeoutRef.current); } preview.dispose(); }; }, [gcodeUrl, buildVolume, filamentColors]); // Debounce render to prevent freezing when dragging slider const handleLayerChange = useCallback((layer: number) => { if (!previewRef.current) return; const newLayer = Math.max(1, Math.min(layer, totalLayers)); setCurrentLayer(newLayer); // Debounce the actual render to avoid freezing if (renderTimeoutRef.current) { cancelAnimationFrame(renderTimeoutRef.current); } renderTimeoutRef.current = requestAnimationFrame(() => { if (previewRef.current) { previewRef.current.endLayer = newLayer; previewRef.current.render(); } }); }, [totalLayers]); const handleSliderChange = (e: React.ChangeEvent) => { handleLayerChange(parseInt(e.target.value, 10)); }; return (
{/* Canvas container */}
{loading && (

Loading G-code...

)} {notSliced && (

G-code not available

This file hasn't been sliced yet. G-code preview is only available after slicing the model in Bambu Studio or Orca Slicer.

)} {error && !notSliced && (

{error}

)}
{/* Layer controls */} {!loading && !error && !notSliced && totalLayers > 0 && (
{currentLayer} / {totalLayers}
)}
); }