# S01: Export Flow (View 3) + DXF Generation

**Goal:** Complete Export view (View 3) with DXF/SVG/PNG download. Users design a sign in View 2, navigate to View 3, select export format, see pre-export validation warnings, and download files with correct real-world scale.
**Demo:** After this: Design a sign with text + imported vector, export as DXF, open in Inkscape/LightBurn with correct geometry and scale

## Tasks
- [x] **T01: Extended generate_dxf() with units/scale_factor/layer_map params and wired units+scale_factor through /engine/simplify API endpoint** — The engine's `generate_dxf()` currently produces DXF files with raw pixel coordinates and no unit metadata. This task extends it to accept `units` ('inches'|'mm') and `scale_factor` parameters, sets the `$INSUNITS` and `$MEASUREMENT` DXF headers, applies `scale_factor` to all polyline coordinates, and adds optional `layer_map` support. The `/engine/simplify` endpoint is extended with optional `units` and `scale_factor` Form params that pass through to the DXF generator via `_format_response()`. Tests verify a known-size artboard (384×576 px = 4×6 inches at 96 PPI) produces DXF coordinates spanning 0–4 × 0–6 with correct `$INSUNITS=1`.
  - Estimate: 1h
  - Files: engine/output/dxf.py, engine/api/routes.py, engine/tests/test_output.py
  - Verify: cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning
- [ ] **T02: Lift canvas state to App.tsx and add Export navigation** — Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor.

Key constraints:
- `useCanvasState` hook itself is NOT modified — only where it's called changes
- DesignCanvas receives `state`, `addObject`, `removeObject`, `updateObject`, `selectObjects`, `deselectAll`, `reorderObject`, `toggleVisibility`, `toggleLock`, `setArtboard`, `undo`, `redo`, `canUndo`, `canRedo` as props instead of calling the hook internally
- The `traceMetadata` param to `useCanvasState()` comes from App.tsx's existing `traceMetadata` state
- App.tsx passes a `stageRef` to DesignCanvas and receives it back so PNG export can work later
- A `Ref<Konva.Stage>` is created in App.tsx and passed to DesignCanvas for stage access from View 3
  - Estimate: 1.5h
  - Files: app/src/App.tsx, app/src/views/DesignCanvas.tsx
  - Verify: cd app && npx tsc -b --noEmit && npx vitest run
- [ ] **T03: Build export service with SVG composition, validation, and DXF API client** — This task creates the core export logic as pure utility functions: (1) `composeCanvasSVG()` renders visible canvas objects into an SVG string with correct coordinate space and real-world unit dimensions, (2) `validateForExport()` checks for unconverted text objects (blocking error), raster-only images (warning), and missing artboard (blocking error), (3) `exportDxf()` in the API client POSTs composed SVG to `/engine/simplify?output_format=dxf` with `units` and `scale_factor` params and returns a Blob, (4) `triggerDownload()` creates a hidden anchor + blob URL to trigger browser download with proper cleanup.

SVG composition details:
- The SVG viewBox matches artboard dimensions in pixels
- `width`/`height` attributes use real-world units (e.g., `width="4in"` or `width="101.6mm"`)
- RectObject → `<rect>`, CircleObject → `<circle>`, EllipseObject → `<ellipse>`, LineObject → `<polyline>`
- ImageObject with SVG blob src → inline the SVG content (extract path data from blob URL)
- ImageObject with raster src → skip (validation warns about this)
- TextObject → error (validation blocks this — must be converted to paths first)
- Objects with `visible: false` are skipped
- All coordinates are in the artboard's pixel space (the engine handles conversion via scale_factor)

DXF API client:
- New function `exportAsDxf(svgContent: string, units: 'inches' | 'mm', scaleFactor: number, signal?: AbortSignal): Promise<Blob>`
- Uses FormData with file as Blob, output_format=dxf, units, scale_factor
- Returns `response.blob()` not `response.json()`

Unit tests cover: SVG composition with known objects produces correct SVG elements, validation catches text objects, validation warns on raster images, coordinate space is correct.
  - Estimate: 2h
  - Files: app/src/utils/exportService.ts, app/src/utils/__tests__/exportService.test.ts, app/src/api/engine.ts, app/src/api/__tests__/engine.test.ts
  - Verify: cd app && npx vitest run src/utils/__tests__/exportService.test.ts src/api/__tests__/engine.test.ts && npx tsc -b --noEmit
- [ ] **T04: Build Export view UI with format selection, validation panel, and download wiring** — This task builds the ExportView component — the final piece that wires everything together. The view receives canvas state from App.tsx (lifted in T02) and uses the export service (built in T03) to compose SVG, validate, call the engine API, and trigger downloads.

ExportView layout:
- Header with "Export" title and a "← Back to Design" button that navigates back to View 2
- Format selector: three cards/buttons for DXF, SVG, PNG with descriptions
- Unit selector (DXF/SVG only): inches or mm radio buttons, defaulting to the artboard's unit
- Validation panel: shows blocking errors (red, disables export) and warnings (yellow, allows export). Runs `validateForExport()` on mount and when objects change
- Canvas preview: a small thumbnail of the current design (use Konva `stage.toDataURL()` from the stageRef passed through App.tsx, captured before navigating to export view)
- Download button: disabled when blocking errors exist; triggers the appropriate export flow

Export flows by format:
- **DXF**: Call `composeCanvasSVG()` → call `exportAsDxf()` with units and scale_factor (1/96 for inches, 25.4/96 for mm) → `triggerDownload()` with the returned blob and filename `export.dxf`
- **SVG**: Call `composeCanvasSVG()` → create blob from SVG string → `triggerDownload()` with filename `export.svg`
- **PNG**: Use the preview data URL (captured from Konva stage before navigating) or re-render. Since stageRef may not be mounted in View 3, capture PNG data URL before transitioning to export view and pass it as a prop. Convert data URL to blob → `triggerDownload()` with filename `export.png`

PNG capture strategy: When user clicks 'Export' in View 2, capture `stageRef.current.toDataURL({ pixelRatio: 2 })` and store in App.tsx state, then navigate to export view. This avoids needing the Konva stage mounted in View 3.

App.tsx updates:
- Add `pngDataUrl` state, set it in the onExport handler before view transition
- Pass `pngDataUrl` to ExportView
- Wire ExportView's onBack to navigate back to canvas view

CSS: New `ExportView.module.css` with the view layout. Follow existing patterns from DesignCanvas.module.css and App.css.
  - Estimate: 2h
  - Files: app/src/views/ExportView.tsx, app/src/views/ExportView.module.css, app/src/App.tsx
  - Verify: cd app && npx tsc -b --noEmit && npx vitest run
