# Visual Worksheet Editor - Developer Guide

**Version:** 1.0
**Datum:** 2026-01-23

## 1. Schnellstart

### 1.1 Dependencies installieren

```bash
cd studio-v2
npm install fabric@^6.0.0 pdf-lib@^1.17.1
```

### 1.2 Entwicklungsserver starten

```bash
# Frontend (Port 3001)
cd studio-v2
npm run dev

# Backend (Port 8086)
cd klausur-service/backend
source venv/bin/activate
python main.py
```

### 1.3 Editor öffnen

```
http://localhost:3001/worksheet-editor
```

## 2. Komponenten-Entwicklung

### 2.1 Neues Werkzeug hinzufügen

1. **Tool-Typ definieren** in `types.ts`:

```typescript
export type EditorTool =
  | 'select'
  | 'text'
  // ... existierende Tools
  | 'neues-tool'  // NEU
```

2. **Button hinzufügen** in `EditorToolbar.tsx`:

```tsx
<ToolButton
  tool="neues-tool"
  isActive={activeTool === 'neues-tool'}
  onClick={() => handleToolClick('neues-tool')}
  isDark={isDark}
  label="Neues Tool"
  icon={<svg>...</svg>}
/>
```

3. **Handler implementieren** in `FabricCanvas.tsx`:

```typescript
case 'neues-tool': {
  // Canvas-Objekt erstellen
  const obj = new fabric.CustomObject({...})
  fabricCanvas.add(obj)
  fabricCanvas.setActiveObject(obj)
  setActiveTool('select')
  break
}
```

### 2.2 Eigenschaften-Panel erweitern

In `PropertiesPanel.tsx` neue Eigenschaften für einen Objekttyp hinzufügen:

```tsx
{isMyType && (
  <div className="space-y-4">
    <div>
      <label className={`block text-sm font-medium mb-2 ${labelStyle}`}>
        Neue Eigenschaft
      </label>
      <input
        type="text"
        value={myProperty}
        onChange={(e) => {
          setMyProperty(e.target.value)
          updateProperty('myProperty', e.target.value)
        }}
        className={`w-full px-3 py-2 rounded-xl border text-sm ${inputStyle}`}
      />
    </div>
  </div>
)}
```

## 3. Context API

### 3.1 State abrufen

```tsx
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'

function MyComponent() {
  const {
    canvas,
    activeTool,
    setActiveTool,
    selectedObjects,
    zoom,
    setZoom
  } = useWorksheet()

  // Verwenden...
}
```

### 3.2 Canvas-Operationen

```typescript
// Objekt hinzufügen
canvas.add(newObject)
canvas.setActiveObject(newObject)
canvas.renderAll()

// Objekt entfernen
canvas.remove(selectedObject)

// Alle Objekte abrufen (ohne Grid)
const objects = canvas.getObjects().filter(obj => !obj.isGrid)

// Canvas exportieren
const json = canvas.toJSON()
const dataUrl = canvas.toDataURL({ format: 'png', multiplier: 2 })

// Canvas laden
canvas.loadFromJSON(jsonData, () => {
  canvas.renderAll()
})
```

## 4. API-Integration

### 4.1 Worksheet speichern

```typescript
const saveWorksheet = async () => {
  const host = window.location.hostname
  const apiBase = `http://${host}:8086`

  const response = await fetch(`${apiBase}/api/v1/worksheet/save`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: worksheetId,
      title: 'Mein Arbeitsblatt',
      pages: [{
        id: 'page_1',
        index: 0,
        canvasJSON: JSON.stringify(canvas.toJSON())
      }]
    })
  })

  const result = await response.json()
  console.log('Gespeichert:', result.id)
}
```

### 4.2 KI-Bild generieren

```typescript
const generateAIImage = async (prompt: string) => {
  const host = window.location.hostname
  const apiBase = `http://${host}:8086`

  const response = await fetch(`${apiBase}/api/v1/worksheet/ai-image`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      prompt,
      style: 'educational',
      width: 512,
      height: 512
    })
  })

  const { image_base64, error } = await response.json()

  if (image_base64) {
    // Bild zum Canvas hinzufügen
    fabric.Image.fromURL(image_base64, (img) => {
      canvas.add(img)
      canvas.setActiveObject(img)
      canvas.renderAll()
    })
  }
}
```

## 5. Fabric.js Patterns

### 5.1 Text-Objekt erstellen

```typescript
const text = new fabric.IText('Text eingeben', {
  left: 100,
  top: 100,
  fontFamily: 'Arial',
  fontSize: 16,
  fill: '#000000',
})
canvas.add(text)
text.enterEditing() // Bearbeitungsmodus
```

### 5.2 Form erstellen

```typescript
// Rechteck
const rect = new fabric.Rect({
  left: 100,
  top: 100,
  width: 200,
  height: 100,
  fill: 'transparent',
  stroke: '#000000',
  strokeWidth: 2,
  rx: 5,  // Eckenradius
  ry: 5,
})

// Kreis
const circle = new fabric.Circle({
  left: 100,
  top: 100,
  radius: 50,
  fill: '#ff6b6b',
  stroke: '#000000',
  strokeWidth: 2,
})

// Linie
const line = new fabric.Line([50, 50, 200, 50], {
  stroke: '#000000',
  strokeWidth: 2,
})
```

### 5.3 Bild laden

```typescript
fabric.Image.fromURL(imageUrl, (img) => {
  // Skalierung auf max. Größe
  const maxWidth = 400
  const maxHeight = 300
  const scale = Math.min(maxWidth / img.width, maxHeight / img.height, 1)

  img.set({
    left: 100,
    top: 100,
    scaleX: scale,
    scaleY: scale,
  })

  canvas.add(img)
}, { crossOrigin: 'anonymous' })
```

### 5.4 Events

```typescript
// Selection Events
canvas.on('selection:created', (e) => {
  const selected = canvas.getActiveObjects()
  console.log('Ausgewählt:', selected.length)
})

canvas.on('selection:cleared', () => {
  console.log('Auswahl aufgehoben')
})

// Object Events
canvas.on('object:modified', (e) => {
  console.log('Objekt geändert:', e.target)
  saveToHistory('modified')
})

canvas.on('object:added', (e) => {
  console.log('Objekt hinzugefügt:', e.target)
})

// Mouse Events
canvas.on('mouse:down', (e) => {
  const pointer = canvas.getPointer(e.e)
  console.log('Klick bei:', pointer.x, pointer.y)
})
```

## 6. Testing

### 6.1 Unit Tests für Context

```typescript
// __tests__/worksheet-context.test.tsx
import { renderHook, act } from '@testing-library/react'
import { WorksheetProvider, useWorksheet } from '../WorksheetContext'

describe('useWorksheet', () => {
  it('should initialize with default values', () => {
    const { result } = renderHook(() => useWorksheet(), {
      wrapper: WorksheetProvider,
    })

    expect(result.current.activeTool).toBe('select')
    expect(result.current.zoom).toBe(1)
    expect(result.current.showGrid).toBe(true)
  })

  it('should change tool', () => {
    const { result } = renderHook(() => useWorksheet(), {
      wrapper: WorksheetProvider,
    })

    act(() => {
      result.current.setActiveTool('text')
    })

    expect(result.current.activeTool).toBe('text')
  })
})
```

### 6.2 API Tests

```python
# tests/test_worksheet_editor_api.py
import pytest
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_save_worksheet():
    response = client.post("/api/v1/worksheet/save", json={
        "title": "Test Worksheet",
        "pages": [{
            "id": "page_1",
            "index": 0,
            "canvasJSON": "{}"
        }]
    })
    assert response.status_code == 200
    assert "id" in response.json()

def test_get_worksheet():
    # Erst erstellen
    create_response = client.post("/api/v1/worksheet/save", json={
        "title": "Test",
        "pages": [{"id": "p1", "index": 0, "canvasJSON": "{}"}]
    })
    worksheet_id = create_response.json()["id"]

    # Dann laden
    response = client.get(f"/api/v1/worksheet/{worksheet_id}")
    assert response.status_code == 200
    assert response.json()["title"] == "Test"

def test_ai_image_generation():
    response = client.post("/api/v1/worksheet/ai-image", json={
        "prompt": "A friendly dog",
        "style": "cartoon",
        "width": 256,
        "height": 256
    })
    # Kann 200 (Bild) oder 503 (Ollama nicht verfügbar) sein
    assert response.status_code in [200, 503]
```

## 7. Styling

### 7.1 Glassmorphism Utilities

```typescript
// Für Theme-aware Styling
const glassCard = isDark
  ? 'backdrop-blur-xl bg-white/10 border border-white/20'
  : 'backdrop-blur-xl bg-white/70 border border-black/10 shadow-xl'

const glassInput = isDark
  ? 'bg-white/10 border-white/20 text-white placeholder-white/40'
  : 'bg-white/50 border-black/10 text-slate-900 placeholder-slate-400'

const labelStyle = isDark ? 'text-white/70' : 'text-slate-600'
```

### 7.2 Button States

```typescript
const buttonStyle = (active: boolean) => isDark
  ? active
    ? 'bg-purple-500/30 text-purple-300'
    : 'text-white/70 hover:bg-white/10 hover:text-white'
  : active
    ? 'bg-purple-100 text-purple-700'
    : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
```

## 8. Best Practices

### 8.1 Performance

- Grid-Objekte mit `isGrid: true` markieren und vom Export ausschließen
- Canvas-JSON nur bei tatsächlichen Änderungen speichern
- History auf 50 Einträge limitieren
- Bilder mit `multiplier: 2` für Retina-Export

### 8.2 Fehlerbehandlung

```typescript
try {
  const response = await fetch(apiUrl)
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }
  const data = await response.json()
  return data
} catch (error) {
  console.error('API Error:', error)
  // Fallback auf localStorage
  return JSON.parse(localStorage.getItem(key) || '{}')
}
```

### 8.3 Hydration Safety

```typescript
const [mounted, setMounted] = useState(false)

useEffect(() => {
  setMounted(true)
}, [])

if (!mounted) {
  return <LoadingSpinner />
}
```

## 9. Debugging

### 9.1 Canvas-State inspizieren

```typescript
// Im Browser DevTools Console
const canvas = document.querySelector('canvas')?.__fabric
console.log('Objects:', canvas?.getObjects())
console.log('Active:', canvas?.getActiveObject())
console.log('JSON:', canvas?.toJSON())
```

### 9.2 API-Calls überwachen

```bash
# Backend-Logs
tail -f /var/log/klausur-service.log

# Oder im Terminal wo main.py läuft
```

## 10. Deployment Checklist

- [ ] Dependencies in package.json
- [ ] Fabric.js und pdf-lib installiert
- [ ] Backend-Router registriert
- [ ] i18n-Übersetzungen vorhanden
- [ ] Sidebar-Navigation aktualisiert
- [ ] CORS für Produktions-Domain konfiguriert
- [ ] Ollama-URL in Umgebungsvariablen
