# Technologie-Zusammenfassung: iPad Skizzen-Tool mit Symbolbibliothek **Datum:** 09. April 2026 **Thema:** Hybride iPad-Anwendung für technische Skizzen (Schienen/Weichen/Kreisel) mit Snap-to-Grid, Symbolbibliothek und JSON/SVG-Export --- ## 1. Anforderungen - iPad als Eingabegerät (Bleistift-Look) - ~10 Grundsymbole (Weichen, Kreisel, etc.) aus Katalog - Snap-to-Grid - Output: **JSON + SVG** - Symbole parametrisch veränderbar (z.B. Kreiselab stand) --- ## 2. Gewählte Architektur: Option C (Hybrid) ``` iPad (PWA/Safari) Hetzner Server ───────────────── ────────────── tldraw (Canvas) FastAPI (Python) rough.js (Bleistift) │ Custom Shapes ├── GET /symbols → Bibliothek ausliefern │ ├── POST /normalize → Skizze → SVG │ JSON (Shapes + Meta) └── POST /export → finales JSON/SVG └──────────────────────────────────────► ``` **Warum Hybrid:** - iPad nur für Eingabe + lokales Snap/Rendering (kein App Store, kein Swift) - Server für Bibliotheksverwaltung, Normalisierung und Export (Python/FastAPI) - PWA läuft direkt im Safari, gehostet auf Hetzner via nginx --- ## 3. Kerntechnologien | Schicht | Technologie | Zweck | |---|---|---| | iPad-Frontend | **tldraw** (PWA) | Canvas, Snap-to-Grid, SVG-Export | | Bleistift-Look | **rough.js** | Skizzenhafter Renderstil | | Symbolbibliothek | **JSON + SVG-Pfade** | Katalog mit Metadaten | | Backend | **FastAPI (Python)** | Bibliothek, Normalisierung, Export | | Output | **SVG + JSON** | Weiterverarbeitung / CAD | --- ## 4. Symbolbibliothek-Format (JSON) ```json { "symbols": [ { "id": "weiche_links_r300", "label": "Weiche links R300", "type": "weiche", "radius": 300, "direction": "left", "svg_path": "M 0,0 L 100,0 Q 150,0 200,50", "snap_points": [[0,0], [100,0], [200,50]], "ports": ["in", "out_gerade", "out_abzweig"] }, { "id": "kreisel_standard", "label": "Kreisel", "type": "kreisel", "geometry": { "circle_top_r": 40, "circle_bottom_r": 40, "straight_length": 80 }, "svg_path": "...", "snap_points": [[0,0], [0,160]] } ] } ``` --- ## 5. Metadaten an tldraw-Shapes anhängen ### Option A: `meta`-Feld (schnell, für Prototypen) ```typescript editor.createShape({ type: 'geo', x: 100, y: 200, props: { geo: 'rectangle', w: 80, h: 40, }, meta: { symbol_id: 'weiche_links_r300', symbol_type: 'weiche', radius: 300, direction: 'left', ports: ['in', 'out_gerade', 'out_abzweig'], catalog_version: '1.2' } }) ``` ### Option B: Custom Shape Type (produktiv, empfohlen) ```typescript const WeicheShapeUtil = defineShape({ type: 'weiche', props: { radius: T.number, direction: T.string, ports: T.arrayOf(T.string), svg_path: T.string, }, component(shape) { return ( ) }, getBounds(shape) { /* ... */ } }) ``` **JSON-Export einer Custom Shape:** ```json { "id": "shape:abc123", "type": "weiche", "x": 100, "y": 200, "props": { "radius": 300, "direction": "left", "ports": ["in", "out_gerade", "out_abzweig"], "svg_path": "M 0,0 L 100,0 Q 150,0 200,50" } } ``` --- ## 6. Parametrische Shapes: Kreisel-Beispiel Ziel: Nutzer kann nur den **Abstand der zwei Kreise** verändern, alles andere ist gesperrt. ### 6.1 Shape-Definition mit eingeschränkten Props ```typescript const KreiselShapeUtil = defineShape({ type: 'kreisel', props: { abstand: T.number, // ← einziger veränderbarer Parameter radius: T.number, // fest (aus Katalog) }, ``` ### 6.2 Custom Handle (nur vertikale Bewegung) ```typescript getHandles(shape) { return { mitte: { id: 'mitte', type: 'vertex', x: 0, y: shape.props.abstand / 2, // Handle mittig zwischen den Kreisen canBind: false, } } }, onHandleDrag(shape, { handle, delta }) { return { props: { // Nur abstand ändern, x-Bewegung ignorieren, Minimum erzwingen abstand: Math.max(20, shape.props.abstand + delta.y * 2) } } }, ``` ### 6.3 Resize einschränken (nur Höhe) ```typescript onResize(shape, info) { return { props: { abstand: Math.max(20, info.newPoint.y * 2) } } }, getResizeSnapGeometry(shape) { return { points: [[0, 0], [0, shape.props.abstand]] } }, ``` ### 6.4 SVG-Rendering (reagiert auf `abstand`) ```typescript component(shape) { const { abstand, radius } = shape.props const r = radius return ( {/* Oberer Kreis */} {/* Unterer Kreis – position hängt von abstand ab */} {/* Zwei vertikale Verbindungslinien */} ) } ``` ### 6.5 Freiheitsgrade im Überblick | Aktion | Erlaubt | |---|---| | Kreisel verschieben | ✓ | | Abstand der Kreise ändern | ✓ (Handle mittig) | | Breite / Radius ändern | ✗ gesperrt | | Drehen | konfigurierbar | | Im JSON-Export | `"abstand": 120` | --- ## 7. FastAPI-Backend (3 Endpunkte) ```python from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() # 1. Symbolbibliothek ausliefern @app.get("/symbols") async def get_symbols(): with open("catalog/symbols.json") as f: return json.load(f) # 2. Koordinaten normalisieren / bereinigen class SketchPayload(BaseModel): shapes: list[dict] grid_size: int = 10 @app.post("/normalize") async def normalize(payload: SketchPayload): # Snap-Koordinaten auf Grid runden, SVG-Pfade normalisieren ... return {"svg": normalized_svg, "shapes": cleaned_shapes} # 3. Finales Projektfile exportieren @app.post("/export") async def export(payload: SketchPayload): return { "json": build_project_json(payload.shapes), "svg": build_final_svg(payload.shapes) } ``` --- ## 8. Umsetzungsplan (Phasen) | Phase | Inhalt | Aufwand | |---|---|---| | 1 | tldraw als PWA auf Hetzner (nginx + Docker) | 1 Tag | | 2 | Symbolbibliothek JSON + SVG-Pfade für 10 Symbole | 1–2 Tage | | 3 | Custom Shape Types in tldraw registrieren | 1 Tag | | 4 | FastAPI-Backend mit 3 Endpunkten | 1 Tag | | 5 | Parametrische Handles (Kreisel etc.) | 1 Tag | | 6 | Export → JSON + SVG finalisieren | 1 Tag | **Geschätzter Gesamtaufwand Prototyp: ~1 Woche** --- ## 9. Entscheidungsmatrix: `meta` vs. Custom Shape | Kriterium | `meta`-Feld | Custom Shape Type | |---|---|---| | Implementierungsaufwand | minimal | mittel | | Eigenes SVG-Rendering | ✗ | ✓ | | Typvalidierung der Props | ✗ | ✓ | | Parametrische Handles | ✗ | ✓ | | Im JSON-Export enthalten | ✓ | ✓ | | Empfehlung | Prototyp Phase 1 | Produktiv ab Phase 3 | --- *Erstellt mit Claude (Anthropic) – Technologiegespräch vom 09.04.2026*