# S03: Text System + Font Loading

**Goal:** Add text objects to the design canvas with font loading from bundled fonts, property editing (font family, size, letter spacing, line height), and text-to-path conversion using opentype.js.
**Demo:** After this: Add text objects, select fonts from picker, change size/spacing, convert text to paths — paths match original glyphs

## Tasks
- [x] **T01: Built fontService with opentype.js font loading, caching, text-to-path conversion, bundled 3 OFL Google Fonts, and added 24 passing unit tests** — Create the fontService utility that loads .ttf fonts via fetch + opentype.parse(), caches parsed Font objects, and converts text to SVG path data. Bundle 3-5 OFL-licensed Google Fonts as .ttf files in app/public/fonts/. Add @font-face CSS declarations in App.css so Konva Text rendering matches the opentype.js fonts. Install opentype.js dependency. Write comprehensive unit tests for font loading, caching, and text-to-path conversion.

This is the riskiest piece of the slice — if opentype.js path extraction doesn't produce accurate SVG `d` attributes compatible with Konva's Path component, everything downstream breaks. Testing with real bundled font files proves the integration.

## Key Constraints
- opentype.js loads fonts from ArrayBuffer — must fetch() font files then call opentype.parse()
- Path.toPathData() needs consideration of coordinate systems (font Y-up vs canvas Y-down)
- Letter spacing is NOT built into getPath() — must implement per-character glyph positioning with manual x-advance
- Font files served from /fonts/ via Vite's public directory
- @font-face CSS must load before Konva renders Text nodes
- TypeScript strict mode with verbatimModuleSyntax — use `import type` for type-only imports
- Tests should mock fetch() to return real font file ArrayBuffers loaded from disk
  - Estimate: 1h
  - Files: app/src/utils/fontService.ts, app/src/utils/__tests__/fontService.test.ts, app/public/fonts/, app/src/App.css, app/package.json
  - Verify: cd app && npx vitest run src/utils/__tests__/fontService.test.ts --reporter=verbose && npx tsc --noEmit && ls public/fonts/*.ttf | wc -l
- [x] **T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage** — Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types.

## Key Implementation Details
- TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping)
- CanvasTool union in KonvaStage.tsx must add 'text' — this is imported by CanvasToolbar
- KonvaStage renderObject switch: case 'text' renders <Text> from react-konva with fontFamily, fontSize, fill, stroke, strokeWidth, width, letterSpacing, lineHeight props
- KonvaStage handleStageMouseDown switch: case 'text' creates a TextObject with defaults (text: 'Text', fontFamily: 'Roboto', fontSize: 24, letterSpacing: 0, lineHeight: 1.2)
- KonvaStage getObjWidth for text: use obj.width (wrapping width) or estimate from fontSize * text.length * 0.6
- KonvaStage getObjHeight for text: obj.fontSize * obj.lineHeight
- KonvaStage onTransformEnd for text: scale width, keep fontSize unchanged (text wraps to new width)
- ObjectPanel TYPE_ICONS: text → 'T'
- ShapeProperties getWidth/getHeight: add case 'text' returning obj.width and obj.fontSize * obj.lineHeight
- All switch statements must be exhaustive — TypeScript noFallthroughCasesInSwitch enforces this
- TypeScript strict mode — no unused locals/params, erasableSyntaxOnly, use import type
  - Estimate: 1h
  - Files: app/src/types/canvas.ts, app/src/components/canvas/KonvaStage.tsx, app/src/components/canvas/CanvasToolbar.tsx, app/src/components/canvas/ObjectPanel.tsx, app/src/components/canvas/ShapeProperties.tsx
  - Verify: cd app && npx tsc --noEmit && npx vitest run --reporter=verbose
- [ ] **T03: Build text properties panel and convert-to-paths action in ShapeProperties** — Add text-specific property controls to ShapeProperties and implement the 'Convert to Paths' action that replaces a TextObject with path-based objects using fontService.textToPathData().

## Text Property Controls (in ShapeProperties)
When the selected object has type 'text', show:
1. **Text content** — textarea input bound to obj.text
2. **Font family** — dropdown/select populated from fontService.getAvailableFonts(), bound to obj.fontFamily
3. **Font size** — number input (min 8, max 200, step 1), bound to obj.fontSize
4. **Letter spacing** — number input (min -5, max 20, step 0.5), bound to obj.letterSpacing
5. **Line height** — number input (min 0.5, max 3, step 0.1), bound to obj.lineHeight
6. **Fill color** — color input (reuse existing pattern from hasFill), bound to obj.fill
7. **Stroke color + weight** — reuse existing stroke controls
8. **Convert to Paths button** — calls fontService.textToPathData(), then replaces the text object with a new path object (type 'image' with SVG data URL, or a new 'path' type if simpler)

## Convert to Paths Implementation
- Show confirmation dialog (window.confirm) since this is destructive
- Call `textToPathData(obj.text, obj.fontFamily, obj.fontSize, obj.letterSpacing)` from fontService
- Create SVG string from returned path data: `<svg xmlns=... viewBox=...><path d="{pathData}" fill="{obj.fill}" stroke="{obj.stroke}" stroke-width="{obj.strokeWidth}"/></svg>`
- Convert SVG to Blob URL, create ImageObject replacing the TextObject at same x,y position
- Use onUpdate to remove old text object and add new image object (or dispatch through a callback prop)

## Integration Notes
- fontService.ts from T01 provides loadFont(), getAvailableFonts(), textToPathData()
- The text property controls should only show when object.type === 'text'
- Existing stroke/fill/opacity/rotation controls still show for text objects
- ShapeProperties receives onUpdate callback from DesignCanvas — convert-to-paths needs onUpdate + onAddObject + onRemoveObject, so ShapeProperties props may need extending, or the convert action can be handled via a new callback prop
- DesignCanvas.tsx needs to pass the additional callback(s) to ShapeProperties
  - Estimate: 1h
  - Files: app/src/components/canvas/ShapeProperties.tsx, app/src/views/DesignCanvas.tsx
  - Verify: cd app && npx tsc --noEmit && npx vitest run --reporter=verbose
