# Pipeline: OSM → STL/3MF

Komplett im Browser. Dieses Dokument ist gleichzeitig die öffentliche
„Methode der Veränderung" im Sinne von ODbL 4.6 (siehe LIZENZEN.md) — bei
Änderungen aktuell halten.

```
Ausschnitt wählen ─► Daten holen ─► GeoJSON ─► Projektion (m → mm)
      │                                              │
  (Leaflet)            (Overpass + Cache)            ▼
                                        Klassifizieren (Straße/Gebäude/…)
                                                     │
                                                     ▼
                              2D: Linien puffern, Flächen vereinigen,
                                  Mindestbreiten, auf Rahmen clippen
                                                     │
                                                     ▼
                              3D: pro Klasse extrudieren, Basis + Rahmen
                                                     │
                                                     ▼
                              Vorschau (three.js) ─► Export STL / 3MF
```

## 1. Ausschnitt

- Leaflet-Karte, darüber ein Rahmen im Seitenverhältnis der Druckplatte
  (rechteckig, quadratisch, später rund/Hexagon — Formen aus stl_sign).
- Nutzer gibt **Druckgröße in mm** vor; die Karte bestimmt die Fläche in m
  → daraus Maßstab (`mmPerMeter`). Anzeige z. B. „1 : 12 500".
- Ergebnis: Mittelpunkt, Breite/Höhe in m, Drehung (optional, später).
- Ortssuche über Nominatim (nur Positionierung der Karte, keine Kartendaten fürs Modell); Koordinaten werden lokal erkannt.

## 2. Daten holen

- Overpass-Abfrage (DATENQUELLEN.md) für die bbox **+ Rand von ~5 %**, damit
  Features am Rand sauber abgeschnitten werden statt zu fehlen.
- Cache in IndexedDB. Neue Abfrage nur bei geändertem Ausschnitt.
- Modul-Schnittstelle: `fetchFeatures(bbox) → GeoJSON FeatureCollection`
  (austauschbar gegen eigene Instanz / PMTiles).
- Fortschritt: Cache → Anfrage an Server i/n → empfangene Bytes → Lesen; Abbrechen möglich. Beim Öffnen eines Projekts/Links wird nur der Cache benutzt, keine Netzanfrage ohne Klick.

## 3. Projektion

Lokale equirektanguläre Näherung um den Mittelpunkt (φ0, λ0):

```
x_m = R · (λ − λ0) · cos φ0
y_m = R · (φ − φ0)            R = 6 371 008.8 m
x_mm = x_m · mmPerMeter
```

Fehler bei ≤ 10 km Ausschnitt vernachlässigbar (< 0,1 %). Keine Library nötig.

## 4. Klassifizieren

Jedes Feature bekommt genau eine Klasse. Reihenfolge bei Konflikten (oben
gewinnt): Gebäude > Straßen/Wege > Bahn > Wasser > Grün > Basis.

| Klasse | OSM-Tags | Geometrie |
|---|---|---|
| `road_major` | highway = motorway, trunk, primary, secondary (+ `_link`) | Linie |
| `road_minor` | tertiary, unclassified, residential, living_street, service, road | Linie |
| `path` | footway, path, cycleway, pedestrian (Linie), steps, track, bridleway | Linie |
| `rail` | railway = rail, light_rail, tram, subway, narrow_gauge | Linie |
| `building` | building=* (außer `no`) | Fläche |
| `water` | natural=water, waterway=riverbank, landuse=reservoir/basin | Fläche |
| `waterway` | waterway = river, stream, canal | Linie |
| `green` | leisure=park/garden/pitch, landuse=grass/forest/meadow/recreation_ground/village_green, natural=wood/scrub | Fläche |

Ausschließen: alles, was in keine der obigen Klassen fällt (u. a.
`highway=proposed/construction/platform`); `area=yes`-Straßen werden als
Fläche behandelt. `tunnel=yes`/`culvert` und `layer<0` werden nur gemeinsam
gefiltert, und nur wenn die Option „Tunnel zeigen" aus ist (Default: an, d. h.
Tunnel und `layer<0` erscheinen normal wie oberirdische Objekte). Brücken: v1
flach wie normale Straßen.

Multipolygone: Geschlossene Mitgliedswege sind direkt Ringe. Wegstücke mit
einem losen Ende (am Abfragerand abgeschnitten, Ausläufer) werden vorab
aussortiert. Die übrigen werden per Tiefensuche zu Ringen geschlossen; an
Verzweigungen wird eine Sackgasse zurückgenommen (Budget 20 Schritte je
Wegstück und Relation, danach gierig). Wegstücke ohne Ring und Relationen
ohne äußeren Ring werden verworfen und in der Oberfläche gezählt.

## 5. 2D-Geometrie (mm, Clipper)

Alles in 2D lösen, erst am Ende extrudieren — 2D-Booleans sind schnell und
robust, 3D-Booleans teuer.

1. **Linien puffern:** Breite = `max(realeBreite_m · mmPerMeter · Faktor,
   minBreite_mm, Düsendurchmesser)`. Reale Breite: `width`-Tag, sonst
   `lanes × 3,5 m`, sonst Default pro Typ (siehe PARAMETER.md). Enden rund,
   Ecken rund.
2. **Vereinigen pro Klasse** (Straßen-Netz wird ein zusammenhängendes Polygon;
   die Vereinigung ergibt sich implizit, weil Clipper alle Teilflächen einer
   Klasse gemeinsam mit der Non-Zero-Füllregel verarbeitet).
3. **Auf Rahmen clippen** (Innenkante des Rahmens) und **Prioritäten
   abziehen:** höchste Priorität zuerst, jede Klasse behält, was ihr zusteht,
   tiefere Klassen bekommen den Rest (Gebäude > Hauptstraßen > Nebenstraßen >
   Wege > Bahn > Wasserläufe > Wasser > Grün).
4. **Mindestgrößen / dünne Stege:** Morphologisches Öffnen mit Radius
   `0,45 × Düsendurchmesser` (schrumpfen, dann wieder wachsen; das Ergebnis
   wird zusätzlich mit der Ausgangsform geschnitten, damit nichts über sie
   hinauswächst) entfernt Stege und Spitzen, die eine Düse nicht drucken kann.
   Danach werden verbliebene Teilflächen kleiner als (Düse)² und Löcher
   kleiner als (Düse)² verworfen.
5. **Vereinfachen:** `CleanPolygons` (Clipper) mit Toleranz `print.simplify`
   (Default 0,05 mm) entfernt Punkte, die näher als die Toleranz beieinander
   liegen oder praktisch auf einer Linie liegen. Das ist keine echte
   Konturvereinfachung (kein Douglas-Peucker), sondern Rauschunterdrückung
   für die Booleans — angewendet sowohl beim Einlesen der Flächen als auch
   nach dem Zuschnitt auf den Rahmen.
6. **Trennfuge (SEPARATION):** jede fertige Klassenfläche wird zusätzlich um
   0,0005 mm (0,5 µm) nach innen versetzt, bevor sie als „beansprucht" für
   tiefere Klassen gilt. Ohne diesen Versatz berühren sich benachbarte
   Teile/Löcher exakt an einer Kante oder einem Punkt; die Extrusion würde
   dort eine Kante erzeugen, die zu vier Flächen gehört — nicht mehr
   manifold. Der Versatz kostet 0,01–0,23 % Fläche, macht die Netze aber ohne
   3D-Boolean (manifold-3d) wasserdicht.

7. **Markierte Gebäude** (vor allen Klassen): vom Nutzer per Klick gewählte
   Gebäude (OSM-IDs in `marks.ids`) verlassen ihre Klasse und werden eigene
   Teile in der Markierungsfarbe. Das Teil behält die echte Grundfläche
   (auf die Kartenfläche minus Spalt zugeschnitten, geöffnet wie oben,
   SEPARATION); um jedes Teil bleibt ein Spalt `marks.gap` frei
   (Keepout = Teil + Spalt). Der Keepout wird zuerst „beansprucht“, d. h.
   keine andere Klasse steht im Spalt – Nachbarhäuser einer Häuserzeile
   verlieren diesen Streifen. Benachbarte markierte Gebäude halten den Spalt
   auch untereinander. Zu kleine Teile werden als „zu klein“ gemeldet,
   IDs, die im Ausschnitt fehlen, als „nicht im Ausschnitt“.

## 6. 3D

- Pro Klasse: 2D-Polygone → Solid mit Unterkante = Oberkante Basis (deckungs-
  gleich, kein Eintauchen in die Basis) und Oberkante = Basis + Klassenhöhe.
  Alle Körper werden zwischen zwei Flächen extrudiert (`extrudeBetween`); die
  Plattenoberkante ist die Funktion `ground(x, y)` (`makeGround`, in v1
  konstant `plate.base`). Phase 2 ersetzt nur diese Funktion.
- Gebäude: pro Gebäude eigene Höhe möglich (Modus „echt"), sonst einheitlich.
- **Platte als Bänderstapel:** Inlay-Taschen und Wassermulde werden von oben,
  die Prägung von unten in die Platte geschnitten; jedes Band zwischen zwei
  Schnitttiefen ist Plattenumriss minus alle hineinreichenden Schnitte
  (bereinigt wie Inlay-Oberseite). Unter jeder Vertiefung bleiben 0,6 mm
  (Prägung zuerst, dann Mulde, dann Taschentiefe). **Vertieftes Wasser:**
  Wasser und Wasserläufe liegen 0,6 mm unter der Oberkante; bei
  Multi-Material füllt ein Wasserkörper (0,4 mm, auf Layer gerundet) die
  Mulde, bei Höhenstufen bleibt eine Mulde in Plattenfarbe, bei Inlay mit
  Wasser als Inlay-Klasse wird nicht vertieft. **Prägung Unterseite:** eigene
  Beschriftung (bis 3 Zeilen) und „© OpenStreetMap contributors /
  openstreetmap.org/copyright“ (sonst vierzeilige Kurzform; unter 12,5 ×
  Düse Versalhöhe „(c)“ statt „©“, weil der ©-Ring sonst nicht druckbar ist),
  Roboto Bold, Versalhöhe max(3 mm, 7,5 × Düse) bis Schrifthöhe, Tintenbox
  (mit Unterlängen) 4 mm vom Rand, gespiegelt, 0,4 mm tief; Glyphen werden
  wie Kartenflächen bereinigt (Punkte zusammenfassen, Öffnen mit 0,45 × Düse,
  Teile/Löcher < Düse² weg, SEPARATION).
- **Markierte Gebäude:** Der Keepout wird als Tasche (Tiefe `marks.depth`,
  höchstens bis 0,6 mm Restboden) von oben in die Platte geschnitten; das
  Teil reicht bis `FLOOR_GAP` (0,1 mm) über den Taschenboden und ist so hoch
  wie das Gebäude (einheitlich oder echt skaliert). Passt keine Tasche
  (≥ 0,4 mm) mehr in die Basis, wird das Teil im Spaltring aufgeklebt.
  Unabhängig vom Farbmodus (bei Höhenstufen nicht Teil der Stufen).
- Basisplatte + Rahmen als eigene Bodies; optional Beschriftung (aus stl_sign).
- **Technik:** three.js `ExtrudeGeometry` pro Klassenfläche — korrekt, weil
  die 2D-Flächen aus Schritt 5 bereits überlappungsfrei, „StrictlySimple" und
  durch die Trennfuge (SEPARATION) sauber getrennt sind. Kein manifold-3d
  (WASM) nötig; die Wasserdichtheit kommt aus der 2D-Vorverarbeitung, nicht
  aus einem 3D-Boolean.
- Rechnen im **Web Worker**, damit die UI nicht hängt.

## 7. Export

- Wiederverwendung aus `stl_sign/js/export.js`: STL (binär), 3MF mit einem
  Objekt pro Farbe/Klasse, Zip-Writer.
- Drei Farbmodi (Entscheidung 2026-09-26, siehe PARAMETER.md „Farbmodus"):
  1. **Multi-Material:** 3MF, ein Body pro Farbe (Klassen gleicher Farbe
     zusammengefasst). Für AMS/MMU/Toolchanger.
  2. **Höhenstufen:** jede Farbe bekommt eine eigene, streng steigende
     Höhenstufe; das Tool staffelt automatisch, rundet auf Layerhöhe und
     listet die Wechsel-Layer (Filamentwechsel/Pause, jeder Drucker).
     Wechselhöhen werden absolut auf das Layerraster (ab z = 0) gerundet;
     eine Mindeststufe unter einer Layerhöhe zählt als eine Layerhöhe.
  3. **Inlay:** Logik aus `stl_sign/js/inlay.js` (`splitInlay`) übernehmen.
     Gewählte Klassen werden separat gedruckte Teile, die in Taschen der
     Platte gesteckt/geklebt werden. Freigang wird vom Teil abgezogen, Tasche
     = exakte Kontur, Boden-Luft `FLOOR_GAP`.
     Sinnvoll für **große zusammenhängende Flächen** (Wasser, Grün) und das
     **Straßennetz** (nach Union meist ein oder wenige große Teile).
     Nicht sinnvoll für Gebäude (hunderte Kleinteile) → Gebäude bleiben
     Plattenfarbe oder kommen über Multi-Material/Höhenstufen.
     UI muss Anzahl Inlay-Teile und verlorene Inseln (`lost`) anzeigen und
     Inseln unter Mindestfläche verwerfen bzw. mit Platte verschmelzen.
     Die Platten-Oberseite (Taschen ausgeschnitten) enthält an Stellen, wo
     zwei Taschen dicht beieinanderliegen, durch die Trennfuge (SEPARATION,
     siehe Abschnitt 5) Haarlinien-Stege unter 2× SEPARATION (1 µm) Breite;
     diese werden mit demselben morphologischen Öffnen wie in Schritt 5
     entfernt, bevor die Plattenoberseite extrudiert wird.
     Inseln, deren Zapfen beim Freigang verschwinden würde, bekommen keine
     Tasche und bleiben in Plattenfarbe auf der Platte.
- **Namen im Slicer:** Jedes 3MF-Teil heißt nach seiner Art und Farbe in der
  Sprache der Oberfläche, z. B. „Gebäude (#E07A3F)“, „Platte 2 (#F2EFE9)“,
  „Grün – Zapfen (#7FB069)“ (mehrere gleiche Teile werden nummeriert); das
  Objekt heißt wie das Projekt, Inlay-Drucke „Inlay N: <Klassen>“. STL-Namen
  enthalten die Klassen als ASCII, z. B. `Karte_05_Wege_Nebenstrassen_ffffff.stl`.
- Markierte Gebäude sind in jedem Farbmodus eigene Objekte (3MF: eigenes
  `item` „Markierung N: <Name> (#Farbe)“, auf z = 0 abgesenkt, neben der Karte aufgereiht;
  STL: eigene Datei im Zip).
- Attribution in 3MF-Metadaten bzw. `LICENSE.txt` im Zip (LIZENZEN.md).
- 3MF: Jedes Netz ist ein eigenes Teil (Komponente) des Objekts „map“,
  Farben sind Materialien; Netze gleicher Farbe werden nicht verschweißt,
  weil sich Rahmen und Plattenbänder berühren (sonst ‚non manifold edges‘
  im Slicer). Inlay: Platte und jede Teilfarbe sind eigene Objekte auf dem
  Bett.

## 8. Phase 2 vorbereiten (Relief)

`ground(x, y)` ist als Funktion umgesetzt (`makeGround` in `build3d.js`).
Für echtes Relief müssen die Flächen zusätzlich unterteilt werden
(Deckflächen sind heute nur an den Konturpunkten gestützt).
