/** * ExportView (View 3) — format selection, validation, and download. * * Receives canvas state from App.tsx (lifted in T02) and uses the export * service (T03) to compose SVG, validate, call the engine API, and trigger * downloads. */ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ArtboardConfig, ArtboardUnit, CanvasObject } from '../types/canvas'; import { composeCanvasSVG, validateForExport, triggerDownload, type ValidationResult, } from '../utils/exportService'; import { exportAsDxf } from '../api/engine'; import styles from './ExportView.module.css'; // -- Types -------------------------------------------------------------------- export type ExportFormat = 'dxf' | 'svg' | 'png'; export interface ExportViewProps { objects: CanvasObject[]; artboard: ArtboardConfig | null; pngDataUrl: string | null; onBack: () => void; } interface FormatOption { id: ExportFormat; name: string; description: string; } const FORMAT_OPTIONS: FormatOption[] = [ { id: 'dxf', name: 'DXF', description: 'AutoCAD / LightBurn — vector paths with real-world scale', }, { id: 'svg', name: 'SVG', description: 'Scalable vector — Inkscape, Illustrator, web', }, { id: 'png', name: 'PNG', description: 'Raster image — print preview, sharing', }, ]; // Scale factors: canvas uses 96 PPI internally const SCALE_FACTORS: Record = { inches: 1 / 96, mm: 25.4 / 96, }; // -- Component ---------------------------------------------------------------- export default function ExportView({ objects, artboard, pngDataUrl, onBack, }: ExportViewProps) { const [selectedFormat, setSelectedFormat] = useState('dxf'); const [selectedUnit, setSelectedUnit] = useState( artboard?.unit ?? 'inches', ); const [exporting, setExporting] = useState(false); const [exportError, setExportError] = useState(null); // Run validation on mount and when objects/artboard change const validation: ValidationResult = useMemo( () => validateForExport(objects, artboard), [objects, artboard], ); const hasBlockingErrors = validation.issues.some( (i) => i.severity === 'error', ); // Reset unit when artboard changes useEffect(() => { if (artboard) { setSelectedUnit(artboard.unit); } }, [artboard]); // Whether unit selector is relevant for the selected format const showUnitSelector = selectedFormat === 'dxf' || selectedFormat === 'svg'; // -- Export handlers -------------------------------------------------------- const handleExport = useCallback(async () => { if (!artboard && selectedFormat !== 'png') return; setExporting(true); setExportError(null); try { switch (selectedFormat) { case 'dxf': { const svgString = composeCanvasSVG(objects, artboard!); const scaleFactor = SCALE_FACTORS[selectedUnit]; const dxfBlob = await exportAsDxf( svgString, selectedUnit, scaleFactor, ); triggerDownload(dxfBlob, 'export.dxf'); break; } case 'svg': { const svgString = composeCanvasSVG(objects, artboard!); const svgBlob = new Blob([svgString], { type: 'image/svg+xml' }); triggerDownload(svgBlob, 'export.svg'); break; } case 'png': { if (!pngDataUrl) { setExportError('No PNG preview available. Go back and try again.'); break; } // Convert data URL to Blob const response = await fetch(pngDataUrl); const pngBlob = await response.blob(); triggerDownload(pngBlob, 'export.png'); break; } } } catch (err) { const message = err instanceof Error ? err.message : 'Export failed unexpectedly.'; setExportError(message); console.error('[ExportView] export error:', err); } finally { setExporting(false); } }, [selectedFormat, selectedUnit, objects, artboard, pngDataUrl]); // Disable download when there are blocking errors (except PNG which // doesn't need artboard/vector validation) const downloadDisabled = exporting || (selectedFormat !== 'png' && hasBlockingErrors) || (selectedFormat === 'png' && !pngDataUrl); // -- Render ----------------------------------------------------------------- return (
{/* Header */}

Export

{/* Main panel — preview */}
Preview {pngDataUrl ? ( Design preview ) : (
No preview available
)}
{/* Side panel — format, units, validation, download */}
); }