7.3 KiB
7.3 KiB
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)
{
"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)
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)
const WeicheShapeUtil = defineShape({
type: 'weiche',
props: {
radius: T.number,
direction: T.string,
ports: T.arrayOf(T.string),
svg_path: T.string,
},
component(shape) {
return (
<SVGContainer>
<path d={shape.props.svg_path} stroke="black" fill="none" />
</SVGContainer>
)
},
getBounds(shape) { /* ... */ }
})
JSON-Export einer Custom Shape:
{
"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
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)
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)
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)
component(shape) {
const { abstand, radius } = shape.props
const r = radius
return (
<SVGContainer>
{/* Oberer Kreis */}
<circle cx={0} cy={0} r={r} stroke="black" fill="none" />
{/* Unterer Kreis – position hängt von abstand ab */}
<circle cx={0} cy={abstand} r={r} stroke="black" fill="none" />
{/* Zwei vertikale Verbindungslinien */}
<line x1={-r} y1={0} x2={-r} y2={abstand} stroke="black" />
<line x1={ r} y1={0} x2={ r} y2={abstand} stroke="black" />
</SVGContainer>
)
}
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)
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