# 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 handleToolClick('neues-tool')} isDark={isDark} label="Neues Tool" icon={...} /> ``` 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 && (
{ setMyProperty(e.target.value) updateProperty('myProperty', e.target.value) }} className={`w-full px-3 py-2 rounded-xl border text-sm ${inputStyle}`} />
)} ``` ## 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 } ``` ## 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