Semantik aus PLAN.md „Map-Größe ändern" (17.10., Sly-geankert):
- Anker Mitte: Karte bleibt am Ursprung zentriert, bestehendes Terrain behält
seine WELTPOSITION (bilinear resampeln, kein Stretch).
- Neue Randzone: flach auf min_height + Material 0 (Gras), wie create_new.
- Dichte-Regel: würde m/Punkt über ~2 sinken, res hoch 129→257→513
(Format-Limit 513 hart — darüber Warnband, Anzeige in R3).
- size clamp 256–4096 m (MIN_SIZE_M/MAX_SIZE_M), bounds auf neue Größe
(Y-Grenzen unverändert).
- R1-Schnitt: spawn_zones/objects/gebaeude unverändert übernommen (Filter +
Out-of-Bounds-Dialog kommen in R2). Reine Funktion — Eingabe-doc bleibt
unangetastet (Undo-Snapshot), alte Prüfsumme verworfen (save signiert neu).
Tests (test_terrain.gd, 22 neue Checks → Suite 134/134, 11 Suiten ALL OK):
Grow 256→512 — alte Gridpunkte ±0.01 m an Weltposition, Rand flach/Gras,
Material resampelt, res-Bump 129→257 (bei 2048 m gedeckelt auf 513),
spawn/object-Erhalt, svmap-v2-Roundtrip, Clamp.
Root-Cause 1 (Spiegelung): terrain_mesh.gd positionierte die Mesh-Vertices
gespiegelt (+0.5 − index·cell in X UND Z), während ALLE anderen Verbraucher
der Heightmap der Dokument-Konvention folgen (Index 0 = −X/−Z-Ecke):
SvmapDocument.terrain_height_at, TerrainField.height_at/apply_brush und der
Match-Client (svmap_terrain.gd). Der Renderer war der einzige Ausreißer —
der Pinsel-Strahl traf das gerenderte Mesh punktgespiegelt um die Map-Mitte
(Sly-Befund: Erhöhung erschien an ganz anderer Stelle). Fix: Vertices auf
−size*0.5 + index*cell (identisch zum Client), Normalen-Vorzeichen angepasst
(n ∝ (−∂h/∂x, 1, −∂h/∂z)); Winding bleibt gültig (180°-Rotation erhält die
Orientierung, Kreuzprodukt-Normale weiter nach unten = Frontseite).
Root-Cause 2 (Tangenten-Graze): Ein Strahl, der exakt auf eine Hügelkuppe
zielt, grast die gewölbte Oberfläche — f(t) bleibt minimal knapp über 0 und
steigt danach wieder, obwohl weiter hinten (VERDECKT hinter dem Hügel) noch
ein Vorzeichenwechsel an der Basis-Ebene lag. Der March lief durch und der
Pinsel sprang auf den verdeckten Basis-Punkt meterweit weg. Fix in
TerrainField.raycast: Regel 'erster sichtbarer Oberflächen-Kontakt gewinnt' —
kommt f innerhalb des Bandes (½ March-Schritt) an die Oberfläche und entfernt
sich der Strahl wieder, wird das Graze-Minimum lokal per Ternärsuche
verfeinert und als Treffer zurückgegeben. Echter Treffer (f ≤ 0) gewinnt
weiterhin immer; betrifft auch Bauen-Werkzeug und Spawn-Zonen (gleiche
Raycast-Routine).
Tests (test_terrain.gd, 64 → 86 Checks): Neue Gruppe 'Ray→Terrain-Mapping'
mit simulierter Kamera (SubViewport, schief + steil): exakter Screen-Punkt
(unproject) → project_ray → raycast → asserted Treffer-Zelle == Cursor-Zelle
(< 1 Zelle) auf Mitte/Innenraum/Corner/Kanten; Brush-Stroke am Treffer mit
Höhen-Änderungs-Zentrum == erwarteter Zelle; erhabenes Terrain (Strahl trifft
NEUE Oberfläche, y = Hügelhöhe); Mesh-Vertices komplett gegen die
Dokument-Konvention asserted (Root-Cause-Regression). Hügel-Normalen-Checks
auf die korrigierte Konvention umgestellt (lehnt jetzt −X/−Z).
Suite: 7 Editor-Suiten 463 Checks grün + Export 21/0 + GUI-Smoke 20 grün.
TerrainField (src/svmap/terrain_field.gd): Höhenfeld-Mathematik auf dekodiertem
PackedFloat32Array — Pinsel mit Kosinus-Falloff (RAISE/LOWER/SMOOTH, zweiphasiges
3x3-Glättung), R16-Encode (uint16 LE row-major), ray-march+Bisektion-Cursor auf
dem GELÄNDE (nicht Basis-Ebene). EDITOR_MAX_RESOLUTION=129 (Editor-MVP-Perf-Limit,
Format bleibt 17-513 für Interop).
TerrainMeshView (src/viewport/terrain_mesh.gd): Höhenfeld als ArrayMesh mit
Höhen-Farbverlauf (Vertex-Farben); update_heights schreibt Vertices live neu
während des Strichs (res 129 = 16,6k Punkte, flott). Ersetzt die flache
Boden-Plane in map_view_builder (rebuild + rebuild_terrain_from_doc + update_terrain).
TerrainTool (src/editor/terrain_tool.gd): Pinsel-Modus im Viewport — Linksklick-
Ziehen formt (Heben/Senken/Glättten nach Panel-Wahl), Rechtsklick-Ziehen das
Gegenteil (kein Umschalten nötig). Pinsel-Ring (Torus) folgt dem Cursor auf dem
Terrain, farbcodiert pro Modus. GANZER Strich = EIN ActionHistory-Eintrag
(snapshot terrain VOR dem Strich, commit NACH dem Encode). Range-Expansion:
min/max_height_m weiten sich automatisch auf, wenn der Pinsel drüber hinaus will
(sonst wäre Senken auf flacher Map unmöglich) — Spannweite ≤ 500 m (V6), Undo
stellt die Range mit zurück. Doc wird nur ZUM Strich-Ende verändert.
main.gd: Werkzeug-Button 'Terrain formen' (groß, deutsch), Terrain-Panel mit
Heben/Senken/Glättten-Buttons + Pinsel-Größe/Stärke-Slider mit Live-Werten,
Modus-Wechsel objects<->terrain (Bibliothek-Klick wechselt zurück, ESC beendet),
interaction.gd ruht im Terrain-Modus. Performance-Zähler in der Statusleiste:
Terrain-Grid + Dreiecke (+ % vom Quest-3-Budget 524.288) + Editor-FPS.
Demo-Hook --terrain-demo [ordner]: 5 Szenarien inkl. Undo-Roundtrip als PNGs.
Neue Maps: res 129 (vorher 257) — erfüllt das Editor-MVP-Perf-Limit automatisch.