Files
plantplan/doc/tldraw_technologie.md
2026-04-13 14:45:27 +02:00

7.3 KiB
Raw Permalink Blame History

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 12 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