docs: Umsetzungsplan für Flamingo Park, Spec beim Planen geschärft

Beim Nachrechnen korrigiert: affine statt bilinearer Einpassung (bilinear
verfehlte den Hood Yard um 4,2 m), offset_u 6,25 m, gemessenes Schnitt-Vieleck
der Gasse, Mittellinienstücke erst ab 15 m, Tribüne ohne replaces, Startpunkt
als eigenes Modul, PYTHONUTF8=1 unter Windows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-15 23:57:30 +02:00
1 parent c278ad3d9d
commit c6241db90f
2 files changed
+2391 -18

No files matched your search

@@ -115,19 +115,19 @@ Bei D bleibt östlich der Anlage ein 37 m breiter Rasenstreifen.
### Parkdatei (`tools/extract/data/flamingo-park.toml`)
Alles, was von Hand entschieden ist. Die Werte unten sind die Werte der Umsetzung; nur `offset_u` wird beim Umsetzen mit der bilinearen Umrechnung neu eingestellt (siehe Einpassung).
Alles, was von Hand entschieden ist. Die Werte unten sind die Werte der Umsetzung.
```toml
# Flamingo Park nach OpenStreetMap. Spec: docs/superpowers/specs/2026-09-15-flamingo-park-design.md
osm = "osm/flamingo-park.json"
[fit]
# Innenkante des echten Parks, Breite/Länge in Grad
real = { sw = [25.781905, -80.138373], se = [25.781960, -80.136373], ne = [25.786554, -80.136523], nw = [25.786524, -80.138527] }
# Innenkante des echten Parks, Breite/Länge in Grad. Die Umrechnung ist affin über diese drei Ecken.
real = { sw = [25.781905, -80.138373], se = [25.781960, -80.136373], nw = [25.786524, -80.138527] }
# Innenkante der Straßen um den Park im Spiel, Welt-x/Welt-y wie auf der Minimap
game = { sw = [1418.3, 658.6], se = [1661.5, 672.8], ne = [1626.0, 1096.0], nw = [1382.1, 1074.2] }
scale_u = 1.0 # quer wie in echt
offset_u = 6.3 # Meter vom Westrand; echte Tribüne und Kleinbauten auf Tribüne und Hood Yard
offset_u = 6.25 # Meter vom Westrand; echte Tribüne und Kleinbauten auf Tribüne und Hood Yard
[[anchor]] # Prüfung der Einpassung, höchstens 1,5 m Abweichung
name = "Tribüne"
@@ -162,19 +162,22 @@ from = [1516.5, 664.0]
to = [1545.2, 334.2]
width = 12.0
remove_buildings = [[1513.0, 628.0], [1532.0, 426.0]]
# Gassenstreifen zwischen den Kanten von 11th Street und der Querstraße bei y ≈ 332, samt der
# ausgerundeten Ecken an beiden Einmündungen. Wird beim Planen einmal aus den Chunk-Daten gemessen.
alley_cut = [[x, y], ...]
# Gassenstreifen zwischen 11th Street (Fahrbahnkante y 644,5–645,5) und der Querstraße bei y ≈ 332
# (Fahrbahnkante y 339,5), gemessen an den Chunk-Daten. Die ausgerundeten Ecken bleiben bis auf je
# einen Streifen von 1–3 m stehen; die Fahrbahnen der Querstraßen berührt das Vieleck nicht.
alley_cut = [[1470, 644], [1496, 644], [1500, 600], [1506, 540], [1514, 460], [1520, 380], [1520, 340.5],
[1497, 340.5], [1486, 380], [1480, 460], [1474, 540], [1468, 600]]
```
**Prüfregeln beim Einlesen:** Fehlt ein Pflichtfeld, hat ein Feld den falschen Typ, fehlt die OSM-Datei oder ist eine Breite oder Höhe ≤ 0, bricht `run.py` mit einer Meldung ab, die Datei und Feld nennt. Fehlt die OSM-Datei, nennt die Meldung den Aufruf von `osm.py`.
## Einpassung
- **Echte Lage:** Jeder OSM-Punkt wird zuerst in lokale Meter umgerechnet (Bezugspunkt und Maß wie im Befund). Danach bekommt er Parkkoordinaten (s, t), mit der Umkehrung der bilinearen Abbildung über die vier echten Ecken: s läuft von West nach Ost, t von Süd nach Nord. Das echte Parkviereck ist s, t ∈ [0, 1].
- **Lage im Spiel:** Quer wird s zu s' = (offset_u + scale_u · s · B_echt) / B_Spiel umgerechnet, mit B_echt = 200,6 m und B_Spiel = 243,6 m, jeweils entlang der Südkante. Längs bleibt t. Der Spielpunkt ist die bilineare Abbildung von (s', t) über die vier Spielecken. Die Nordkante des Parks folgt so auch der schrägen Nordkante im Spiel.
- **Echte Lage:** Jeder OSM-Punkt wird zuerst in lokale Meter umgerechnet (Bezugspunkt und Maß wie im Befund). Danach bekommt er Parkkoordinaten (s, t) aus p − SW = s · (SE − SW) + t · (NW − SW), über die echten Ecken SW, SE und NW: s läuft von West nach Ost, t von Süd nach Nord. Das echte Parkviereck ist s, t ∈ [0, 1].
- **Lage im Spiel:** Quer wird s zu s' = (offset_u + scale_u · s · B_echt) / B_Spiel umgerechnet, mit B_echt = |SE − SW| echt (200,57 m) und B_Spiel = |SE − SW| im Spiel (243,61 m). Der Spielpunkt ist SW + s' · (SE − SW) + t · (NW − SW) über die Spielecken.
- **Warum affin und nicht über alle vier Ecken:** Eine bilineare Umrechnung folgte der steileren Nordkante im Spiel und schöbe den Hood-Yard-Anker 4,2 m nach Norden; beide Anker gleichzeitig unter 1,5 m zu bringen, geht damit nicht. Die affine Umrechnung ist genau die Variante D aus dem Vergleichsbild. Folge: Die Anlage endet im Nordosten bis zu 6,4 m vor der Nordstraße; der Rasen füllt das ganze Parkpolygon, es bleibt also kein nackter Boden.
- **Länge:** Sie ergibt sich aus den Ecken, 417,2 / 514,4 ≈ 0,811.
- **`offset_u`:** Der Wert wird so gewählt, dass beide Anker aus der Parkdatei höchstens 1,5 m neben ihrer Spielposition landen. Die 6,3 m aus dem Befund sind mit affiner Näherung gemessen, die Umsetzung stellt den Wert bilinear neu ein.
- **`offset_u`:** 6,25 m. Damit landen die echte Tribüne 1,19 m und die echten Kleinbauten 1,21 m neben Tribüne und Hood Yard. Ein Test hält beide unter 1,5 m.
- **Zuschneiden:** Wege werden am echten Parkviereck abgeschnitten. Endet ein Weg danach höchstens 5 m vor dessen West- oder Ostrand, läuft er im Spiel in seiner letzten Richtung bis an die Straßenkante weiter. Flächen und Gebäude zählen nur, wenn ihr Mittelpunkt im echten Parkviereck liegt.
## Anwendung in der Pipeline (`tools/extract/leonida/park.py`)
@@ -188,7 +191,7 @@ alley_cut = [[x, y], ...]
1. **Gebäude:** Jedes Gebäude, in dem ein Punkt aus `remove_buildings` liegt, entfällt.
2. **Gasse:**
- Das Vieleck `alley_cut` wird aus der Straßenfläche geschnitten. Es reicht von der Kante der 11th Street bis zur Kante der Querstraße bei y ≈ 332 und schließt die ausgerundeten Ecken der Einmündungen ein, aber keine Fahrbahn der Querstraßen.
- Die Mittellinienstücke in `alley_cut` entfallen.
- Die Mittellinienstücke in `alley_cut` entfallen, ebenso Reststücke unter 15 m.
3. **Neue Straße:**
- Ein Rechteck von `width` Breite von `from` nach `to` wird mit der Straßenfläche vereinigt, `bridge = false`. Die Querstraßen sind danach durchgehend.
- Dazu kommt eine Mittellinie von `from` nach `to`.
@@ -200,7 +203,7 @@ alley_cut = [[x, y], ...]
Das Parkpolygon ist das Viereck aus den Spielecken.
**Entfernen**
- Straßenflächen werden am Parkpolygon abgeschnitten, Mittellinien ebenso. Stücke unter 1 m entfallen.
- Straßenflächen werden am Parkpolygon abgeschnitten, Mittellinien ebenso. Mittellinienstücke unter 15 m entfallen, wie in `centerlines()` der Extraktion; kürzere Stummel setzten sonst Laternen mitten auf die Kreuzung an der Parkkante. Straßenstücke unter 20 m² entfallen, wie in `road_polygons()`.
- Beläge (`sand`, `park`, `urban`) werden am Parkpolygon abgeschnitten.
- Gebäude mit mehr als der Hälfte ihrer Fläche im Park entfallen.
- Palmen im Park entfallen.
@@ -256,7 +259,8 @@ Das Parkpolygon ist das Viereck aus den Spielecken.
- `POLYGON_OFFSET_ORDER` wird zu `ground`, `urban`, `park`, `sand`, `field`, `court`, `pool`, `parking`, `paths`, `roads`.
- Die Regel, dass jeder Schlüssel eine Textur hat, gilt künftig nur für texturierte Schlüssel. `court` und `pool` stehen in einer eigenen Farbliste.
- **`src/world/streaming.js`:** `DECALS` bekommt `paths`, `field`, `court`, `pool` und `parking`. Das sind reine Auflagen ohne eigene Kollision, wie heute Straßen und Beläge. Die Landplatte trägt, Gebäude kollidieren wie alle anderen. Man läuft also auch über den Pool.
- **`tools/assets/textures.mjs`:** `SLUGS` bekommt `concrete_pavement` (Poly Haven, CC0, 1k). Die Textur ist auch für die Gehwege in Teil 2 gedacht.
- **`tools/assets/textures.mjs`:** `SLUGS` bekommt `concrete_pavement` (Poly Haven, CC0, 1k). Die Textur ist auch für die Gehwege in Teil 2 gedacht. Vorhandene Dateien lädt das Skript nicht erneut, damit sich die übrigen Texturen nicht still ändern.
- **`src/world/places.js`:** Die Tribüne verliert `replaces`. Den alten Gebäudekasten entfernt künftig schon die Pipeline, weil er zu mehr als der Hälfte im Park liegt; ein `replaces`-Punkt träfe dann kein Gebäude mehr, und `placesSite.test.js` schlüge an. `withoutReplacedBuildings` bleibt als Mechanismus für spätere Orte.
- **Kosten:** Bis zu fünf Draw Calls mehr, nur in den Chunks von Park und Jefferson Avenue (J6, K6, J7, K7, J8, K8, J9, K9).
## Minimap (`tools/extract/overview.py`)
@@ -269,8 +273,8 @@ Das Parkpolygon ist das Viereck aus den Spielecken.
1. **Branch:** Die Arbeit läuft auf `leonida/flamingo-park`, `main` bleibt bis zur Abnahme unberührt.
2. **Pipeline auf Windows nachbauen:**
- `cd tools/extract && uv sync`. `uv` lädt dafür Python 3.12 und die Pakete aus `uv.lock`.
- Danach `uv run python run.py` mit unverändertem Code.
- `cd tools/extract && uv sync --python 3.12`. `uv` lädt dafür Python 3.12 und die Pakete aus `uv.lock`.
- Danach `PYTHONUTF8=1 uv run python run.py` mit unverändertem Code. `PYTHONUTF8=1` ist unter Windows nötig: Python 3.12 liest Textdateien dort sonst als cp1252, und `annotations.toml` ist UTF-8.
- Verglichen werden alle Chunk-Dateien und `manifest.json` als geparstes JSON, dazu `map.png` und `map-walkable.png` Pixel für Pixel sowie `map.json`.
- Weicht etwas ab, geht es erst weiter, wenn die Ursache geklärt und mit dem Nutzer entschieden ist.
3. **OSM-Daten:** `osm.py` schreiben, einmal abrufen, dazu `flamingo-park.toml` anlegen.
@@ -284,8 +288,8 @@ Das Parkpolygon ist das Viereck aus den Spielecken.
### pytest (`tools/extract/tests/test_park.py`)
- **Einpassung:**
- Südkante (t = 0) und Nordkante (t = 1) des echten Parks landen auf Süd- und Nordkante im Spiel.
- s = 0 landet `offset_u` Meter östlich der Westkante.
- Die echte SW-Ecke landet `offset_u` Meter östlich der Spiel-SW-Ecke auf der Südkante, die echte NW-Ecke ebenso weit östlich der Spiel-NW-Ecke.
- Die echte Südkante bleibt 1:1 lang (200,57 m).
- Beide Anker liegen höchstens 1,5 m neben ihrer Spielposition.
- **Zuschneiden und Verlängern:**
- Ein Weg über den echten Parkrand endet an der Kante.
@@ -313,7 +317,7 @@ Das Parkpolygon ist das Viereck aus den Spielecken.
- **`tests/streaming.test.js`:** Die neuen Schichten landen in der Szene, aber nicht in der Kollisionsgruppe.
- **`tests/placesSite.test.js`:** Mit den neuen Daten bleibt der Test grün. Neu: Unter Tribüne und Hood Yard liegt kein Punkt einer `paths`-Fläche.
- **Neuer Datentest:** Die `keepout`-Einträge in `flamingo-park.toml` stimmen in `at`, `rotation` und `bounds` mit `PLACES` überein. Der Test liest die wenigen Felder aus der TOML-Datei.
- **Startpunkt:** `SPAWN` in `src/main.js` (1600 | 690) liegt danach im Park. Ein Datentest prüft, dass er auf Land und in keinem Gebäude liegt; die vier Fahrzeugpunkte liegen weiter auf einer Straßenfläche.
- **Startpunkt:** `SPAWN` (1600 | 690) liegt danach im Park. Ein Datentest prüft, dass er auf Land und in keinem Gebäude liegt; die vier Fahrzeugpunkte liegen weiter auf einer Straßenfläche. Damit der Test die Punkte lesen kann, wandern `SPAWN` und die Fahrzeugpunkte aus `src/main.js` in ein kleines Modul `src/world/spawn.js`; `main.js` lässt sich wegen seiner Ladeschritte auf oberster Ebene nicht in Tests importieren.
### Sichtprüfung