import { useMemo, useState } from 'react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Download, FileText } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { DECK_HOLE_RADIUS, DECK_PRESETS, DEFAULT_DECK, deckDxf, deckHoles, deckLength, deckOutline, deckPath, deckSidePoints, deckSvg, type DeckParams, } from '@/lib/deckDesign'; /** * Parametric 2D deck designer — outline + printable 1:1 template + DXF. * * Deliberately 2D (a template you trace and cut), not a 3D CAD clone. State is * local-only (localStorage). See `src/lib/deckDesign.ts` for the geometry. */ export function DeckDesigner() { const { t } = useTranslation(); const { toast } = useToast(); const [raw, setRaw] = useLocalStorage('skatespots:deck-design:v1', DEFAULT_DECK); const [busy, setBusy] = useState(false); const p = useMemo(() => ({ ...DEFAULT_DECK, ...raw }), [raw]); const patch = (next: Partial) => setRaw({ ...DEFAULT_DECK, ...raw, ...next }); const totalLen = deckLength(p); const holes = useMemo(() => deckHoles(p), [p]); const inches = (mm: number) => (mm / 25.4).toFixed(2); const label = 'text-[11px] font-semibold text-gray-400'; const download = (name: string, text: string, mime: string) => { const url = URL.createObjectURL(new Blob([text], { type: mime })); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url); }; const exportSvg = () => download(`deck-${Math.round(totalLen)}x${Math.round(p.width)}mm.svg`, deckSvg(p), 'image/svg+xml'); const exportDxf = () => download(`deck-${Math.round(totalLen)}x${Math.round(p.width)}mm.dxf`, deckDxf(p), 'application/dxf'); /** Tiled 1:1 PDF: split the outline across A4/Letter sheets with registration marks. */ const exportPdf = async (paper: 'a4' | 'letter') => { setBusy(true); try { const { jsPDF } = await import('jspdf'); const doc = new jsPDF({ unit: 'mm', format: paper, orientation: 'landscape' }); const pageW = doc.internal.pageSize.getWidth(); const pageH = doc.internal.pageSize.getHeight(); const M = 10; const tileW = pageW - 2 * M; const tileH = pageH - 2 * M; const W = p.width; const pts = deckOutline(p); const cols = Math.max(1, Math.ceil(totalLen / tileW)); const rows = Math.max(1, Math.ceil(W / tileH)); const tx = (x: number, c: number) => M + x - c * tileW; const ty = (y: number, r: number) => M + (y + W / 2) - r * tileH; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (r > 0 || c > 0) doc.addPage(); // Outline, drawn as deltas (doc.lines) so no path-API dependency. const deltas: [number, number][] = []; for (let i = 1; i < pts.length; i++) deltas.push([pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y]); deltas.push([pts[0].x - pts[pts.length - 1].x, pts[0].y - pts[pts.length - 1].y]); doc.setDrawColor(0); doc.setLineWidth(0.25); doc.lines(deltas, tx(pts[0].x, c), ty(pts[0].y, r), [1, 1], 'S', true); for (const h of holes) doc.circle(tx(h.x, c), ty(h.y, r), DECK_HOLE_RADIUS, 'S'); // Registration crosses at the tile corners (adjacent pages align on them). doc.setDrawColor(150); doc.setLineWidth(0.15); for (const [px, py] of [[M, M], [M + tileW, M], [M, M + tileH], [M + tileW, M + tileH]] as [number, number][]) { doc.line(px - 3, py, px + 3, py); doc.line(px, py - 3, px, py + 3); } doc.setFontSize(7); doc.setTextColor(110); doc.text(`Deck template · ${Math.round(totalLen)} × ${Math.round(W)} mm · 1:1 · print at 100% · tile ${r + 1},${c + 1} of ${rows}×${cols}`, M, pageH - 4); } } doc.save(`deck-template-1to1-${paper}.pdf`); } catch { toast({ title: 'Could not build the PDF', variant: 'destructive' }); } finally { setBusy(false); } }; const field = (key: keyof DeckParams, labelText: string, opts?: { min?: number; max?: number; step?: number; hint?: string }) => ( ); return (
{/* Presets */}
{t('diyUi.designer.presets')} {DECK_PRESETS.map((preset) => ( ))}
{/* Inputs */}

{t('diyUi.designer.dimensions')}

{field('width', t('diyUi.designer.width'), { min: 20, hint: `≈ ${inches(p.width)}″` })} {field('wheelbase', t('diyUi.designer.wheelbase'), { min: 30, hint: `≈ ${inches(p.wheelbase)}″` })} {field('nose', t('diyUi.designer.nose'), { min: 5, hint: `≈ ${inches(p.nose)}″` })} {field('tail', t('diyUi.designer.tail'), { min: 5, hint: `≈ ${inches(p.tail)}″` })}

{t('diyUi.designer.length')} {totalLen.toFixed(0)} mm ({inches(totalLen)}″)

{t('diyUi.designer.shape')}

{field('waist', t('diyUi.designer.waist'), { min: 0, max: 0.15, step: 0.01 })} {field('endRound', t('diyUi.designer.endRound'), { min: 1.2, max: 4, step: 0.1 })} {field('concave', t('diyUi.designer.concave'), { min: 0, hint: 'mm' })} {field('thickness', t('diyUi.designer.thickness'), { min: 2, hint: 'mm' })}

{t('diyUi.designer.kicks')}

{field('tailKick', t('diyUi.designer.tailKick'), { min: 0, max: 40, hint: '°' })} {field('noseKick', t('diyUi.designer.noseKick'), { min: 0, max: 40, hint: '°' })}

{t('diyUi.designer.hardware')}

{(['new', 'old'] as const).map((pattern) => ( ))}

{t('diyUi.designer.holeSpacing')}

{/* Preview + export */}

{t('diyUi.designer.topView')}

{Math.round(totalLen)} × {Math.round(p.width)} mm
{holes.map((h, i) => ( ))}

{t('diyUi.designer.sideView')}

{t('diyUi.designer.concaveSection')}

{t('diyUi.designer.export')}

{t('diyUi.designer.printNote')}

{t('diyUi.designer.note')}

); } /** Small profile drawing: flat middle, kicked nose and tail. */ function SideView({ p }: { p: DeckParams }) { const sp = deckSidePoints(p); const totalLen = deckLength(p); const maxRise = Math.max(...sp.map((s) => s.y), 1); const H = maxRise + p.thickness + 4; const flip = (y: number) => H - y; const top = sp.map((s) => `${s.x} ${flip(s.y)}`).join(' L '); const bottom = [...sp].reverse().map((s) => `${s.x} ${flip(s.y - p.thickness)}`).join(' L '); return ( ); } /** Small cross-section drawing: the concave arc across the width. */ function CrossSection({ p }: { p: DeckParams }) { const W = p.width; const dip = p.concave; const samples = 40; const pts: string[] = []; for (let i = 0; i <= samples; i++) { const x = (W * i) / samples; const t = (2 * x) / W - 1; // -1..1 const y = -dip * (1 - t * t); // parabola, deepest at the centre pts.push(`${x} ${y}`); } const H = Math.max(dip, 2) + 4; const d = `M 0 0 L ${pts.join(' L ')} L ${W} 0`; return ( {dip} mm concave · {Math.round(W)} mm ); }