Compare commits

..

2 commits

Author SHA1 Message Date
slyphax
fa2feee780 Thumbnails für die großen Dächer/Giebelwände (4 m, 6 m)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:32:53 +01:00
slyphax
3f0d0f3f33 Map-Editor: UI aufgeräumt, Auswahl-Sync, Drag&Drop, Bauen (Wände bündig, Geschosshöhe, große Dächer)
- Tabs Auswahl/Prüfen/Test-Start/Einstellungen/Symmetrie weg; Spawn-Zonen, Test-Start, Symmetrie im Map-Tab; Einstellungen-Fenster (Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe, Raster-Snap, Gizmo-Schritt, Karte prüfen)
- Bauen-Tab nur Bauteile; Objekt-Werkzeuge entfernt
- Klick in der Map-Inhalt-Liste wählt in der Szene aus (SELECT_MULTI feuert nur multi_selected)
- Objekt-Kachel per Ziehen in die Szene setzen
- Wände bündig in der Zelle, gleiche Geschosshöhe mit/ohne Decke, Dächer/Giebelwände 4 m und 6 m

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:32:32 +01:00
45 changed files with 1106 additions and 440 deletions

View file

@ -25,20 +25,28 @@ Ein Bauteil ist ein **ganz normales Bibliotheks-Objekt** (`bau_*`, Kategorie
| --- | --- |
| Fundament & Boden | Fundament, Boden/Decke (2 × 2 m, 20 cm) |
| Wände (2 m breit, 3 m hoch) | Wand, Wand mit Tür (1,2 × 2,2 m), Wand mit Fenster, Wand mit Schießscharte, Brüstung (1,1 m), Eckpfeiler |
| Dach & Giebel | Giebelwand, Satteldach, Pyramidendach |
| Dach & Giebel (klein 2 m) | Giebelwand, Satteldach, Pyramidendach |
| Dach & Giebel (mittel 4 m) | Giebelwand 4 m, Satteldach 4×4, Pyramidendach 4×4 (Spitze 2 m) |
| Dach & Giebel (groß 6 m) | Giebelwand 6 m, Satteldach 6×6, Pyramidendach 6×6 (Spitze 3 m) |
| Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) |
Dazu im Tab „Pack-Module“: Container, Hangar-Tore, Betonsperren aus dem
Military-Desert-Pack (normale Objekte).
Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab,
sondern wie alle Objekte in der Objekt-Bibliothek unten.
## Snap (`src/editor/build_snap.gd`)
* **XZ:** Wände auf die nächste Zellkante (Drehung ergibt sich aus der Kante, `R`
dreht die Wand um), Böden/Dächer/Fundamente in die Zellmitte, Pfeiler auf die
Zellecke, Treppen auf eine 4 × 2-m-Fläche. Zellen sind 2 × 2 m, global ab (0, 0).
* **XZ:** Wände auf die nächste Zellkante — und zwar GANZ in die Zelle, in der die
Maus ist (Außenseite bündig mit der Kante, kein Überhang, Ecken schließen ohne
Lücke). Klick auf eine vorhandene Wand (Seite oder Oberkante) übernimmt deren Lage
exakt (ersetzen bzw. stapeln). Böden/Dächer/Fundamente in die Zellmitte, große
Dächer (4 m / 6 m) je nach Zellzahl auf Zellecke bzw. Zellmitte, Pfeiler auf die
Zellecke, Treppen auf eine 4 × 2-m-Fläche. `R` dreht die Wand um. Zellen sind
2 × 2 m, global ab (0, 0).
* **Y:** nie die Klickhöhe, sondern eine **Ebene** — Gelände (höchster Punkt unter
der Grundfläche) oder Oberkante eines tragenden Teils darunter. Klick auf die
Wandoberkante stapelt das nächste Geschoss.
der Grundfläche) oder Oberkante eines tragenden Teils darunter. **Wände und
Pfeiler zählen von der Unterkante einer Decke/eines Fundaments**, die Decke selbst
liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder
ohne Decke darunter stehen die oberen Wände gleich hoch.
* **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene
Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt.

View file

@ -0,0 +1 @@
{"aabb_max":[2.071,2.141,2.0],"aabb_min":[-2.071,0.0,-2.0],"dreiecke":48,"oid":"bau_dach_giebel_4","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_giebel_4.glb","vertices":[[-2.0707,0.1414,-2.0],[-0.0707,2.1414,2.0],[-2.0707,0.1414,2.0],[-0.0707,2.1414,2.0],[-2.0707,0.1414,-2.0],[-2.0707,0.1414,2.0],[0.0707,2.0,2.0],[-1.9293,0.0,-2.0],[-1.9293,0.0,2.0],[-1.9293,0.0,-2.0],[0.0707,2.0,2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,-2.0],[-0.0707,2.1414,-2.0],[-0.0707,2.1414,2.0],[-0.0707,2.1414,-2.0],[-2.0707,0.1414,-2.0],[-0.0707,2.1414,2.0],[0.0707,2.0,-2.0],[-1.9293,0.0,-2.0],[0.0707,2.0,2.0],[-1.9293,0.0,-2.0],[0.0707,2.0,-2.0],[0.0707,2.0,2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,2.0],[-1.9293,0.0,-2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,-2.0],[-2.0707,0.1414,-2.0],[-2.0707,0.1414,2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,2.0],[0.0707,2.0,2.0],[-1.9293,0.0,2.0],[0.0707,2.0,2.0],[-2.0707,0.1414,2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,2.0],[-0.0707,2.1414,2.0],[0.0707,2.0,2.0],[-0.0707,2.1414,2.0],[-2.0707,0.1414,2.0],[0.0707,2.0,2.0],[-0.0707,2.1414,2.0],[0.0707,2.0,-2.0],[0.0707,2.0,2.0],[0.0707,2.0,-2.0],[-0.0707,2.1414,2.0],[0.0707,2.0,2.0],[-0.0707,2.1414,2.0],[-0.0707,2.1414,-2.0],[0.0707,2.0,-2.0],[-0.0707,2.1414,-2.0],[-0.0707,2.1414,2.0],[0.0707,2.0,-2.0],[-0.0707,2.1414,-2.0],[-1.9293,0.0,-2.0],[0.0707,2.0,-2.0],[-1.9293,0.0,-2.0],[-0.0707,2.1414,-2.0],[0.0707,2.0,-2.0],[-0.0707,2.1414,-2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,-2.0],[-2.0707,0.1414,-2.0],[-0.0707,2.1414,-2.0],[-1.9293,0.0,-2.0],[0.0707,2.1414,-2.0],[2.0707,0.1414,2.0],[0.0707,2.1414,2.0],[2.0707,0.1414,2.0],[0.0707,2.1414,-2.0],[0.0707,2.1414,2.0],[1.9293,0.0,2.0],[-0.0707,2.0,-2.0],[-0.0707,2.0,2.0],[-0.0707,2.0,-2.0],[1.9293,0.0,2.0],[-0.0707,2.0,2.0],[0.0707,2.1414,-2.0],[2.0707,0.1414,-2.0],[2.0707,0.1414,2.0],[2.0707,0.1414,-2.0],[0.0707,2.1414,-2.0],[2.0707,0.1414,2.0],[1.9293,0.0,-2.0],[-0.0707,2.0,-2.0],[1.9293,0.0,2.0],[-0.0707,2.0,-2.0],[1.9293,0.0,-2.0],[1.9293,0.0,2.0],[0.0707,2.1414,-2.0],[-0.0707,2.0,2.0],[-0.0707,2.0,-2.0],[-0.0707,2.0,2.0],[0.0707,2.1414,-2.0],[-0.0707,2.0,-2.0],[0.0707,2.1414,-2.0],[0.0707,2.1414,2.0],[-0.0707,2.0,2.0],[0.0707,2.1414,2.0],[0.0707,2.1414,-2.0],[-0.0707,2.0,2.0],[0.0707,2.1414,2.0],[1.9293,0.0,2.0],[-0.0707,2.0,2.0],[1.9293,0.0,2.0],[0.0707,2.1414,2.0],[-0.0707,2.0,2.0],[0.0707,2.1414,2.0],[2.0707,0.1414,2.0],[1.9293,0.0,2.0],[2.0707,0.1414,2.0],[0.0707,2.1414,2.0],[1.9293,0.0,2.0],[2.0707,0.1414,2.0],[1.9293,0.0,-2.0],[1.9293,0.0,2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,2.0],[1.9293,0.0,2.0],[2.0707,0.1414,2.0],[2.0707,0.1414,-2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,-2.0],[2.0707,0.1414,2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,-2.0],[-0.0707,2.0,-2.0],[1.9293,0.0,-2.0],[-0.0707,2.0,-2.0],[2.0707,0.1414,-2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,-2.0],[0.0707,2.1414,-2.0],[-0.0707,2.0,-2.0],[0.0707,2.1414,-2.0],[2.0707,0.1414,-2.0],[-0.0707,2.0,-2.0]]}

View file

@ -0,0 +1 @@
{"aabb_max":[3.071,3.141,3.0],"aabb_min":[-3.071,0.0,-3.0],"dreiecke":48,"oid":"bau_dach_giebel_6","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_giebel_6.glb","vertices":[[-3.0707,0.1414,-3.0],[-0.0707,3.1414,3.0],[-3.0707,0.1414,3.0],[-0.0707,3.1414,3.0],[-3.0707,0.1414,-3.0],[-3.0707,0.1414,3.0],[0.0707,3.0,3.0],[-2.9293,0.0,-3.0],[-2.9293,0.0,3.0],[-2.9293,0.0,-3.0],[0.0707,3.0,3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,-3.0],[-0.0707,3.1414,-3.0],[-0.0707,3.1414,3.0],[-0.0707,3.1414,-3.0],[-3.0707,0.1414,-3.0],[-0.0707,3.1414,3.0],[0.0707,3.0,-3.0],[-2.9293,0.0,-3.0],[0.0707,3.0,3.0],[-2.9293,0.0,-3.0],[0.0707,3.0,-3.0],[0.0707,3.0,3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,3.0],[-2.9293,0.0,-3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,-3.0],[-3.0707,0.1414,-3.0],[-3.0707,0.1414,3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,3.0],[0.0707,3.0,3.0],[-2.9293,0.0,3.0],[0.0707,3.0,3.0],[-3.0707,0.1414,3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,3.0],[-0.0707,3.1414,3.0],[0.0707,3.0,3.0],[-0.0707,3.1414,3.0],[-3.0707,0.1414,3.0],[0.0707,3.0,3.0],[-0.0707,3.1414,3.0],[0.0707,3.0,-3.0],[0.0707,3.0,3.0],[0.0707,3.0,-3.0],[-0.0707,3.1414,3.0],[0.0707,3.0,3.0],[-0.0707,3.1414,3.0],[-0.0707,3.1414,-3.0],[0.0707,3.0,-3.0],[-0.0707,3.1414,-3.0],[-0.0707,3.1414,3.0],[0.0707,3.0,-3.0],[-0.0707,3.1414,-3.0],[-2.9293,0.0,-3.0],[0.0707,3.0,-3.0],[-2.9293,0.0,-3.0],[-0.0707,3.1414,-3.0],[0.0707,3.0,-3.0],[-0.0707,3.1414,-3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,-3.0],[-3.0707,0.1414,-3.0],[-0.0707,3.1414,-3.0],[-2.9293,0.0,-3.0],[0.0707,3.1414,-3.0],[3.0707,0.1414,3.0],[0.0707,3.1414,3.0],[3.0707,0.1414,3.0],[0.0707,3.1414,-3.0],[0.0707,3.1414,3.0],[2.9293,0.0,3.0],[-0.0707,3.0,-3.0],[-0.0707,3.0,3.0],[-0.0707,3.0,-3.0],[2.9293,0.0,3.0],[-0.0707,3.0,3.0],[0.0707,3.1414,-3.0],[3.0707,0.1414,-3.0],[3.0707,0.1414,3.0],[3.0707,0.1414,-3.0],[0.0707,3.1414,-3.0],[3.0707,0.1414,3.0],[2.9293,0.0,-3.0],[-0.0707,3.0,-3.0],[2.9293,0.0,3.0],[-0.0707,3.0,-3.0],[2.9293,0.0,-3.0],[2.9293,0.0,3.0],[0.0707,3.1414,-3.0],[-0.0707,3.0,3.0],[-0.0707,3.0,-3.0],[-0.0707,3.0,3.0],[0.0707,3.1414,-3.0],[-0.0707,3.0,-3.0],[0.0707,3.1414,-3.0],[0.0707,3.1414,3.0],[-0.0707,3.0,3.0],[0.0707,3.1414,3.0],[0.0707,3.1414,-3.0],[-0.0707,3.0,3.0],[0.0707,3.1414,3.0],[2.9293,0.0,3.0],[-0.0707,3.0,3.0],[2.9293,0.0,3.0],[0.0707,3.1414,3.0],[-0.0707,3.0,3.0],[0.0707,3.1414,3.0],[3.0707,0.1414,3.0],[2.9293,0.0,3.0],[3.0707,0.1414,3.0],[0.0707,3.1414,3.0],[2.9293,0.0,3.0],[3.0707,0.1414,3.0],[2.9293,0.0,-3.0],[2.9293,0.0,3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,3.0],[2.9293,0.0,3.0],[3.0707,0.1414,3.0],[3.0707,0.1414,-3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,-3.0],[3.0707,0.1414,3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,-3.0],[-0.0707,3.0,-3.0],[2.9293,0.0,-3.0],[-0.0707,3.0,-3.0],[3.0707,0.1414,-3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,-3.0],[0.0707,3.1414,-3.0],[-0.0707,3.0,-3.0],[0.0707,3.1414,-3.0],[3.0707,0.1414,-3.0],[-0.0707,3.0,-3.0]]}

View file

@ -0,0 +1 @@
{"aabb_max":[2.071,2.141,2.071],"aabb_min":[-2.071,0.0,-2.071],"dreiecke":64,"oid":"bau_dach_pyramide_4","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_pyramide_4.glb","vertices":[[-2.0,0.0,-1.9293],[0.0,2.0,0.0707],[2.0,0.0,-1.9293],[0.0,2.0,0.0707],[-2.0,0.0,-1.9293],[2.0,0.0,-1.9293],[0.0,2.1414,-0.0707],[-2.0,0.1414,-2.0707],[2.0,0.1414,-2.0707],[-2.0,0.1414,-2.0707],[0.0,2.1414,-0.0707],[2.0,0.1414,-2.0707],[-2.0,0.0,-1.9293],[2.0,0.1414,-2.0707],[-2.0,0.1414,-2.0707],[2.0,0.1414,-2.0707],[-2.0,0.0,-1.9293],[-2.0,0.1414,-2.0707],[-2.0,0.0,-1.9293],[2.0,0.0,-1.9293],[2.0,0.1414,-2.0707],[2.0,0.0,-1.9293],[-2.0,0.0,-1.9293],[2.0,0.1414,-2.0707],[2.0,0.0,-1.9293],[0.0,2.1414,-0.0707],[2.0,0.1414,-2.0707],[0.0,2.1414,-0.0707],[2.0,0.0,-1.9293],[2.0,0.1414,-2.0707],[2.0,0.0,-1.9293],[0.0,2.0,0.0707],[0.0,2.1414,-0.0707],[0.0,2.0,0.0707],[2.0,0.0,-1.9293],[0.0,2.1414,-0.0707],[0.0,2.0,0.0707],[-2.0,0.1414,-2.0707],[0.0,2.1414,-0.0707],[-2.0,0.1414,-2.0707],[0.0,2.0,0.0707],[0.0,2.1414,-0.0707],[0.0,2.0,0.0707],[-2.0,0.0,-1.9293],[-2.0,0.1414,-2.0707],[-2.0,0.0,-1.9293],[0.0,2.0,0.0707],[-2.0,0.1414,-2.0707],[1.9293,0.0,-2.0],[-0.0707,2.0,0.0],[1.9293,0.0,2.0],[-0.0707,2.0,0.0],[1.9293,0.0,-2.0],[1.9293,0.0,2.0],[0.0707,2.1414,0.0],[2.0707,0.1414,-2.0],[2.0707,0.1414,2.0],[2.0707,0.1414,-2.0],[0.0707,2.1414,0.0],[2.0707,0.1414,2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,2.0],[2.0707,0.1414,-2.0],[2.0707,0.1414,2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,-2.0],[1.9293,0.0,-2.0],[1.9293,0.0,2.0],[2.0707,0.1414,2.0],[1.9293,0.0,2.0],[1.9293,0.0,-2.0],[2.0707,0.1414,2.0],[1.9293,0.0,2.0],[0.0707,2.1414,0.0],[2.0707,0.1414,2.0],[0.0707,2.1414,0.0],[1.9293,0.0,2.0],[2.0707,0.1414,2.0],[1.9293,0.0,2.0],[-0.0707,2.0,0.0],[0.0707,2.1414,0.0],[-0.0707,2.0,0.0],[1.9293,0.0,2.0],[0.0707,2.1414,0.0],[-0.0707,2.0,0.0],[2.0707,0.1414,-2.0],[0.0707,2.1414,0.0],[2.0707,0.1414,-2.0],[-0.0707,2.0,0.0],[0.0707,2.1414,0.0],[-0.0707,2.0,0.0],[1.9293,0.0,-2.0],[2.0707,0.1414,-2.0],[1.9293,0.0,-2.0],[-0.0707,2.0,0.0],[2.0707,0.1414,-2.0],[2.0,0.0,1.9293],[0.0,2.0,-0.0707],[-2.0,0.0,1.9293],[0.0,2.0,-0.0707],[2.0,0.0,1.9293],[-2.0,0.0,1.9293],[0.0,2.1414,0.0707],[2.0,0.1414,2.0707],[-2.0,0.1414,2.0707],[2.0,0.1414,2.0707],[0.0,2.1414,0.0707],[-2.0,0.1414,2.0707],[2.0,0.0,1.9293],[-2.0,0.1414,2.0707],[2.0,0.1414,2.0707],[-2.0,0.1414,2.0707],[2.0,0.0,1.9293],[2.0,0.1414,2.0707],[2.0,0.0,1.9293],[-2.0,0.0,1.9293],[-2.0,0.1414,2.0707],[-2.0,0.0,1.9293],[2.0,0.0,1.9293],[-2.0,0.1414,2.0707],[-2.0,0.0,1.9293],[0.0,2.1414,0.0707],[-2.0,0.1414,2.0707],[0.0,2.1414,0.0707],[-2.0,0.0,1.9293],[-2.0,0.1414,2.0707],[-2.0,0.0,1.9293],[0.0,2.0,-0.0707],[0.0,2.1414,0.0707],[0.0,2.0,-0.0707],[-2.0,0.0,1.9293],[0.0,2.1414,0.0707],[0.0,2.0,-0.0707],[2.0,0.1414,2.0707],[0.0,2.1414,0.0707],[2.0,0.1414,2.0707],[0.0,2.0,-0.0707],[0.0,2.1414,0.0707],[0.0,2.0,-0.0707],[2.0,0.0,1.9293],[2.0,0.1414,2.0707],[2.0,0.0,1.9293],[0.0,2.0,-0.0707],[2.0,0.1414,2.0707],[-1.9293,0.0,2.0],[0.0707,2.0,0.0],[-1.9293,0.0,-2.0],[0.0707,2.0,0.0],[-1.9293,0.0,2.0],[-1.9293,0.0,-2.0],[-0.0707,2.1414,0.0],[-2.0707,0.1414,2.0],[-2.0707,0.1414,-2.0],[-2.0707,0.1414,2.0],[-0.0707,2.1414,0.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,-2.0],[-2.0707,0.1414,2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,2.0],[-1.9293,0.0,2.0],[-1.9293,0.0,-2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,-2.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,-2.0],[-0.0707,2.1414,0.0],[-2.0707,0.1414,-2.0],[-0.0707,2.1414,0.0],[-1.9293,0.0,-2.0],[-2.0707,0.1414,-2.0],[-1.9293,0.0,-2.0],[0.0707,2.0,0.0],[-0.0707,2.1414,0.0],[0.0707,2.0,0.0],[-1.9293,0.0,-2.0],[-0.0707,2.1414,0.0],[0.0707,2.0,0.0],[-2.0707,0.1414,2.0],[-0.0707,2.1414,0.0],[-2.0707,0.1414,2.0],[0.0707,2.0,0.0],[-0.0707,2.1414,0.0],[0.0707,2.0,0.0],[-1.9293,0.0,2.0],[-2.0707,0.1414,2.0],[-1.9293,0.0,2.0],[0.0707,2.0,0.0],[-2.0707,0.1414,2.0]]}

View file

@ -0,0 +1 @@
{"aabb_max":[3.071,3.141,3.071],"aabb_min":[-3.071,0.0,-3.071],"dreiecke":64,"oid":"bau_dach_pyramide_6","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_pyramide_6.glb","vertices":[[-3.0,0.0,-2.9293],[0.0,3.0,0.0707],[3.0,0.0,-2.9293],[0.0,3.0,0.0707],[-3.0,0.0,-2.9293],[3.0,0.0,-2.9293],[0.0,3.1414,-0.0707],[-3.0,0.1414,-3.0707],[3.0,0.1414,-3.0707],[-3.0,0.1414,-3.0707],[0.0,3.1414,-0.0707],[3.0,0.1414,-3.0707],[-3.0,0.0,-2.9293],[3.0,0.1414,-3.0707],[-3.0,0.1414,-3.0707],[3.0,0.1414,-3.0707],[-3.0,0.0,-2.9293],[-3.0,0.1414,-3.0707],[-3.0,0.0,-2.9293],[3.0,0.0,-2.9293],[3.0,0.1414,-3.0707],[3.0,0.0,-2.9293],[-3.0,0.0,-2.9293],[3.0,0.1414,-3.0707],[3.0,0.0,-2.9293],[0.0,3.1414,-0.0707],[3.0,0.1414,-3.0707],[0.0,3.1414,-0.0707],[3.0,0.0,-2.9293],[3.0,0.1414,-3.0707],[3.0,0.0,-2.9293],[0.0,3.0,0.0707],[0.0,3.1414,-0.0707],[0.0,3.0,0.0707],[3.0,0.0,-2.9293],[0.0,3.1414,-0.0707],[0.0,3.0,0.0707],[-3.0,0.1414,-3.0707],[0.0,3.1414,-0.0707],[-3.0,0.1414,-3.0707],[0.0,3.0,0.0707],[0.0,3.1414,-0.0707],[0.0,3.0,0.0707],[-3.0,0.0,-2.9293],[-3.0,0.1414,-3.0707],[-3.0,0.0,-2.9293],[0.0,3.0,0.0707],[-3.0,0.1414,-3.0707],[2.9293,0.0,-3.0],[-0.0707,3.0,0.0],[2.9293,0.0,3.0],[-0.0707,3.0,0.0],[2.9293,0.0,-3.0],[2.9293,0.0,3.0],[0.0707,3.1414,0.0],[3.0707,0.1414,-3.0],[3.0707,0.1414,3.0],[3.0707,0.1414,-3.0],[0.0707,3.1414,0.0],[3.0707,0.1414,3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,3.0],[3.0707,0.1414,-3.0],[3.0707,0.1414,3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,-3.0],[2.9293,0.0,-3.0],[2.9293,0.0,3.0],[3.0707,0.1414,3.0],[2.9293,0.0,3.0],[2.9293,0.0,-3.0],[3.0707,0.1414,3.0],[2.9293,0.0,3.0],[0.0707,3.1414,0.0],[3.0707,0.1414,3.0],[0.0707,3.1414,0.0],[2.9293,0.0,3.0],[3.0707,0.1414,3.0],[2.9293,0.0,3.0],[-0.0707,3.0,0.0],[0.0707,3.1414,0.0],[-0.0707,3.0,0.0],[2.9293,0.0,3.0],[0.0707,3.1414,0.0],[-0.0707,3.0,0.0],[3.0707,0.1414,-3.0],[0.0707,3.1414,0.0],[3.0707,0.1414,-3.0],[-0.0707,3.0,0.0],[0.0707,3.1414,0.0],[-0.0707,3.0,0.0],[2.9293,0.0,-3.0],[3.0707,0.1414,-3.0],[2.9293,0.0,-3.0],[-0.0707,3.0,0.0],[3.0707,0.1414,-3.0],[3.0,0.0,2.9293],[0.0,3.0,-0.0707],[-3.0,0.0,2.9293],[0.0,3.0,-0.0707],[3.0,0.0,2.9293],[-3.0,0.0,2.9293],[0.0,3.1414,0.0707],[3.0,0.1414,3.0707],[-3.0,0.1414,3.0707],[3.0,0.1414,3.0707],[0.0,3.1414,0.0707],[-3.0,0.1414,3.0707],[3.0,0.0,2.9293],[-3.0,0.1414,3.0707],[3.0,0.1414,3.0707],[-3.0,0.1414,3.0707],[3.0,0.0,2.9293],[3.0,0.1414,3.0707],[3.0,0.0,2.9293],[-3.0,0.0,2.9293],[-3.0,0.1414,3.0707],[-3.0,0.0,2.9293],[3.0,0.0,2.9293],[-3.0,0.1414,3.0707],[-3.0,0.0,2.9293],[0.0,3.1414,0.0707],[-3.0,0.1414,3.0707],[0.0,3.1414,0.0707],[-3.0,0.0,2.9293],[-3.0,0.1414,3.0707],[-3.0,0.0,2.9293],[0.0,3.0,-0.0707],[0.0,3.1414,0.0707],[0.0,3.0,-0.0707],[-3.0,0.0,2.9293],[0.0,3.1414,0.0707],[0.0,3.0,-0.0707],[3.0,0.1414,3.0707],[0.0,3.1414,0.0707],[3.0,0.1414,3.0707],[0.0,3.0,-0.0707],[0.0,3.1414,0.0707],[0.0,3.0,-0.0707],[3.0,0.0,2.9293],[3.0,0.1414,3.0707],[3.0,0.0,2.9293],[0.0,3.0,-0.0707],[3.0,0.1414,3.0707],[-2.9293,0.0,3.0],[0.0707,3.0,0.0],[-2.9293,0.0,-3.0],[0.0707,3.0,0.0],[-2.9293,0.0,3.0],[-2.9293,0.0,-3.0],[-0.0707,3.1414,0.0],[-3.0707,0.1414,3.0],[-3.0707,0.1414,-3.0],[-3.0707,0.1414,3.0],[-0.0707,3.1414,0.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,-3.0],[-3.0707,0.1414,3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,3.0],[-2.9293,0.0,3.0],[-2.9293,0.0,-3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,-3.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,-3.0],[-0.0707,3.1414,0.0],[-3.0707,0.1414,-3.0],[-0.0707,3.1414,0.0],[-2.9293,0.0,-3.0],[-3.0707,0.1414,-3.0],[-2.9293,0.0,-3.0],[0.0707,3.0,0.0],[-0.0707,3.1414,0.0],[0.0707,3.0,0.0],[-2.9293,0.0,-3.0],[-0.0707,3.1414,0.0],[0.0707,3.0,0.0],[-3.0707,0.1414,3.0],[-0.0707,3.1414,0.0],[-3.0707,0.1414,3.0],[0.0707,3.0,0.0],[-0.0707,3.1414,0.0],[0.0707,3.0,0.0],[-2.9293,0.0,3.0],[-3.0707,0.1414,3.0],[-2.9293,0.0,3.0],[0.0707,3.0,0.0],[-3.0707,0.1414,3.0]]}

View file

@ -0,0 +1 @@
{"aabb_max":[2.0,2.0,0.1],"aabb_min":[-2.0,0.0,-0.1],"dreiecke":16,"oid":"bau_giebelwand_4","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_giebelwand_4.glb","vertices":[[-2.0,0.0,0.1],[0.0,2.0,0.1],[2.0,0.0,0.1],[0.0,2.0,0.1],[-2.0,0.0,0.1],[2.0,0.0,0.1],[2.0,0.0,-0.1],[0.0,2.0,-0.1],[-2.0,0.0,-0.1],[0.0,2.0,-0.1],[2.0,0.0,-0.1],[-2.0,0.0,-0.1],[-2.0,0.0,0.1],[2.0,0.0,-0.1],[-2.0,0.0,-0.1],[2.0,0.0,-0.1],[-2.0,0.0,0.1],[-2.0,0.0,-0.1],[-2.0,0.0,0.1],[2.0,0.0,0.1],[2.0,0.0,-0.1],[2.0,0.0,0.1],[-2.0,0.0,0.1],[2.0,0.0,-0.1],[2.0,0.0,0.1],[0.0,2.0,-0.1],[2.0,0.0,-0.1],[0.0,2.0,-0.1],[2.0,0.0,0.1],[2.0,0.0,-0.1],[2.0,0.0,0.1],[0.0,2.0,0.1],[0.0,2.0,-0.1],[0.0,2.0,0.1],[2.0,0.0,0.1],[0.0,2.0,-0.1],[0.0,2.0,0.1],[-2.0,0.0,-0.1],[0.0,2.0,-0.1],[-2.0,0.0,-0.1],[0.0,2.0,0.1],[0.0,2.0,-0.1],[0.0,2.0,0.1],[-2.0,0.0,0.1],[-2.0,0.0,-0.1],[-2.0,0.0,0.1],[0.0,2.0,0.1],[-2.0,0.0,-0.1]]}

View file

@ -0,0 +1 @@
{"aabb_max":[3.0,3.0,0.1],"aabb_min":[-3.0,0.0,-0.1],"dreiecke":16,"oid":"bau_giebelwand_6","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_giebelwand_6.glb","vertices":[[-3.0,0.0,0.1],[0.0,3.0,0.1],[3.0,0.0,0.1],[0.0,3.0,0.1],[-3.0,0.0,0.1],[3.0,0.0,0.1],[3.0,0.0,-0.1],[0.0,3.0,-0.1],[-3.0,0.0,-0.1],[0.0,3.0,-0.1],[3.0,0.0,-0.1],[-3.0,0.0,-0.1],[-3.0,0.0,0.1],[3.0,0.0,-0.1],[-3.0,0.0,-0.1],[3.0,0.0,-0.1],[-3.0,0.0,0.1],[-3.0,0.0,-0.1],[-3.0,0.0,0.1],[3.0,0.0,0.1],[3.0,0.0,-0.1],[3.0,0.0,0.1],[-3.0,0.0,0.1],[3.0,0.0,-0.1],[3.0,0.0,0.1],[0.0,3.0,-0.1],[3.0,0.0,-0.1],[0.0,3.0,-0.1],[3.0,0.0,0.1],[3.0,0.0,-0.1],[3.0,0.0,0.1],[0.0,3.0,0.1],[0.0,3.0,-0.1],[0.0,3.0,0.1],[3.0,0.0,0.1],[0.0,3.0,-0.1],[0.0,3.0,0.1],[-3.0,0.0,-0.1],[0.0,3.0,-0.1],[-3.0,0.0,-0.1],[0.0,3.0,0.1],[0.0,3.0,-0.1],[0.0,3.0,0.1],[-3.0,0.0,0.1],[-3.0,0.0,-0.1],[-3.0,0.0,0.1],[0.0,3.0,0.1],[-3.0,0.0,-0.1]]}

View file

@ -3506,6 +3506,36 @@
"kategorie": "bauteile",
"name_de": "Satteldach"
},
"bau_dach_giebel_4": {
"poly": 48,
"collision": {
"quelle": "kollisions/bau_dach_giebel_4.json",
"shape": "trimesh",
"size": [
4.141,
2.141,
4.0
]
},
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel_4.glb",
"kategorie": "bauteile",
"name_de": "Satteldach 4×4 m"
},
"bau_dach_giebel_6": {
"poly": 48,
"collision": {
"quelle": "kollisions/bau_dach_giebel_6.json",
"shape": "trimesh",
"size": [
6.141,
3.141,
6.0
]
},
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel_6.glb",
"kategorie": "bauteile",
"name_de": "Satteldach 6×6 m"
},
"bau_dach_pyramide": {
"poly": 64,
"collision": {
@ -3521,6 +3551,36 @@
"kategorie": "bauteile",
"name_de": "Pyramidendach"
},
"bau_dach_pyramide_4": {
"poly": 64,
"collision": {
"quelle": "kollisions/bau_dach_pyramide_4.json",
"shape": "trimesh",
"size": [
4.141,
2.141,
4.141
]
},
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_pyramide_4.glb",
"kategorie": "bauteile",
"name_de": "Pyramidendach 4×4 m"
},
"bau_dach_pyramide_6": {
"poly": 64,
"collision": {
"quelle": "kollisions/bau_dach_pyramide_6.json",
"shape": "trimesh",
"size": [
6.141,
3.141,
6.141
]
},
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_pyramide_6.glb",
"kategorie": "bauteile",
"name_de": "Pyramidendach 6×6 m"
},
"bau_fundament": {
"poly": 24,
"collision": {
@ -3551,6 +3611,36 @@
"kategorie": "bauteile",
"name_de": "Giebelwand"
},
"bau_giebelwand_4": {
"poly": 16,
"collision": {
"quelle": "kollisions/bau_giebelwand_4.json",
"shape": "trimesh",
"size": [
4.0,
2.0,
0.2
]
},
"glb": "res://assets/objekte/military_desert/bauteile/bau_giebelwand_4.glb",
"kategorie": "bauteile",
"name_de": "Giebelwand 4 m"
},
"bau_giebelwand_6": {
"poly": 16,
"collision": {
"quelle": "kollisions/bau_giebelwand_6.json",
"shape": "trimesh",
"size": [
6.0,
3.0,
0.2
]
},
"glb": "res://assets/objekte/military_desert/bauteile/bau_giebelwand_6.glb",
"kategorie": "bauteile",
"name_de": "Giebelwand 6 m"
},
"bau_pfeiler": {
"poly": 12,
"collision": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bt6lbs34ymm1a"
path="res://.godot/imported/bau_dach_giebel_4.png-168cdf0c90f3e99c7b117e37cec1b44c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://library/thumbs/bau_dach_giebel_4.png"
dest_files=["res://.godot/imported/bau_dach_giebel_4.png-168cdf0c90f3e99c7b117e37cec1b44c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbvqftv8ptl76"
path="res://.godot/imported/bau_dach_giebel_6.png-fa634574bba400175ee1613d1760412b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://library/thumbs/bau_dach_giebel_6.png"
dest_files=["res://.godot/imported/bau_dach_giebel_6.png-fa634574bba400175ee1613d1760412b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://y5g22t4cndf0"
path="res://.godot/imported/bau_dach_pyramide_4.png-b23be54768ab1d080337a426aa59d63d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://library/thumbs/bau_dach_pyramide_4.png"
dest_files=["res://.godot/imported/bau_dach_pyramide_4.png-b23be54768ab1d080337a426aa59d63d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cao1art7cdwnm"
path="res://.godot/imported/bau_dach_pyramide_6.png-76fda4b6ce5077d861a5cf721b328832.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://library/thumbs/bau_dach_pyramide_6.png"
dest_files=["res://.godot/imported/bau_dach_pyramide_6.png-76fda4b6ce5077d861a5cf721b328832.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dvf3l17nuahbv"
path="res://.godot/imported/bau_giebelwand_4.png-31bd67d6005e218e8ee8915ba9b2723f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://library/thumbs/bau_giebelwand_4.png"
dest_files=["res://.godot/imported/bau_giebelwand_4.png-31bd67d6005e218e8ee8915ba9b2723f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://crpyxcrmio21s"
path="res://.godot/imported/bau_giebelwand_6.png-80357dac5fdcf76c9bb8a094c69cceda.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://library/thumbs/bau_giebelwand_6.png"
dest_files=["res://.godot/imported/bau_giebelwand_6.png-80357dac5fdcf76c9bb8a094c69cceda.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -49,11 +49,15 @@ const GRUPPEN := [
{"key": "grund", "label": "Fundament & Boden", "oids": ["bau_fundament", "bau_boden"]},
{"key": "waende", "label": "Wände", "oids": ["bau_wand", "bau_wand_tuer", "bau_wand_fenster",
"bau_wand_scharte", "bau_bruestung", "bau_pfeiler"]},
{"key": "dach", "label": "Dach & Giebel", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]},
{"key": "dach", "label": "Dach & Giebel (klein 2 m)", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]},
{"key": "dach_mittel", "label": "Dach & Giebel (mittel 4 m)", "oids": ["bau_giebelwand_4", "bau_dach_giebel_4", "bau_dach_pyramide_4"]},
{"key": "dach_gross", "label": "Dach & Giebel (groß 6 m)", "oids": ["bau_giebelwand_6", "bau_dach_giebel_6", "bau_dach_pyramide_6"]},
{"key": "treppe", "label": "Treppe", "oids": ["bau_treppe"]},
]
const QUADRAT := [[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]]
const QUADRAT_4 := [[-2.0, 0.0, -2.0], [2.0, 0.0, -2.0], [2.0, 0.0, 2.0], [-2.0, 0.0, 2.0]]
const QUADRAT_6 := [[-3.0, 0.0, -3.0], [3.0, 0.0, -3.0], [3.0, 0.0, 3.0], [-3.0, 0.0, 3.0]]
## Reihenfolge = Anzeige-Reihenfolge (GRUPPEN nutzt dieselben IDs).
## "traeger": darauf darf gestapelt werden (Oberkante zählt als Ebene).
@ -107,6 +111,34 @@ const KINDS := {
"label": "Pyramidendach", "info": "2×2 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT, "rise": 1.0, "farbe": Color(0.40, 0.25, 0.18),
},
# Große Dächer (Test-Befund 25.10.: 2×2 m reichte nur für eine Zelle). Immer 45°
# (Spitze = halbe Breite); Giebelwand und Dach gleicher Größe passen zusammen.
"bau_giebelwand_4": {
"label": "Giebelwand 4 m", "info": "Dreieck-Abschluss, 4 m breit, 2 m Spitze — zum Satteldach 4 m",
"snap": "kante", "traeger": false,
"typ": T_GIEBEL, "punkte": [[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]], "rise": 2.0, "farbe": Color(0.62, 0.53, 0.38),
},
"bau_dach_giebel_4": {
"label": "Satteldach 4×4 m", "info": "4×4 m, First in Richtung der Vorschau (R dreht)", "snap": "zelle", "traeger": false,
"typ": T_DACH_GIEBEL, "punkte": QUADRAT_4, "rise": 2.0, "farbe": Color(0.40, 0.25, 0.18),
},
"bau_dach_pyramide_4": {
"label": "Pyramidendach 4×4 m", "info": "4×4 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_4, "rise": 2.0, "farbe": Color(0.40, 0.25, 0.18),
},
"bau_giebelwand_6": {
"label": "Giebelwand 6 m", "info": "Dreieck-Abschluss, 6 m breit, 3 m Spitze — zum Satteldach 6 m",
"snap": "kante", "traeger": false,
"typ": T_GIEBEL, "punkte": [[0.0, 0.0, 0.0], [6.0, 0.0, 0.0]], "rise": 3.0, "farbe": Color(0.62, 0.53, 0.38),
},
"bau_dach_giebel_6": {
"label": "Satteldach 6×6 m", "info": "6×6 m, First in Richtung der Vorschau (R dreht)", "snap": "zelle", "traeger": false,
"typ": T_DACH_GIEBEL, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18),
},
"bau_dach_pyramide_6": {
"label": "Pyramidendach 6×6 m", "info": "6×6 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18),
},
"bau_treppe": {
"label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber",
"snap": "treppe", "traeger": false,
@ -134,6 +166,11 @@ static func snap_art(oid: String) -> String:
return str((KINDS.get(oid, {}) as Dictionary).get("snap", "zelle"))
## Platte = flaches Zell-Teil, auf dem man steht (Fundament, Boden/Decke).
static func ist_platte(oid: String) -> bool:
return snap_art(oid) == "zelle" and ist_traeger(oid)
static func ist_traeger(oid: String) -> bool:
return bool((KINDS.get(oid, {}) as Dictionary).get("traeger", false))

View file

@ -122,22 +122,22 @@ static func haus_demo(doc: Dictionary, history: Variant, mitte: Vector3) -> int:
["bau_fundament", Vector3(ox + 1, mitte.y, oz - 1), 0.0],
["bau_fundament", Vector3(ox - 1, mitte.y, oz + 1), 0.0],
["bau_fundament", Vector3(ox + 1, mitte.y, oz + 1), 0.0],
["bau_wand", Vector3(ox - 1, mitte.y, oz - 2), 0.0],
["bau_wand_fenster", Vector3(ox + 1, mitte.y, oz - 2), 0.0],
["bau_wand_tuer", Vector3(ox - 1, mitte.y, oz + 2), 0.0],
["bau_wand", Vector3(ox + 1, mitte.y, oz + 2), 0.0],
["bau_wand", Vector3(ox - 2, mitte.y, oz - 1), 90.0],
["bau_wand_scharte", Vector3(ox - 2, mitte.y, oz + 1), 90.0],
["bau_wand", Vector3(ox + 2, mitte.y, oz - 1), 90.0],
["bau_wand", Vector3(ox + 2, mitte.y, oz + 1), 90.0],
# Wände liegen GANZ in der Zelle, in der die Maus ist (Außenseite bündig mit
# der Kante) — deshalb zielen die Klicks 10 cm ins Hausinnere.
["bau_wand", Vector3(ox - 1, mitte.y, oz - 1.9), 0.0],
["bau_wand_fenster", Vector3(ox + 1, mitte.y, oz - 1.9), 0.0],
["bau_wand_tuer", Vector3(ox - 1, mitte.y, oz + 1.9), 0.0],
["bau_wand", Vector3(ox + 1, mitte.y, oz + 1.9), 0.0],
["bau_wand", Vector3(ox - 1.9, mitte.y, oz - 1), 90.0],
["bau_wand_scharte", Vector3(ox - 1.9, mitte.y, oz + 1), 90.0],
["bau_wand", Vector3(ox + 1.9, mitte.y, oz - 1), 90.0],
["bau_wand", Vector3(ox + 1.9, mitte.y, oz + 1), 90.0],
]
for st in schritte:
_demo_setzen(doc, str(st[0]), st[1] as Vector3, float(st[2]), gid, terrain)
# Decke + Dach auf die Wandoberkante (Klick auf die Oberkante = Ebene Wand oben)
var oben := mitte.y + 0.2 + BauKatalog.WAND_H
for cx in [-1.0, 1.0]:
for cz in [-1.0, 1.0]:
_demo_setzen(doc, "bau_dach_giebel", Vector3(ox + cx, oben, oz + cz), 0.0, gid, terrain)
# Ein großes Satteldach (4×4 m) auf die Wandoberkante.
var oben := mitte.y + BauKatalog.WAND_H
_demo_setzen(doc, "bau_dach_giebel_4", Vector3(ox, oben, oz), 0.0, gid, terrain)
if history != null:
history.commit("Haus gebaut (Demo)", before, history.snapshot(["objects", "group_names"]))
return gid

View file

@ -43,18 +43,31 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
var q := int(roundf(dreh / 90.0))
var viertel := ((q % 4) + 4) % 4
var p := Vector2(hit.x, hit.z)
# Treffer auf einem Objekt: Zelle auf der Kameraseite wählen (Klick auf die
# Wandoberkante liegt genau auf der Zellgrenze).
if hit_ist_objekt and art != "kante" and kamera_dir != Vector3.ZERO:
var d2 := Vector2(kamera_dir.x, kamera_dir.z)
if d2.length() > 0.01:
p -= d2.normalized() * 0.2
# Treffer auf einem Objekt: Wände liegen GANZ in einer Zelle — die Quer-Achse
# kommt dann von der Wand (Decke/Dach landet in der Zelle der Wand). Bei anderen
# Objekten die Zelle auf der Kameraseite wählen (Klick liegt auf einer Grenze).
var wand_getroffen := {}
if hit_ist_objekt:
wand_getroffen = _wand_unter_klick(teile, p, hit.y)
if hit_ist_objekt and art != "kante":
if not wand_getroffen.is_empty():
var wp: Vector3 = wand_getroffen["pos"]
if absf(sin(deg_to_rad(float(wand_getroffen["rot"])))) > 0.5:
p.x = wp.x
else:
p.y = wp.z
elif kamera_dir != Vector3.ZERO:
var d2 := Vector2(kamera_dir.x, kamera_dir.z)
if d2.length() > 0.01:
p -= d2.normalized() * 0.2
var mitte := Vector2.ZERO
var rot := 0.0
var r := BauKatalog.RASTER_M
match art:
"zelle":
mitte = Vector2(floorf(p.x / r) * r + r * 0.5, floorf(p.y / r) * r + r * 0.5)
# Teile über mehrere Zellen (große Dächer): gerade Zellzahl → Mitte auf
# einer Zellecke, ungerade → Mitte einer Zelle.
mitte = Vector2(_raster_mitte(p.x, gr.x), _raster_mitte(p.y, gr.z))
rot = viertel * 90.0
"ecke":
mitte = Vector2(roundf(p.x / r) * r, roundf(p.y / r) * r)
@ -65,16 +78,31 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
else:
mitte = Vector2(floorf(p.x / r) * r + r * 0.5, roundf(p.y / r) * r)
_: # "kante"
var fx := p.x / r
var fz := p.y / r
var h_mitte := Vector2((floorf(fx) + 0.5) * r, roundf(fz) * r) # Kante entlang X
var v_mitte := Vector2(roundf(fx) * r, (floorf(fz) + 0.5) * r) # Kante entlang Z
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte):
mitte = h_mitte
rot = 0.0
var vorhanden := wand_getroffen
if not vorhanden.is_empty():
# Klick auf eine vorhandene Wand (Seite = ersetzen, Oberkante = stapeln):
# GENAU ihre Lage übernehmen, nicht neu aus der Klickstelle raten.
var tp: Vector3 = vorhanden["pos"]
mitte = Vector2(tp.x, tp.z)
rot = 90.0 if absf(sin(deg_to_rad(float(vorhanden["rot"])))) > 0.5 else 0.0
else:
mitte = v_mitte
rot = 90.0
# Wand liegt GANZ IN der Zelle, in der der Mauszeiger ist: Außenseite
# bündig mit der Zellkante (Boden/Fundament), nicht halb in der Luft.
# Dadurch schließen sich auch Ecken ohne Lücke (Test-Befund 25.10.).
var laenge := gr.x
var halbe_dicke := gr.z * 0.5
var fx := p.x / r
var fz := p.y / r
var h_mitte := Vector2(_raster_mitte(p.x, laenge), roundf(fz) * r) # Kante entlang X
var v_mitte := Vector2(roundf(fx) * r, _raster_mitte(p.y, laenge)) # Kante entlang Z
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte):
mitte = h_mitte
mitte.y += halbe_dicke if p.y >= h_mitte.y else -halbe_dicke
rot = 0.0
else:
mitte = v_mitte
mitte.x += halbe_dicke if p.x >= v_mitte.x else -halbe_dicke
rot = 90.0
rot += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile)
rot = fposmod(rot, 360.0)
var quer := absf(sin(deg_to_rad(rot))) > 0.5 # 90°/270°: X- und Z-Ausdehnung getauscht
@ -95,7 +123,15 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
if not tr.intersects(kern):
continue
var top: float = (t["pos"] as Vector3).y + BauKatalog.groesse(troid).y
if BauKatalog.ist_traeger(troid):
if not BauKatalog.ist_traeger(troid):
continue
if (art == "kante" or art == "ecke") and BauKatalog.ist_platte(troid):
# Wände/Pfeiler zählen von der UNTERKANTE einer Platte (Boden/Decke/
# Fundament): jedes Geschoss liegt dadurch auf Vielfachen der Wandhöhe —
# mit oder ohne Decke darunter sitzen obere Wände gleich hoch
# (Test-Befund 25.10.). Die Platte selbst liegt auf der Wandoberkante.
ebenen.append((t["pos"] as Vector3).y)
else:
ebenen.append(top)
var ebene := _waehle_ebene(ebenen, hit.y)
@ -123,6 +159,39 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
"ebene": ebene, "grund": ""}
## Mitte eines Teils der Länge `laenge` entlang einer Achse: gerade Zellzahl →
## auf der nächsten Zellgrenze, ungerade → Mitte der Zelle.
static func _raster_mitte(wert: float, laenge: float) -> float:
var r := BauKatalog.RASTER_M
var zellen := maxi(1, int(roundf(laenge / r)))
if zellen % 2 == 0:
return roundf(wert / r) * r
return floorf(wert / r) * r + r * 0.5
## Vorhandene Wand, auf die der Klick trifft (Seite oder Oberkante): Rückgabe das
## Teil-Wörterbuch oder {}. `p` = Klickpunkt in XZ, `y` = Klickhöhe.
static func _wand_unter_klick(teile: Array, p: Vector2, y: float) -> Dictionary:
var bester := {}
var bester_d := INF
for t in teile:
var troid := str(t["oid"])
if BauKatalog.snap_art(troid) != "kante":
continue
var tr := teil_rect(t).grow(0.06)
if not tr.has_point(p):
continue
var unten: float = (t["pos"] as Vector3).y
var oben := unten + BauKatalog.groesse(troid).y
if y < unten - 0.1 or y > oben + 0.1:
continue
var d := p.distance_squared_to(tr.get_center())
if d < bester_d:
bester_d = d
bester = t
return bester
## Höchste Ebene ≤ hit_y + TOL; gibt es keine (Klick tief unter allen), die tiefste.
static func _waehle_ebene(ebenen: Array, hit_y: float) -> float:
var best := -INF

View file

@ -76,7 +76,10 @@ var inspector: Control # Objekt-Inspektor (Karte t_bb0d11c9) — im Insp
# bleiben hier, damit Tests/Tools denselben Vertrag sehen wie vorher)
var toolbar_mod # src/ui/panels/toolbar.gd
var status_mod # src/ui/panels/status_bar.gd
var selection_mod # src/ui/panels/selection_panel.gd
var selection_mod # src/ui/panels/selection_panel.gd (nur Logik, kein Tab mehr)
var settings_mod # src/ui/panels/settings_panel.gd (im Einstellungen-Fenster)
var settings_dialog: AcceptDialog # Fenster „Einstellungen“ (Knopf in der Kopfleiste)
var sym_mod # src/ui/panels/symmetry_panel.gd (im Map-Tab)
var map_mod # src/ui/panels/map_panel.gd
var terrain_mod # src/ui/panels/terrain_panel.gd
var malen_mod # src/ui/panels/malen_panel.gd (Malen-Tab)
@ -91,13 +94,12 @@ var tool_label: Label # weiter in status_mod
var sym_label: Label # weiter in status_mod
var undo_btn: Button # weiter in toolbar_mod
var redo_btn: Button # weiter in toolbar_mod
var grid_btn: Button # weiter in toolbar_mod
var contours_btn: Button # weiter in toolbar_mod
var size_menu: MenuButton # weiter in toolbar_mod
var sym_menu: MenuButton # weiter in toolbar_mod
var grid_btn: CheckButton # weiter in settings_mod (Einstellungen-Fenster)
var contours_btn: CheckButton # weiter in settings_mod
var size_menu: OptionButton # weiter in settings_mod
var sym_menu: OptionButton # weiter in settings_mod
var prop_name: LineEdit # weiter in map_mod
var sel_label: Label # weiter in selection_mod
var gizmo_step_box: SpinBox # weiter in selection_mod
var gizmo_step_box: SpinBox # weiter in settings_mod
var library_search: LineEdit # weiter in assets_mod
var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit)
var _library_group_rows: Array = [] # weiter in assets_mod (Bequemlichkeit)
@ -273,6 +275,7 @@ func _build_ui() -> void:
ui_right_panel = right_tabs
right_tabs.tab_changed.connect(_on_right_tab_changed)
split.add_child(right_tabs)
_build_map_sections()
# --- Assets unten (Registry-Gruppe "unten", U1 Auftragspunkt 4) ---
# Die Objekt-Bibliothek wohnt jetzt UNTER dem Hauptbereich (Unity-
@ -337,13 +340,6 @@ func _make_module(script: GDScript, parent: Control) -> Control:
func _pull_toolbar_refs() -> void:
undo_btn = toolbar_mod.undo_btn
redo_btn = toolbar_mod.redo_btn
grid_btn = toolbar_mod.grid_btn
contours_btn = toolbar_mod.contours_btn
size_menu = toolbar_mod.size_menu
sym_menu = toolbar_mod.sym_menu
# Häkchen im UI-Größe-Menü auf die aktuelle Stufe (vor U1 rief die
# Top-Bar _sync_size_menu während des Baus — jetzt nach dem Referenz-Zug).
_sync_size_menu()
func _pull_status_refs() -> void:
@ -386,8 +382,6 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
match child.name:
"inspektor":
inspector = child
"auswahl":
selection_mod = child
"map":
map_mod = child
"terrain":
@ -396,12 +390,7 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
malen_mod = child
"bauen":
bauen_mod = child
"check":
check_mod = child
sel_label = selection_mod.sel_label
gizmo_step_box = selection_mod.gizmo_step_box
prop_name = map_mod.prop_name
spawn_status = check_mod.spawn_status
terrain_info = map_mod.terrain_info
# Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die
# alten Namen als Aliase (Tests/Werkzeug-Vertrag).
@ -438,7 +427,73 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
library_close_all_btn = assets_mod.library_close_all_btn
## Abschnitte im Map-Tab (Registry-Gruppe "map"): Spawn-Zonen, Test-Start-Info,
## Symmetrie. Sie hängen UNTER den Map-Eigenschaften im selben Scroll-Bereich.
func _build_map_sections() -> void:
var host: Control = map_mod.sections_host()
for entry in PanelRegistryScript.entries("map"):
var script: GDScript = load(str(entry["skript"]))
var panel: Control = script.new()
panel.name = str(entry["id"])
panel.setup(self)
if panel.has_method("build"):
panel.call("build")
host.add_child(HSeparator.new())
host.add_child(panel)
match str(entry["id"]):
"check":
check_mod = panel
spawn_status = check_mod.spawn_status
"symmetrie":
sym_mod = panel
## Einstellungen-Fenster (Knopf ⚙ in der Kopfleiste): Inhalt kommt aus der
## Registry-Gruppe "dialog". Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe,
## Raster-Snap und Gizmo-Schritt wohnen dort (vorher Kopfleiste / Objekt-Werkzeuge).
func _build_settings_dialog() -> void:
settings_dialog = AcceptDialog.new()
settings_dialog.name = "EinstellungenFenster"
settings_dialog.title = "Einstellungen"
settings_dialog.exclusive = false
settings_dialog.ok_button_text = "Schließen"
settings_dialog.min_size = UIScale.v2i(520, 420)
var scroll := ScrollContainer.new()
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
scroll.custom_minimum_size = UIScale.v2(500, 380)
settings_dialog.add_child(scroll)
for entry in PanelRegistryScript.entries("dialog"):
var script: GDScript = load(str(entry["skript"]))
var panel: Control = script.new()
panel.name = str(entry["id"])
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.setup(self)
if panel.has_method("build"):
panel.call("build")
scroll.add_child(panel)
settings_mod = panel
add_child(settings_dialog)
grid_btn = settings_mod.grid_btn
contours_btn = settings_mod.contours_btn
size_menu = settings_mod.size_menu
sym_menu = settings_mod.sym_menu
gizmo_step_box = settings_mod.gizmo_step_box
settings_mod.call("refresh")
func _open_settings() -> void:
if settings_dialog == null:
return
settings_mod.call("refresh")
settings_dialog.popup_centered(UIScale.v2i(560, 520))
func _build_dialogs_and_overlays() -> void:
selection_mod = load("res://src/ui/panels/selection_panel.gd").new()
selection_mod.name = "AuswahlLogik"
selection_mod.setup(self)
add_child(selection_mod)
_build_settings_dialog()
# --- Dialoge ---
new_dialog = preload("res://src/ui/new_map_dialog.gd").new()
new_dialog.confirmed.connect(_on_new_map_confirmed)
@ -558,11 +613,26 @@ func _build_dialogs_and_overlays() -> void:
add_child(help_overlay)
## Ziehen aus der Objekt-Bibliothek: Vorschau folgt der Maus, Loslassen setzt.
func _viewport_can_drop(at_position: Vector2, data: Variant) -> bool:
if not (data is Dictionary) or not (data as Dictionary).has("svr_oid") or doc.is_empty() or assets_mod == null:
return false
assets_mod.drag_hover(str(data["svr_oid"]), at_position)
return true
func _viewport_drop(at_position: Vector2, data: Variant) -> void:
if data is Dictionary and (data as Dictionary).has("svr_oid") and assets_mod != null:
assets_mod.drag_drop(str(data["svr_oid"]), at_position)
func _build_center_viewport() -> SubViewportContainer:
var container := SubViewportContainer.new()
container.stretch = true
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
# Objekt-Kachel aus der Bibliothek in die Szene ziehen (25.10.).
container.set_drag_forwarding(Callable(), _viewport_can_drop, _viewport_drop)
var viewport := SubViewport.new()
viewport.own_world_3d = true
@ -714,10 +784,7 @@ func _on_menu_action(id: int) -> void:
func _sync_size_menu() -> void:
if size_menu == null or not is_instance_valid(size_menu):
return
var popup := size_menu.get_popup()
var cur := UIScale.step_index()
for i in popup.item_count:
popup.set_item_checked(i, i == cur)
size_menu.select(UIScale.step_index())
## Neue Stufe gewählt: speichern, kompletten UI-Neuaufbau mit dem neuen
@ -732,7 +799,10 @@ func _on_ui_size_selected(index: int) -> void:
_sync_size_menu()
return
UIScale.set_step_index(index)
var settings_war_offen := settings_dialog != null and settings_dialog.visible
_rebuild_ui_for_scale()
if settings_war_offen:
_open_settings.call_deferred()
_set_status("UI-Größe: %s — sofort wirksam und gespeichert (bleibt auch nach Neustart)." % UIScale.step_label(index))
@ -1363,11 +1433,10 @@ func _update_sym_ui() -> void:
if sym_menu == null or sym_label == null:
return
var label := "Symmetrie: %s" % SymmetryCore.mode_label(sym_mode)
sym_menu.text = label
sym_label.text = label
var popup := sym_menu.get_popup()
for i in SymmetryCore.MODE_LABELS.size():
popup.set_item_checked(i, i == sym_mode)
sym_menu.select(sym_mode)
if sym_mod != null and sym_mod.has_method("refresh_mode"):
sym_mod.call("refresh_mode")
# --------------------------------------------------------------- History ----

View file

@ -42,6 +42,8 @@ var _recent_box: VBoxContainer # Zuletzt-benutzt-Ansicht (im Scroll)
var _recent_grid: GridContainer
var _recent_tiles: Dictionary = {} # id → Button (Zuletzt-Ansicht, frisch gebaut)
var _recent_ids: Array = [] # Reihenfolge: neueste zuerst
var _drag_oid := "" # Kachel, die gerade gezogen wird
var _drag_started_placement := false # Ziehen hat das Platzieren gestartet
var selected_oid: String = "" # per Klick gewähltes Objekt
var _id_font_normal: int = 0 # ID-Schriftgröße (skaliert), Normalzustand
var _empty_hint: Label # Leerzustand (U7): kein Treffer / leerer Bereich
@ -476,6 +478,9 @@ func _build_tile(oid: String) -> Button:
btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)"
btn.pressed.connect(_on_tile_pressed.bind(oid))
btn.gui_input.connect(_on_tile_gui_input.bind(oid))
# Ziehen & Loslassen (25.10.): Kachel anklicken, gedrückt halten, in die Szene
# ziehen — beim Loslassen wird das Objekt dort gesetzt (Doppelklick geht weiter).
btn.set_drag_forwarding(_tile_drag_data.bind(oid, btn), Callable(), Callable())
btn.mouse_entered.connect(_on_tile_hover.bind(oid, true))
btn.mouse_exited.connect(_on_tile_hover.bind(oid, false))
var tex := LibraryBrowser.load_thumb(oid)
@ -610,6 +615,65 @@ func _on_tile_gui_input(event: InputEvent, oid: String) -> void:
_activate_tile(oid)
## Ziehen-Start einer Kachel: Vorschau-Etikett an der Maus, Daten = Objekt-ID.
func _tile_drag_data(_at_position: Vector2, oid: String, btn: Button) -> Variant:
if main.doc.is_empty():
return null
_select_tile(oid)
_drag_oid = oid
var prev := PanelContainer.new()
var box := HBoxContainer.new()
prev.add_child(box)
var tex := LibraryBrowser.load_thumb(oid)
if tex != null:
var tr := TextureRect.new()
tr.texture = tex
tr.custom_minimum_size = UIScale.v2(64, 64)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
box.add_child(tr)
var lbl := Label.new()
lbl.text = LibraryBrowser.entry_label(oid, (main.manifest.get("objects", {}) as Dictionary).get(oid, {}))
box.add_child(lbl)
prev.modulate.a = 0.85
if btn.get_viewport().gui_is_dragging():
btn.set_drag_preview(prev)
else:
prev.free()
return {"svr_oid": oid}
## Maus mit gezogener Kachel ÜBER der 3D-Ansicht: Platzier-Vorschau folgt der Maus.
func drag_hover(oid: String, viewport_pos: Vector2) -> void:
if str(main.get("tool_mode")) != "objects":
main.call("_set_tool_mode", "objects")
if not (main.interaction.placing and main.interaction.place_oid == oid):
main.interaction.start_placement(oid)
_drag_started_placement = true
main.interaction._ghost_update(viewport_pos)
## Loslassen über der 3D-Ansicht: genau wie ein Setz-Klick.
func drag_drop(oid: String, viewport_pos: Vector2) -> void:
drag_hover(oid, viewport_pos)
main.interaction._place_object(viewport_pos, false)
if main.interaction.placing and not BauKatalog.ist_bau(oid):
main.interaction.cancel_placement() # daneben gezielt: Vorschau nicht hängen lassen
_drag_started_placement = false
_drag_oid = ""
func _notification(what: int) -> void:
if what != NOTIFICATION_DRAG_END or _drag_oid.is_empty():
return
# Losgelassen ÜBER der Szene → drag_drop hat aufgeräumt. Sonst (abgebrochen,
# anderswo losgelassen) die Vorschau wieder wegnehmen.
if _drag_started_placement and main != null and main.interaction.placing:
main.interaction.cancel_placement()
_drag_started_placement = false
_drag_oid = ""
func _select_tile(oid: String) -> void:
var changed := selected_oid != oid
selected_oid = oid

View file

@ -1,18 +1,11 @@
extends "res://src/ui/panels/editor_panel.gd"
## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe) plus ein
## paar passende Module aus dem Military-Desert-Pack. Ein Klick auf ein Teil startet
## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe). Fertige
## Pack-Objekte (Container, Hangar-Tore, Sperren …) gehören in die Objekt-Bibliothek
## unten, NICHT hierher. Ein Klick auf ein Teil startet
## das Platzieren (Vorschau rastet an Zelle/Kante/Ebene, siehe build_snap.gd); jedes
## gesetzte Teil ist ein normales Objekt und gehört zum laufenden Gebäude (Gruppe).
## Gebäude kopieren/löschen/auswählen läuft über die Knöpfe unten — alles Undo-fähig.
## Pack-Module, die zum Gebäudebau passen (IDs stabil, siehe Bibliothek).
const PACK_MODULE := [
{"label": "Container", "oids": ["container_001", "container_002", "container_003", "container_004",
"container_005", "container_006", "container_007", "container_008"]},
{"label": "Hangar-Tore", "oids": ["hangar_door_001", "hangar_door_002", "hangar_door_003"]},
{"label": "Betonsperren", "oids": ["barrier_001", "barrier_002", "barrier_003", "barrier_004"]},
]
var bau_buttons: Dictionary = {} # oid → Button
var info_label: Label
var _group: ButtonGroup
@ -32,15 +25,7 @@ func build() -> void:
add_child(_muted_label("Wähle ein Teil und klicke in die Karte. Wände rasten an die nächste Zellkante, Böden und Dächer in die Zellmitte (Raster 2 m). Klick auf die Oberkante einer Wand stapelt das nächste Geschoss, Klick auf die Seite ersetzt das Teil (z. B. Wand → Tür). R dreht, Esc beendet."))
for gruppe in BauKatalog.GRUPPEN:
add_child(_field_label(str(gruppe["label"])))
add_child(_knopf_raster(gruppe["oids"] as Array, true))
var pack := _pack_gruppen()
if not pack.is_empty():
add_child(_separator())
add_child(_header_label("Pack-Module"))
add_child(_muted_label("Fertige Teile aus dem Military-Desert-Pack — normale Objekte, werden auf den Boden gesetzt."))
for pg in pack:
add_child(_field_label(str(pg["label"])))
add_child(_knopf_raster(pg["oids"] as Array, false))
add_child(_knopf_raster(gruppe["oids"] as Array))
add_child(_separator())
add_child(_header_label("Gebäude"))
info_label = _muted_label("")
@ -54,22 +39,10 @@ func build() -> void:
add_child(_aktion("🗑 Gebäude löschen", "Löscht alle Teile des Gebäudes (Strg+Z holt es zurück).",
func() -> void: main.call("_bau_gebaeude_loeschen")))
refresh_info()
wrap_in_scroll()
func _pack_gruppen() -> Array:
var lib: Dictionary = main.manifest.get("objects", {}) if main != null else {}
var out: Array = []
for pg in PACK_MODULE:
var oids: Array = []
for oid in (pg["oids"] as Array):
if lib.has(oid):
oids.append(oid)
if not oids.is_empty():
out.append({"label": pg["label"], "oids": oids})
return out
func _knopf_raster(oids: Array, bau: bool) -> GridContainer:
func _knopf_raster(oids: Array) -> GridContainer:
var grid := GridContainer.new()
grid.columns = 2
grid.add_theme_constant_override("h_separation", UIScale.px(8))
@ -82,14 +55,9 @@ func _knopf_raster(oids: Array, bau: bool) -> GridContainer:
btn.custom_minimum_size = UIScale.v2(148, 44)
btn.clip_text = true
var poly := int((lib.get(oid, {}) as Dictionary).get("poly", 0))
if bau:
btn.text = BauKatalog.label(oid)
btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid),
str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly]
else:
var name_de := str((lib.get(oid, {}) as Dictionary).get("name_de", oid))
btn.text = name_de
btn.tooltip_text = "%s — %d Dreiecke" % [name_de, poly]
btn.text = BauKatalog.label(oid)
btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid),
str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly]
btn.pressed.connect(_on_teil_gedrueckt.bind(str(oid), btn))
bau_buttons[str(oid)] = btn
grid.add_child(btn)

View file

@ -16,7 +16,7 @@ var spawn_delete_btn: Button
static func spec() -> Dictionary:
return {"titel": "Prüfen", "gruppe": "rechts", "order": 50}
return {"titel": "Spawn-Zonen", "gruppe": "map", "order": 10}
func panel_title() -> String:
@ -27,6 +27,7 @@ func panel_title() -> String:
## Live-Status IMMER sichtbar + Werkzeug-Button + Bedien-Panel (Team/Radius/
## Auswahl/Löschen). Ein/Aus des Werkzeugs bleibt beim Koordinator (main.gd).
func build() -> void:
add_child(_header_label("Spawn-Zonen"))
# --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar ---
spawn_status = Label.new()
spawn_status.text = ""

View file

@ -25,6 +25,27 @@ func panel_title() -> String:
return "Panel"
## Lange Tabs (Map, Bauen): den ganzen Inhalt in einen senkrechten Scroll-Bereich
## legen, damit die rechte Spalte nicht mit dem Inhalt wächst. Rückgabe = die
## innere Box (dort hängen spätere Abschnitte an).
func wrap_in_scroll() -> VBoxContainer:
var scroll := ScrollContainer.new()
scroll.name = "%sScroll" % name
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
var inner := VBoxContainer.new()
inner.name = "%sInhalt" % name
inner.size_flags_horizontal = Control.SIZE_EXPAND_FILL
inner.add_theme_constant_override("separation", UIScale.px(8))
for child in get_children():
remove_child(child)
inner.add_child(child)
scroll.add_child(inner)
add_child(scroll)
return inner
# --- Farb-Zugriff NUR über das Theme (Karte t_2336c58e/U7) ------------------
## Farben kommen ausschließlich aus theme/editor_theme.tres (Abschnitt
## Editor/colors/…) — keine Farbwerte mehr im Panel-Code. Der Wert liest

View file

@ -15,6 +15,7 @@ var sky_preview: TextureRect # Vorschau der gewählten Skybox (obere Panora
var prop_water: HSlider
var terrain_info: Label
var _sections: VBoxContainer # Inhalt des Map-Tabs (im Scroll-Bereich)
var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder
## Karte t_28fdef03: Slider-Stellung für „Kein Wasser“ (Meta-Vertrag für
@ -110,6 +111,13 @@ func build() -> void:
prop_weather.item_selected.connect(_on_weather_changed)
prop_sky.item_selected.connect(_on_sky_changed)
prop_water.value_changed.connect(_on_water_changed)
_sections = wrap_in_scroll()
## Hier hängen die Abschnitte der Registry-Gruppe "map" (Spawn-Zonen, Test-Start,
## Symmetrie) unter den Map-Eigenschaften.
func sections_host() -> Control:
return _sections
func _on_prop_changed(text: String, field: String) -> void:

View file

@ -1,33 +0,0 @@
extends "res://src/ui/panels/editor_panel.gd"
## Objekt-Werkzeuge (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik
## unverändert): „Alles auf den Boden setzen" + Raster-Snap-Schalter. Vor U1
## wohnten diese beiden Widgets in der linken Spalte unter der Bibliothek;
## sie sind als eigener Registry-Bereich (Gruppe "unten") erhalten geblieben,
## damit nichts verschwindet.
var snap_btn: Button
var grid_check: CheckButton
static func spec() -> Dictionary:
return {"titel": "Objekt-Werkzeuge", "gruppe": "unten", "order": 20}
func panel_title() -> String:
return "Objekt-Werkzeuge"
func build() -> void:
snap_btn = Button.new()
snap_btn.text = "⚡ Alles auf den Boden setzen"
snap_btn.custom_minimum_size = UIScale.v2(0, 44)
snap_btn.tooltip_text = "Setzt ALLE Objekte auf die Terrain-Höhe — z. B. nach dem Terrain-Formen, wenn etwas in der Luft hängt"
snap_btn.pressed.connect(func() -> void: main.call("_on_snap_to_ground"))
add_child(snap_btn)
grid_check = CheckButton.new()
grid_check.text = "Raster-Snap (1 m)"
grid_check.custom_minimum_size = UIScale.v2(0, 40)
grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein"
grid_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed))
add_child(grid_check)

View file

@ -1 +0,0 @@
uid://cs4xao3sf5y7n

View file

@ -12,7 +12,7 @@ extends RefCounted
## in derselben Datei (gleiche Falle wie früher bei match_hud.gd).
##
## Struktur je Eintrag: titel (Tab-/Überschrift), gruppe ("rechts" = rechtes
## TabControl, "links" = linke Spalte, "unten" = Assets-Bereich, "toolbar" /
## TabControl, "links" = linke Spalte, "unten" = Assets-Bereich, "map" = Abschnitte im Map-Tab, "dialog" = Einstellungen-Fenster, "toolbar" /
## "status" = Leisten-Aktionen), order (Sortier-Schlüssel), skript (preload-Pfad
## des Panel-Moduls). Ein Bereich erscheint GENAU EINMAL im Baum (test_ui_layout
## prüft das gegen Doppel-Namen).
@ -28,8 +28,6 @@ const PANELS: Array = [
# --- rechts: Tab-Gruppe (Unity-Inspector-Spalte) ---
{"id": "inspektor", "titel": "Inspektor", "gruppe": "rechts", "order": 10,
"skript": "res://src/ui/inspector_panel.gd"},
{"id": "auswahl", "titel": "Auswahl", "gruppe": "rechts", "order": 20,
"skript": "res://src/ui/panels/selection_panel.gd"},
{"id": "map", "titel": "Map", "gruppe": "rechts", "order": 30,
"skript": "res://src/ui/panels/map_panel.gd"},
{"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40,
@ -39,28 +37,26 @@ const PANELS: Array = [
"skript": "res://src/ui/panels/malen_panel.gd"},
{"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47,
"skript": "res://src/ui/panels/bauen_panel.gd"},
{"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50,
# --- map: Abschnitte UNTER den Map-Eigenschaften (Tab „Map“) ---
# 25.10. (Test-Befund): Prüfen/Test-Start/Symmetrie sind keine eigenen Tabs
# mehr. Spawn-Zonen, Test-Start-Info und Symmetrie wohnen im Map-Tab.
{"id": "check", "titel": "Spawn-Zonen", "gruppe": "map", "order": 10,
"skript": "res://src/ui/panels/check_panel.gd"},
{"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60,
{"id": "teststart", "titel": "Test-Start", "gruppe": "map", "order": 20,
"skript": "res://src/ui/panels/test_start_panel.gd"},
# T3 Komfort (Karte t_aaff5dcc): Maus-Empfindlichkeit u. a. — Registry-Weg
# statt main.gd (Operator-Vorgabe 20.10.).
{"id": "einstellungen", "titel": "Einstellungen", "gruppe": "rechts", "order": 70,
"skript": "res://src/ui/panels/settings_panel.gd"},
# M5 (Karte t_60014669): Ein-Klick „Karte symmetrisch machen“ + Check mit
# Klartext-Bericht — ebenfalls Registry-Weg, main.gd bleibt unangetastet.
{"id": "symmetrie", "titel": "Symmetrie", "gruppe": "rechts", "order": 80,
{"id": "symmetrie", "titel": "Symmetrie", "gruppe": "map", "order": 30,
"skript": "res://src/ui/panels/symmetry_panel.gd"},
# --- dialog: Inhalt des Einstellungen-Fensters (Knopf in der Kopfleiste) ---
# T3 Komfort (Karte t_aaff5dcc) + 25.10.: Gitter, Höhenlinien, Symmetrie-
# Modus, UI-Größe, Raster-Snap, Gizmo-Schritt, Karte prüfen.
{"id": "einstellungen", "titel": "Einstellungen", "gruppe": "dialog", "order": 10,
"skript": "res://src/ui/panels/settings_panel.gd"},
# --- links: Vorbereitet für U2 (Szenen-Baum); Bibliothek ist UNTEN ---
{"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10,
"skript": "res://src/ui/panels/scene_tree_panel.gd"},
# --- unten: Objekt-Bibliothek / Assets (Unity-Projekt-Fenster-Platz) ---
{"id": "assets", "titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10,
"skript": "res://src/ui/panels/assets_panel.gd"},
# Objekt-Werkzeuge (Boden-Setzen/Raster-Snap, vor U1 links) — erhalten
# geblieben als eigener Tab unten, damit nichts verschwindet.
{"id": "werkzeuge", "titel": "Objekt-Werkzeuge", "gruppe": "unten", "order": 20,
"skript": "res://src/ui/panels/object_tools_panel.gd"},
]
## Leisten-Aktionen (Toolbar-Buttons/Menüs — KEINE Panels, melden sich ditto

View file

@ -5,7 +5,10 @@ extends "res://src/ui/panels/editor_panel.gd"
## "links"), Zustand/Signale wie vorher.
var content_tree: Tree
var group_btn: Button
var ungroup_btn: Button
var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum
var _tree_select_pending := false # multi_selected gesammelt, Auswertung steht aus
var _obj_index_map: Dictionary = {} # obj_index -> TreeItem (schnelle Zwei-Wege-Kopplung)
# U3 (Karte t_a4bf9b8c): Kontextmenü, Umbenennen, Lösch-Rückfrage, Drag&Drop.
@ -30,9 +33,35 @@ func build() -> void:
content_tree.allow_reselect = true
content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2)
content_tree.hide_root = true
# FIX 25.10. (Test-Befund „Liste anklicken wählt in der Szene nichts aus“):
# Im Modus SELECT_MULTI feuert Godot NIE item_selected, sondern nur
# multi_selected (pro Zeile). Beide anschließen; das Sammeln übernimmt der
# Aufrufer-Merker, damit ein Klick mit mehreren Signalen nur EINMAL wirkt.
content_tree.item_selected.connect(_on_tree_selected)
content_tree.multi_selected.connect(_on_tree_multi_selected)
add_child(content_tree)
# Gruppen verwalten (25.10.: der Tab „Auswahl“ ist weg): mehrere Objekte in der
# Liste (Strg+Klick) oder in der Szene markieren, dann hier gruppieren/lösen.
# Rechtsklick auf eine Zeile bietet dieselben Aktionen.
var group_row := HBoxContainer.new()
group_row.add_theme_constant_override("separation", UIScale.px(8))
add_child(group_row)
group_btn = Button.new()
group_btn.text = "🔗 Gruppieren"
group_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
group_btn.custom_minimum_size = UIScale.v2(0, 40)
group_btn.tooltip_text = "Markierte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam. Mehrere markieren: Strg+Klick in der Liste oder Rahmen in der Szene."
group_btn.pressed.connect(func() -> void: main.interaction.group_selection())
group_row.add_child(group_btn)
ungroup_btn = Button.new()
ungroup_btn.text = "⛓ Gruppe lösen"
ungroup_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
ungroup_btn.custom_minimum_size = UIScale.v2(0, 40)
ungroup_btn.tooltip_text = "Die Gruppe der Auswahl auflösen (Strg+Umschalt+G) — die Objekte bleiben, nur die Bindung löst sich"
ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
group_row.add_child(ungroup_btn)
# U3 (Karte t_a4bf9b8c): Rechtsklick-Kontextmenü + Drag&Drop für Gruppen.
content_tree.gui_input.connect(_on_tree_gui_input)
# Tree.set_drag_forwarding: die drei Virtuals (_get_drag_data/_can_drop_data/
@ -61,6 +90,20 @@ func build() -> void:
add_child(_separator())
## multi_selected kommt je Zeile (auch beim Abwählen): die Auswahl EINMAL pro
## Frame auswerten, nicht pro Signal.
func _on_tree_multi_selected(_item: TreeItem, _column: int, _selected: bool) -> void:
if _tree_select_lock or _tree_select_pending:
return
_tree_select_pending = true
_apply_tree_selection.call_deferred()
func _apply_tree_selection() -> void:
_tree_select_pending = false
_on_tree_selected()
## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt.
## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl,
## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln

View file

@ -1,139 +1,21 @@
extends "res://src/ui/panels/editor_panel.gd"
## Auswahl-Panel (U1, Karte t_eaccbfed — aus main.gd ausgezogen, Logik
## UNVERÄNDERT von dort übernommen): Buttons Drehen/Löschen/Duplizieren/
## Spiegeln/Anheben/Absenken + Gruppen-Buttons + Gizmo-Schrittweite.
extends Node
## Auswahl-Logik (U1, Karte t_eaccbfed — aus main.gd ausgezogen). Seit 25.10.
## KEIN Tab mehr (Test-Befund): Drehen/Anheben/Duplizieren/Löschen laufen in der
## 3D-Ansicht (Werkzeugleiste, Gizmo, Tasten), Gruppen im Map-Inhalt links, die
## Gizmo-Schrittweite im Einstellungen-Fenster. Übrig bleibt die Synchronisation:
## Inspektor nachziehen, Gizmo-Schrittweite-Feld folgen, Statuszeile zur frischen
## Auswahl (Klartext + Poly-Budget-Warnung).
##
## Verdrahtung: main.gd ruft sync_selection() an allen Auswahl-Wegen
## (vorher: _sync_selection_panel) und selection_status(idx) bei frischer
## Auswahl (vorher: _selection_status). Alle Objekt-Operationen laufen über
## main.interaction — der Vertrag zu interaction.gd bleibt identisch.
var sel_label: Label
var sel_rotate_btn: Button
var sel_delete_btn: Button
var sel_dup_btn: Button
var sel_mirror_x_btn: Button
var sel_mirror_z_btn: Button
var sel_raise_btn: Button
var sel_lower_btn: Button
var sel_group_btn: Button
var sel_ungroup_btn: Button
var gizmo_step_box: SpinBox
var main # src/ui/main.gd (script-erweitert → untypisiert)
static func spec() -> Dictionary:
return {"titel": "Auswahl", "gruppe": "rechts", "order": 20}
func panel_title() -> String:
return "Auswahl"
func build() -> void:
# --- Auswahl (Anfassen/Greifen) — 1:1 aus der alten _build_right_panel ---
sel_label = Label.new()
sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an."
sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(sel_label)
var sel_btns := HBoxContainer.new()
sel_btns.add_theme_constant_override("separation", UIScale.px(8))
add_child(sel_btns)
sel_rotate_btn = Button.new()
sel_rotate_btn.text = "Drehen (R)"
sel_rotate_btn.custom_minimum_size = UIScale.v2(140, 40)
sel_rotate_btn.tooltip_text = "Ausgewähltes Objekt 15° drehen (auch: Mausrad)"
sel_rotate_btn.pressed.connect(func() -> void: main.interaction.rotate_selected(15.0))
sel_btns.add_child(sel_rotate_btn)
sel_delete_btn = Button.new()
sel_delete_btn.text = "Löschen (Entf)"
sel_delete_btn.custom_minimum_size = UIScale.v2(140, 40)
sel_delete_btn.tooltip_text = "Ausgewähltes Objekt löschen (Strg+Z holt es zurück)"
sel_delete_btn.pressed.connect(main.interaction.delete_selected)
sel_btns.add_child(sel_delete_btn)
# Objekt-Ops (Karte t_7567a06f): Duplizieren · Spiegeln · Anheben/Absenken.
var dup_row := HBoxContainer.new()
dup_row.add_theme_constant_override("separation", UIScale.px(8))
add_child(dup_row)
sel_dup_btn = _sel_op_button("⧉ Duplizieren (Strg+D)",
"Legt eine Kopie daneben — leicht versetzt, direkt angewählt. Strg+D geht auch.",
func() -> void: main.interaction.duplicate_selected())
dup_row.add_child(sel_dup_btn)
var mirror_row := HBoxContainer.new()
mirror_row.add_theme_constant_override("separation", UIScale.px(8))
add_child(mirror_row)
sel_mirror_x_btn = _sel_op_button("⇄ Links↔Rechts",
"Spiegelt das Objekt an seiner Stelle (links↔rechts, X-Achse) — die Blickrichtung wird mitgespiegelt.",
func() -> void: main.interaction.mirror_selected("x"))
mirror_row.add_child(sel_mirror_x_btn)
sel_mirror_z_btn = _sel_op_button("⇅ Vor↔Zurück",
"Spiegelt das Objekt an seiner Stelle (vor↔zurück, Z-Achse) — die Blickrichtung wird mitgespiegelt.",
func() -> void: main.interaction.mirror_selected("z"))
mirror_row.add_child(sel_mirror_z_btn)
var lift_row := HBoxContainer.new()
lift_row.add_theme_constant_override("separation", UIScale.px(8))
add_child(lift_row)
sel_raise_btn = _sel_op_button("▲ Anheben (Bild↑)",
"Objekt 0,25 m anheben — es schwebt dann, auch wenn der Boden darunter anders ist. Bild↑ geht auch.",
func() -> void: main.interaction.lift_selected(ObjectOps.LIFT_STEP_M))
lift_row.add_child(sel_raise_btn)
sel_lower_btn = _sel_op_button("▼ Absenken (Bild↓)",
"Objekt 0,25 m absenken — stoppt am Boden (nichts wird in Hügel begraben). Bild↓ geht auch.",
func() -> void: main.interaction.lift_selected(-ObjectOps.LIFT_STEP_M))
lift_row.add_child(sel_lower_btn)
# Gruppen-Buttons (Karte t_f6f29d86): Mehrfachauswahl → Gruppe
var group_btns := HBoxContainer.new()
group_btns.add_theme_constant_override("separation", UIScale.px(8))
add_child(group_btns)
sel_group_btn = Button.new()
sel_group_btn.text = "🔗 Gruppieren"
sel_group_btn.custom_minimum_size = UIScale.v2(150, 40)
sel_group_btn.tooltip_text = "Ausgewählte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam"
sel_group_btn.pressed.connect(func() -> void: main.interaction.group_selection())
group_btns.add_child(sel_group_btn)
sel_ungroup_btn = Button.new()
sel_ungroup_btn.text = "⛓ Gruppe lösen"
sel_ungroup_btn.custom_minimum_size = UIScale.v2(150, 40)
sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich"
sel_ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
group_btns.add_child(sel_ungroup_btn)
# --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen-
# Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die
# Voreinstellungen (fein/normal/grob); dieses Feld setzt sie exakt und
# überlebt UI-Rebuilds, weil der Wert in main.gizmo_step_m liegen bleibt.
add_child(_header_label("Gizmo-Einstellungen"))
var gizmo_row := HBoxContainer.new()
gizmo_row.add_theme_constant_override("separation", UIScale.px(8))
add_child(gizmo_row)
gizmo_row.add_child(_field_label("Schrittweite"))
gizmo_step_box = SpinBox.new()
gizmo_step_box.min_value = 0.01
gizmo_step_box.max_value = 10.0
gizmo_step_box.step = 0.01
gizmo_step_box.value = main.gizmo_step_m
gizmo_step_box.custom_minimum_size = UIScale.v2(110, 30)
gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)."
gizmo_row.add_child(gizmo_step_box)
gizmo_step_box.value_changed.connect(func(v: float) -> void:
main.gizmo_step_m = v
if main.gizmo != null and is_instance_valid(main.gizmo):
main.gizmo.set_step_m(v)
)
add_child(_separator())
## Ein Auswahl-Button mit Klartext-Tipp (aus main.gd übernommen).
func _sel_op_button(text: String, tip: String, action: Callable) -> Button:
var btn := Button.new()
btn.text = text
btn.custom_minimum_size = UIScale.v2(140, 40)
btn.tooltip_text = tip
btn.pressed.connect(action)
return btn
func setup(p_main) -> void:
main = p_main
## Objekt-Inspektor + Auswahl-Panel synchron halten (vorher
@ -145,54 +27,9 @@ func sync_selection() -> void:
# Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad
# WÄHREND eines Zugs kann sie im Gizmo ändern (cycle_step) — das Panel-
# Feld folgt ohne eigene value_changed-Rückkopplung (set_value_no_signal).
if gizmo_step_box != null and is_instance_valid(gizmo_step_box) \
and not gizmo_step_box.has_focus():
gizmo_step_box.set_value_no_signal(main.gizmo_step_m)
var doc: Dictionary = main.doc
var objects: Array = doc.get("objects", [])
var sel: Array = main.interaction.selected_indices
var valid: Array = []
for i in sel:
if i is int and i >= 0 and i < objects.size():
valid.append(i)
var has_sel := not valid.is_empty()
for btn in [sel_rotate_btn, sel_delete_btn, sel_dup_btn, sel_mirror_x_btn,
sel_mirror_z_btn, sel_raise_btn, sel_lower_btn]:
btn.disabled = not has_sel
var has_group := false
for i in valid:
if GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED:
has_group = true
break
sel_group_btn.disabled = valid.size() < 2
sel_ungroup_btn.disabled = not has_group
if not has_sel:
sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an."
return
if valid.size() == 1:
var idx: int = valid[0]
var obj: Dictionary = objects[idx]
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
var lift := ObjectOps.lift_offset(doc, idx)
var lift_text := "am Boden" if absf(lift) < 0.02 else ("%+.2f m über dem Boden" % lift)
var group_line := ""
var gid := GroupOps.group_id(obj)
if gid != GroupOps.UNGROUPED:
group_line = "\nGruppe %d — zieht & dreht GEMEINSAM (Lösen: Button unten)" % (gid + 1)
sel_label.text = "%s (Objekt %d von %d)\nPosition: %.0f, %.0f m · Höhe: %s · Richtung: %.0f°%s\nZiehen: verschieben · R oder Mausrad: drehen (15°)\nBild↑/↓: anheben/absenken · Strg+D: duplizieren\nSpiegeln/Löschen: Buttons oben · Strg+Z: zurück" % [
main.object_label(str(obj.get("id", ""))), idx + 1, objects.size(),
float(pos[0]), float(pos[2]), lift_text, float(obj.get("rot_y_deg", 0.0)), group_line]
return
# Mehrfachauswahl: Liste aller gewählten Objekte (max. 8 Zeilen, dann „…“).
var names := PackedStringArray()
for i in valid:
if names.size() < 8:
names.append(main.object_label(str((objects[i] as Dictionary).get("id", "?"))))
var more := ""
if valid.size() > 8:
more = "\n… und %d weitere" % (valid.size() - 8)
sel_label.text = "%d Objekte ausgewählt:\n%s%s\nGemeinsam: ziehen (Gruppe wandert zusammen), drehen um die Mitte, Entf löscht alle." % [
valid.size(), " · ".join(names), more]
var step_box: SpinBox = main.gizmo_step_box
if step_box != null and is_instance_valid(step_box) and not step_box.has_focus():
step_box.set_value_no_signal(main.gizmo_step_m)
## Hilfs-Meldung zur frischen Auswahl (Klartext + Budget-Warnung + nächster
@ -202,9 +39,6 @@ func selection_status(idx: int) -> void:
# 3D-Klick, Rahmen-Ausziehen und Map-Inhalt-Baum.
if main.inspector != null:
main.inspector.call("refresh")
# Fix (Vorbestand, beim Inspektor-Einbau gefunden): select_object/
# select_many füllten das alte „Auswahl“-Feld NIE — sel_label blieb auf
# „Nichts ausgewählt“ stehen, während die Statuszeile das Objekt nannte.
# sync_selection ändert bewusst KEINEN Statuszeilen-Text (Stummsync).
sync_selection()
var doc: Dictionary = main.doc

View file

@ -1,5 +1,8 @@
extends "res://src/ui/panels/editor_panel.gd"
## Einstellungen-Bereich (rechte Spalte — Karte t_aaff5dcc, T3 Komfort).
## Einstellungen (Fenster „⚙ Einstellungen“ in der Kopfleiste — Karte
## t_aaff5dcc, T3 Komfort; 25.10. Test-Befund: Gitter, Höhenlinien, Symmetrie-
## Modus, UI-Größe, Raster-Snap, Gizmo-Schrittweite und „Karte prüfen“ ziehen
## aus Kopfleiste/Objekt-Werkzeugen hierher).
##
## Registry-Weg (Operator-Vorgabe 20.10.): main.gd bleibt unangetastet, das
## Modul meldet sich nur über src/ui/panels/registry.gd an. Der ZUSTAND lebt
@ -12,9 +15,19 @@ var sens_box: SpinBox
## Eintrag wie Taste V (PlaytestSettings, persistiert).
var perspektive_box: OptionButton
# Ansicht / Platzieren (vorher Kopfleiste und Tab „Objekt-Werkzeuge“). main
# hält dieselben Namen als Referenzen (grid_btn, contours_btn, size_menu, …).
var grid_btn: CheckButton
var contours_btn: CheckButton
var size_menu: OptionButton
var sym_menu: OptionButton
var raster_snap_check: CheckButton
var gizmo_step_box: SpinBox
var check_btn: Button
static func spec() -> Dictionary:
return {"titel": "Einstellungen", "gruppe": "rechts", "order": 70}
return {"titel": "Einstellungen", "gruppe": "dialog", "order": 10}
func panel_title() -> String:
@ -22,6 +35,72 @@ func panel_title() -> String:
func build() -> void:
add_child(_header_label("Ansicht"))
grid_btn = CheckButton.new()
grid_btn.text = "⊞ Maßstabsgitter (100-m-Raster)"
grid_btn.custom_minimum_size = UIScale.v2(0, 36)
grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus."
grid_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_toggled", pressed))
add_child(grid_btn)
contours_btn = CheckButton.new()
contours_btn.text = "≈ Höhenlinien fixieren"
contours_btn.custom_minimum_size = UIScale.v2(0, 36)
contours_btn.tooltip_text = "Höhenlinien (1 m, alle 5 m kräftiger) FIXIERT an/aus. Ohne Fixierung erscheinen sie nur im Terrain-Werkzeug (F1). Taste L schaltet ebenfalls. Bei flachem Terrain (< 2 m) sind sie automatisch aus."
contours_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_contours_toggled", pressed))
add_child(contours_btn)
add_child(_field_label("UI-Größe"))
size_menu = OptionButton.new()
for i in UIScale.step_count():
size_menu.add_item(UIScale.step_label(i), i)
size_menu.custom_minimum_size = UIScale.v2(0, 36)
size_menu.tooltip_text = "Größe der Oberfläche: Klein (wie bisher), Mittel (1,25-fach), Groß (1,5-fach). Wirkt sofort und bleibt gespeichert."
size_menu.item_selected.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
add_child(size_menu)
add_child(_separator())
add_child(_header_label("Bauen & Platzieren"))
raster_snap_check = CheckButton.new()
raster_snap_check.text = "Raster-Snap (1 m)"
raster_snap_check.custom_minimum_size = UIScale.v2(0, 36)
raster_snap_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein (Strg gedrückt halten geht immer)"
raster_snap_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed))
add_child(raster_snap_check)
add_child(_field_label("Gizmo-Schrittweite (m)"))
gizmo_step_box = SpinBox.new()
gizmo_step_box.min_value = 0.01
gizmo_step_box.max_value = 10.0
gizmo_step_box.step = 0.01
gizmo_step_box.value = main.gizmo_step_m
gizmo_step_box.custom_minimum_size = UIScale.v2(0, 36)
gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)."
gizmo_step_box.value_changed.connect(func(v: float) -> void:
main.gizmo_step_m = v
if main.gizmo != null and is_instance_valid(main.gizmo):
main.gizmo.set_step_m(v))
add_child(gizmo_step_box)
add_child(_separator())
add_child(_header_label("Symmetrie & Prüfen"))
add_child(_field_label("Symmetrie-Modus"))
sym_menu = OptionButton.new()
for i in SymmetryCore.MODE_LABELS.size():
sym_menu.add_item("Symmetrie: %s" % SymmetryCore.MODE_LABELS[i], i)
sym_menu.custom_minimum_size = UIScale.v2(0, 36)
sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt."
sym_menu.item_selected.connect(func(index: int) -> void: main.call("_on_sym_mode_selected", index))
add_child(sym_menu)
check_btn = Button.new()
check_btn.text = "✅ Karte prüfen"
check_btn.custom_minimum_size = UIScale.v2(0, 40)
check_btn.tooltip_text = "Prüft, ob die Map bereit für den Export ist — sagt VORHER, was fehlt"
check_btn.pressed.connect(func() -> void: main.call("_on_check_map"))
add_child(check_btn)
add_child(_separator())
add_child(_header_label("Testlauf-Einstellungen"))
add_child(_field_label("Maus-Empfindlichkeit (0,1 – 5,0)"))
@ -67,3 +146,15 @@ func _on_perspektive_changed(index: int) -> void:
var body: Variant = pt.get("body")
if body != null:
body.call("set_perspektive", dritte)
## Schalter aus dem Editor-Zustand nachziehen (beim Öffnen des Fensters).
func refresh() -> void:
if main == null:
return
grid_btn.set_pressed_no_signal(bool(main.get("_grid_on")))
contours_btn.set_pressed_no_signal(bool(main.get("_contours_on")))
raster_snap_check.set_pressed_no_signal(bool(main.get("grid_snap")))
gizmo_step_box.set_value_no_signal(float(main.gizmo_step_m))
size_menu.select(UIScale.step_index())
sym_menu.select(int(main.get("sym_mode")))

View file

@ -29,7 +29,7 @@ var _pending_high := false # Quell-Wahl des laufenden Widerspruchs-Dialogs
static func spec() -> Dictionary:
return {"titel": "Symmetrie", "gruppe": "rechts", "order": 75}
return {"titel": "Symmetrie", "gruppe": "map", "order": 30}
func panel_title() -> String:
@ -108,6 +108,14 @@ func _refresh_half_option() -> void:
half_option.selected = 0
## Vom Einstellungen-Fenster gerufen, wenn der Modus wechselt.
func refresh_mode() -> void:
if mode_label == null or half_option == null:
return
_refresh_half_option()
_refresh_mode_line()
func _on_half_selected(_index: int) -> void:
_refresh_mode_line()
@ -121,7 +129,7 @@ func _refresh_mode_line() -> void:
func _on_apply_pressed() -> void:
var mode := _current_mode()
if mode == SymmetryCore.Mode.OFF:
_say("Der Symmetrie-Modus ist Aus — oben in der Leiste erst Punkt oder eine Achse wählen.")
_say("Der Symmetrie-Modus ist Aus — erst in den Einstellungen (⚙) Punkt oder eine Achse wählen.")
return
if main.doc.is_empty():
_say("Keine Karte offen — erst eine Map anlegen oder laden.", true)
@ -209,7 +217,7 @@ func _apply(quelle_hoch: bool) -> void:
func _on_check_pressed() -> void:
var mode := _current_mode()
if mode == SymmetryCore.Mode.OFF:
_say("Der Symmetrie-Modus ist Aus — erst Punkt oder eine Achse wählen, dann prüfen.")
_say("Der Symmetrie-Modus ist Aus — erst in den Einstellungen (⚙) Punkt oder eine Achse wählen, dann prüfen.")
return
_refresh_mode_line()
var rep := SymmetryOps.check(main.doc, mode)

View file

@ -16,7 +16,7 @@ var info_hint: Label
static func spec() -> Dictionary:
return {"titel": "Test-Start", "gruppe": "rechts", "order": 60}
return {"titel": "Test-Start", "gruppe": "map", "order": 20}
func panel_title() -> String:

View file

@ -28,10 +28,7 @@ func panel_title() -> String:
var undo_btn: Button
var redo_btn: Button
var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014)
var contours_btn: Button # „≈ Höhenlinien“ (Karte t_3ebe4086)
var sym_menu: MenuButton
var size_menu: MenuButton # „UI-Größe“ (Karte t_297221b5)
var settings_btn: Button # „⚙ Einstellungen“ öffnet das Einstellungen-Fenster
var title_label: Label
var file_label: Label
@ -85,45 +82,6 @@ func build() -> void:
redo_btn.pressed.connect(main.call.bind("_do_redo"))
add_child(redo_btn)
# Maßstabsgitter-Toggle (Karte t_7ac70014): über sichtbarem Terrain ist das
# Gitter standardmäßig AUS (das Terrain ist die Bodendarstellung); wer das
# 100-m-Raster als Planungshilfe will, schaltet es hier ohne Rebuild zu.
grid_btn = Button.new()
grid_btn.text = "⊞ Gitter"
grid_btn.toggle_mode = true
grid_btn.button_pressed = false
grid_btn.custom_minimum_size = UIScale.v2(120, 40)
grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus."
grid_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_toggled", pressed))
add_child(grid_btn)
# Höhenlinien-Button (Karte t_3ebe4086, Kopplung t_89c0ec95): drücken =
# LINIEN FIXIEREN (immer sichtbar, in jedem Werkzeug). Ohne Fix sind die
# Linien NUR im Terrain-Werkzeug (F1) sichtbar; auf flachen Maps (< 2 m
# Gesamtrelief) schaltet das Terrain-Mesh zusätzlich selbst auf aus.
contours_btn = Button.new()
contours_btn.text = "≈ Höhenlinien"
contours_btn.toggle_mode = true
contours_btn.button_pressed = false
contours_btn.custom_minimum_size = UIScale.v2(150, 40)
contours_btn.tooltip_text = "Höhenlinien (1 m, alle 5 m kräftiger) FIXIERT an/aus. Ohne Fixierung erscheinen sie nur im Terrain-Werkzeug (F1). Taste L schaltet ebenfalls. Bei flachem Terrain (< 2 m) sind sie automatisch aus."
contours_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_contours_toggled", pressed))
add_child(contours_btn)
# Symmetrie-Schalter (Karte t_f5935023, M1): Aus / Punkt / Achse X / Achse Z.
# Standard = Punktspiegelung (Sly 20.10.). M1 setzt nur Modus + Anzeige;
# das gespiegelte Bauen selbst kommt mit M2–M4.
sym_menu = MenuButton.new()
sym_menu.text = "Symmetrie: Punkt"
sym_menu.flat = false
sym_menu.custom_minimum_size = UIScale.v2(160, 40)
sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt."
var sym_popup := sym_menu.get_popup()
for i in SymmetryCore.MODE_LABELS.size():
sym_popup.add_check_item(SymmetryCore.MODE_LABELS[i], i)
sym_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_sym_mode_selected", index))
add_child(sym_menu)
# Toolbar-Aktionen DATENGETRIEBEN aus der Registry (Operator-Vorgabe
# 20.10.): Karte prüfen, ▶ Karte testen — künftige Karten melden sich
# hier an, ohne main.gd anzufassen. Die Buttons bekommen den Registry-Namen
@ -147,21 +105,15 @@ func build() -> void:
main.set("test_start_btn", btn)
add_child(btn)
# UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um,
# es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt
# in user://scopedvr_editor.cfg gespeichert.
size_menu = MenuButton.new()
size_menu.text = "UI-Größe"
size_menu.flat = false
size_menu.custom_minimum_size = UIScale.v2(120, 40)
size_menu.tooltip_text = "Größe der Oberfläche: Klein (wie bisher), Mittel (1,25-fach), Groß (1,5-fach). Wirkt sofort und bleibt gespeichert."
var size_popup := size_menu.get_popup()
for i in UIScale.step_count():
size_popup.add_check_item(UIScale.step_label(i), i)
# Häkchen-Sync macht der KOORDINATOR nach _pull_toolbar_refs (hier ist
# main.size_menu noch null — alter Reihenfolge-Bug, Karte t_eaccbfed).
size_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
add_child(size_menu)
# Einstellungen (25.10.): Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe,
# Raster-Snap … stehen jetzt in einem eigenen Fenster.
settings_btn = Button.new()
settings_btn.text = "⚙ Einstellungen"
settings_btn.name = "einstellungen_knopf"
settings_btn.custom_minimum_size = UIScale.v2(170, 40)
settings_btn.tooltip_text = "Gitter, Höhenlinien, Symmetrie, UI-Größe, Raster-Snap und Testlauf-Einstellungen"
settings_btn.pressed.connect(func() -> void: main.call("_open_settings"))
add_child(settings_btn)
var help_btn := Button.new()
help_btn.text = "❓ Hilfe"

View file

@ -55,7 +55,7 @@ func _run_tests() -> void:
func _test_catalog() -> void:
print("Katalog")
_check(BauKatalog.KINDS.size() == 12, "12 Bauteile im Katalog")
_check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog")
var in_gruppen := {}
for g in BauKatalog.GRUPPEN:
for oid in (g["oids"] as Array):
@ -187,11 +187,11 @@ func _test_snap_cells_and_edges() -> void:
s = BuildSnap.berechne("bau_fundament", Vector3(-0.3, 0.0, -2.9), 0.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(-1.0, 0.0, -3.0), "Fundament: negative Zelle (−1, −3)")
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 0.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(1.0, 0.0, 0.0) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0")
_check(s["pos"] == Vector3(1.0, 0.0, 0.1) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 10 cm in die Zelle gerückt (bündig)")
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 0.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(0.0, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90")
_check(s["pos"] == Vector3(0.1, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 10 cm in die Zelle gerückt")
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 90.0, kein, _flach(0.0))
_check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.0), "R dreht die Wand um (180°), bleibt auf der Kante")
_check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.1), "R dreht die Wand um (180°), bleibt an ihrer Stelle")
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 90.0, kein, _flach(0.0))
_check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°")
s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0))
@ -200,17 +200,38 @@ func _test_snap_cells_and_edges() -> void:
_check(s["pos"] == Vector3(4.0, 0.0, 1.0) and float(s["rot"]) == 0.0, "Treppe: 4×2-Fläche entlang X")
s = BuildSnap.berechne("bau_treppe", Vector3(1.2, 0.0, 3.3), 90.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(1.0, 0.0, 4.0) and float(s["rot"]) == 90.0, "Treppe: gedreht 90° → entlang Z")
# Alle Fälle: gerasterte Mitten (Wände auf Kanten = ein gerader + ein ungerader Meter)
# Alle Fälle: Wandmitte = 10 cm neben einer Kantenmitte (Wand liegt in der Zelle)
for k in range(60):
var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k)
var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
var wp: Vector3 = w["pos"]
var gerade_x := int(roundf(wp.x)) % 2 == 0
var gerade_z := int(roundf(wp.z)) % 2 == 0
if not (gerade_x != gerade_z):
_check(false, "Wand-Mitte liegt auf Kantenmitte (%s)" % str(wp))
var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.1) < 0.001 # X nahe einer geraden Linie
var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001
var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001
if not ((ex and oz) or (ez and ox)):
_check(false, "Wand-Mitte liegt in der Zelle an der Kante (%s)" % str(wp))
return
_check(true, "60 Zufallsklicks: Wandmitte liegt immer auf einer Kantenmitte")
# und zwar in der Zelle, in der die Maus war (Außenseite bündig auf der Kante)
if (ez and absf(wp.z - p.z) > 1.01) or (ex and absf(wp.x - p.x) > 1.01):
_check(false, "Wand liegt auf der Maus-Seite (%s zu %s)" % [str(wp), str(p)])
return
_check(true, "60 Zufallsklicks: Wand liegt immer in der Zelle der Maus, bündig an einer Kante")
# Ecke: zwei Wände in einer Zellecke schließen sich ohne Lücke (äußere Ecke voll)
var wa := BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0))
var wb := BuildSnap.berechne("bau_wand", Vector3(0.3, 0.0, 1.0), 0.0, kein, _flach(0.0))
var ra := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wa["pos"], "rot": wa["rot"]})
var rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]})
_check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt")
_check(absf(ra.position.x) < 0.001 and absf(ra.position.y) < 0.001 and absf(rb.position.x) < 0.001 and absf(rb.position.y) < 0.001,
"Ecke: beide Wände beginnen bündig an der Außenkante (0,0)")
# Große Teile: Zellzahl bestimmt die Rasterung
s = BuildSnap.berechne("bau_dach_giebel_4", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(4.0, 0.0, 4.0), "Satteldach 4×4: Mitte auf Zellecke (4, 4)")
s = BuildSnap.berechne("bau_dach_pyramide_6", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)")
s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0))
_check(s["pos"] == Vector3(4.0, 0.0, 0.1), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt")
# Gelände: höchste Höhe unter der Grundfläche zählt
s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0))
_check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)")
@ -224,33 +245,47 @@ func _test_snap_levels() -> void:
print("Snap: Ebenen statt Klickhöhe")
var teile: Array = [
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0),
_teil(1, "bau_wand", Vector3(1, 0.2, 0), 0.0),
_teil(2, "bau_wand", Vector3(2, 0.2, 1), 90.0),
_teil(1, "bau_wand", Vector3(1, 0, 0.1), 0.0),
_teil(2, "bau_wand", Vector3(1.9, 0, 1), 90.0),
]
# Klick mitten auf die Seite der Wand (y=1,5): Boden darf NICHT in der Luft landen
var s := BuildSnap.berechne("bau_boden", Vector3(0.9, 1.5, 0.6), 0.0, teile, _flach(0.0))
_check(absf(float(s["ebene"]) - 0.2) < 0.001, "Boden nach Klick auf Wandseite liegt auf dem Fundament (0,2), nicht bei 1,5")
# Klick auf die Oberkante der Wand: nächste Geschoss-Ebene
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 3.2, 0.1), 0.0, teile, _flach(0.0), true, Vector3(0, -0.5, 1))
_check(absf(float(s["ebene"]) - 3.2) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,2")
_check(s["pos"] == Vector3(1.0, 3.2, -1.0), "…und in der Zelle auf der Kameraseite (1, −1)")
# Wand auf Wand (Stapeln)
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, 0.0), 0.0, teile, _flach(0.0))
_check(absf(float(s["ebene"]) - 3.2) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante")
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 3.0, 0.1), 0.0, teile, _flach(0.0), true, Vector3(0, -0.5, 1))
_check(absf(float(s["ebene"]) - 3.0) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,0")
_check(s["pos"] == Vector3(1.0, 3.0, 1.0), "…und in der Zelle, in der die Wand steht (1, 1) — die Decke liegt auf der Wand")
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 1.0, 1.0), 0.0, [_teil(9, "bau_pfeiler", Vector3(2, 0, 2), 0.0)], _flach(0.0), true, Vector3(0, -0.5, 1))
_check(s["pos"].z == 1.0, "Treffer auf Nicht-Wand: Zelle der Kameraseite (Pfeiler an der Ecke)")
# Wand auf Wand (Stapeln): genau über der Wand darunter
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.0, 0.1), 0.0, teile, _flach(0.0), true)
_check(absf(float(s["ebene"]) - 3.0) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante (3,0)")
_check(s["pos"] == Vector3(1.0, 3.0, 0.1), "…und exakt über der Wand darunter (gleiche Lage)")
# Test-Befund 25.10.: obere Wände sitzen gleich hoch, egal ob auf der Nachbarwand
# eine Decke liegt oder nicht (Decke liegt AUF der Wand, Wände zählen ab Deckenunterkante).
var mit_decke: Array = teile.duplicate()
mit_decke.append(_teil(3, "bau_boden", Vector3(1, 3.0, -1), 0.0)) # Decke über der Wand vorn (Zelle −1)
var ohne := BuildSnap.berechne("bau_wand", Vector3(1.0, 3.0, 0.1), 0.0, mit_decke, _flach(0.0), true)
var mit := BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, -0.1), 0.0, mit_decke, _flach(0.0), true)
_check(absf(float(ohne["ebene"]) - 3.0) < 0.001, "Obergeschoss-Wand ohne Decke: Höhe 3,0 (%.2f)" % float(ohne["ebene"]))
_check(absf(float(mit["ebene"]) - 3.0) < 0.001, "Obergeschoss-Wand AUF der Decke: ebenfalls 3,0 (%.2f)" % float(mit["ebene"]))
# Ein Dach ist kein Träger: nichts darf auf der Dachspitze landen
teile.append(_teil(3, "bau_dach_giebel", Vector3(1, 3.2, 1), 0.0))
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.2, 1.0), 0.0, teile, _flach(0.0))
_check(float(s["ebene"]) <= 3.2 + 0.001, "Dachspitze (4,2) ist keine Ebene (Ebene %.2f)" % float(s["ebene"]))
teile.append(_teil(4, "bau_dach_giebel", Vector3(1, 3.0, 1), 0.0))
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.0, 1.0), 0.0, teile, _flach(0.0))
_check(float(s["ebene"]) <= 3.0 + 0.001, "Dachspitze (4,0) ist keine Ebene (Ebene %.2f)" % float(s["ebene"]))
func _test_replace_and_stack() -> void:
print("Ersetzen / doppelt")
var teile: Array = [
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0),
_teil(1, "bau_wand", Vector3(1, 0.2, 0), 0.0),
_teil(1, "bau_wand", Vector3(1, 0, 0.1), 0.0),
]
var s := BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0))
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür in vorhandene Wand → ersetzt Teil 1")
# Klick direkt auf die Wand (Objekt-Treffer) übernimmt ihre Lage, auch von der anderen Seite
s = BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.0), 0.0, teile, _flach(0.0), true, Vector3(0, 0, 1))
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür: Klick auf die Außenseite der Wand ersetzt dieselbe Wand")
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0))
_check(not bool(s["ok"]) and str(s["grund"]).contains("schon"), "gleiche Wand nochmal → abgelehnt mit Erklärung")
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 90.0, teile, _flach(0.0))
@ -283,13 +318,13 @@ func _test_ops_group_undo() -> void:
_check(BauOps.teile_zahl(doc) == 1, "teile_zahl = 1")
# Wand setzen, dann Tür ersetzt dieselbe Stelle (Index bleibt)
var teile := BuildSnap.sammle_teile(doc)
var ws := BuildSnap.berechne("bau_wand", Vector3(1, 0.2, 0.05), 0.0, teile, _flach(0.0))
var ws := BuildSnap.berechne("bau_wand", Vector3(1, 0.2, 0.15), 0.0, teile, _flach(0.0))
var before2 := hist.snapshot(["objects", "group_names"])
var wr := BauOps.setze_teil(doc, "bau_wand", ws, gid, SymmetryCore.Mode.OFF)
hist.commit("Wand bauen", before2, hist.snapshot(["objects", "group_names"]))
var widx := int(wr["index"])
teile = BuildSnap.sammle_teile(doc)
var ts := BuildSnap.berechne("bau_wand_tuer", Vector3(1, 1.5, 0.05), 0.0, teile, _flach(0.0))
var ts := BuildSnap.berechne("bau_wand_tuer", Vector3(1, 1.5, 0.15), 0.0, teile, _flach(0.0))
_check(int(ts["ersetze"]) == widx, "Tür-Klick findet die Wand")
var before3 := hist.snapshot(["objects", "group_names"])
var tr := BauOps.setze_teil(doc, "bau_wand_tuer", ts, gid, SymmetryCore.Mode.OFF)
@ -314,7 +349,7 @@ func _test_ops_group_undo() -> void:
# Haus komplett
var d2 := _neue_doc()
var gid2 := BauOps.haus_demo(d2, null, Vector3(4.0, 0.0, 4.0))
_check(BauOps.teile_zahl(d2) >= 16, "Demo-Haus: %d Teile über die echte Snap-Logik" % BauOps.teile_zahl(d2))
_check(BauOps.teile_zahl(d2) >= 13, "Demo-Haus: %d Teile über die echte Snap-Logik" % BauOps.teile_zahl(d2))
_check(BauOps.gebaeude_von(d2, 0).size() == BauOps.teile_zahl(d2), "alle Teile des Hauses in einer Gruppe (%d)" % gid2)
var doppelt := {}
var hat_doppelte := false

View file

@ -0,0 +1,99 @@
extends SceneTree
## test_drag_bibliothek (Test-Befund 25.10.): Objekt-Kachel anklicken, gedrückt
## halten, in die Szene ziehen — beim Loslassen wird das Objekt platziert.
## Echte Mausereignisse (push_input), kein Direktaufruf.
var checks := 0
var failures := 0
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: ", label)
else:
failures += 1
print(" FAIL: ", label)
func _mouse(pos: Vector2, pressed: bool, motion := false) -> void:
var ev: InputEvent
if motion:
var mm := InputEventMouseMotion.new()
mm.position = pos
mm.global_position = pos
mm.button_mask = MOUSE_BUTTON_MASK_LEFT
ev = mm
else:
var mb := InputEventMouseButton.new()
mb.button_index = MOUSE_BUTTON_LEFT
mb.position = pos
mb.global_position = pos
mb.pressed = pressed
ev = mb
root.push_input(ev)
func _init() -> void:
var main: Control = (load("res://scenes/main.tscn") as PackedScene).instantiate()
root.add_child(main)
await process_frame
await process_frame
await process_frame
main.sym_mode = SymmetryCore.Mode.OFF # sonst setzt der Standard (Punkt) einen Zwilling dazu
var n0: int = (main.doc["objects"] as Array).size()
var oid := "barrel_001"
var btn: Button = main.library_buttons.get(oid)
_check(btn != null, "Kachel für %s vorhanden" % oid)
if btn == null:
_finish()
return
# Kachel sichtbar machen: Bibliotheks-Gruppen aufklappen, Scroll an die Kachel.
main.assets_mod._on_library_expand_all()
await process_frame
await process_frame
# Headless-Fenster ist 64×64 px: echte Mausereignisse erreichen weder Kachel
# noch Szene. Darum die drei Stationen der Ziehen-Kette einzeln und exakt wie
# Godot sie aufruft (drag_forwarding → can_drop (je Mausbewegung) → drop).
var data: Variant = main.assets_mod.call("_tile_drag_data", Vector2(5, 5), oid, btn)
_check(data is Dictionary and str((data as Dictionary).get("svr_oid", "")) == oid, "Ziehen-Start liefert die Objekt-ID")
var pos_a := Vector2(300, 150)
var pos_b := Vector2(320, 170)
_check(main.call("_viewport_can_drop", pos_a, data), "Über der Szene: Ablegen erlaubt")
_check(main.interaction.placing and main.interaction.place_oid == oid, "Über der Szene: Platzier-Vorschau läuft")
_check(main.call("_viewport_can_drop", pos_b, data), "Mausbewegung: weiter erlaubt")
_check(main.interaction.placing, "Vorschau wird nicht neu gestartet")
main.call("_viewport_drop", pos_b, data)
await process_frame
var objs: Array = main.doc["objects"]
_check(objs.size() == n0 + 1, "Loslassen setzt genau EIN Objekt (%d → %d)" % [n0, objs.size()])
if objs.size() == n0 + 1:
_check(str((objs.back() as Dictionary).get("id", "")) == oid, "Das gesetzte Objekt ist %s" % oid)
_check(int(main.interaction.selected_index) == n0, "Das neue Objekt ist ausgewählt")
_check(not main.interaction.placing, "Danach ist das Platzieren beendet (kein hängender Geist)")
_check(main.history.can_undo(), "Undo-Schritt vorhanden")
main.history.undo()
main.call("_after_structure_change")
_check((main.doc["objects"] as Array).size() == n0, "Strg+Z nimmt es zurück")
main.call("_clear_edit_state")
# Abbruch: Ziehen über der Szene, dann außerhalb loslassen (DRAG_END ohne Drop).
data = main.assets_mod.call("_tile_drag_data", Vector2(5, 5), oid, btn)
main.call("_viewport_can_drop", pos_a, data)
_check(main.interaction.placing, "Abbruch-Zug: Vorschau läuft")
main.assets_mod.notification(Node.NOTIFICATION_DRAG_END)
_check(not main.interaction.placing, "Außerhalb losgelassen: Vorschau weg")
_check((main.doc["objects"] as Array).size() == n0, "Außerhalb losgelassen: nichts gesetzt")
# Fremde Ziehdaten werden abgelehnt.
_check(not main.call("_viewport_can_drop", pos_a, "irgendwas"), "Fremde Ziehdaten: kein Ablegen")
_finish()
func _finish() -> void:
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)

View file

@ -0,0 +1 @@
uid://caa44ddv8g124

View file

@ -113,9 +113,8 @@ func _t1_einzelauswahl() -> void:
_check(str(insp.widget("rot_y").edit.text).contains("0.0"), "T1 rot_y zeigt 0.0")
# Auswahl-Beweis per Klick im Viewport-Kontext: Panel ist sichtbar.
_check(insp.get("_grid").visible, "T1 Felder sichtbar bei Auswahl")
# Fix Vorbestand: das alte „Auswahl“-Feld füllt sich jetzt auch bei
# select_object (Vorher stand dort dauerhaft „Nichts ausgewählt.“).
_check(str(main.get("sel_label").text).contains("Fass"), "T1 altes Auswahl-Feld füllt bei 3D-Klick-Weg")
# Das alte „Auswahl“-Feld ist weg (25.10.): der Inspektor ist die einzige Anzeige.
_check(main.get("sel_label") == null, "T1 kein altes Auswahl-Feld mehr (Tab Auswahl entfernt)")
func _t2_tippen() -> void:

View file

@ -51,6 +51,7 @@ func _init() -> void:
await _t1_baum_fuellt_sich()
await _t2_klick_setzt_auswahl()
await _t2b_echter_klick()
await _t3_auswahl_3d_markiert_baum()
await _t4_mehrfach_gespiegelt()
await _t5_gruppen_ebene()
@ -118,6 +119,44 @@ func _t2_klick_setzt_auswahl() -> void:
_check(int(sel) == 1, "T2 Klick im Baum setzt Auswahl in 3D (Index 1 — hat %s)" % str(sel))
## FIX 25.10.: Im Modus SELECT_MULTI feuert Godot nie item_selected — der Test
## oben simulierte einen Weg, den es in der Praxis nicht gab. Hier ein ECHTER
## Mausklick (Eingabe-Ereignis) auf eine Zeile.
func _t2b_echter_klick() -> void:
var tree: Tree = tree_mod.get("content_tree")
main.interaction.clear_selection()
root.size = Vector2i(1600, 900)
await process_frame
await process_frame
var item0: TreeItem = null
for kat in _find_child(tree.get_root(), "Objekte (3)").get_children():
for oi in _obj_items_under(kat):
if int(oi.get_meta("obj_index")) == 2:
item0 = oi
_check(item0 != null, "T2b Zeile für Objekt-Index 2 gefunden")
if item0 == null:
return
tree.scroll_to_item(item0)
await process_frame
var r := tree.get_item_area_rect(item0, 0)
var pos := tree.get_global_position() + r.position + Vector2(r.size.x * 0.5, r.size.y * 0.5)
_check(tree.get_global_rect().has_point(pos), "T2b Zeile liegt im sichtbaren Baum-Rechteck")
var ev := InputEventMouseButton.new()
ev.button_index = MOUSE_BUTTON_LEFT
ev.position = pos
ev.global_position = pos
ev.pressed = true
root.push_input(ev)
await process_frame
var up := ev.duplicate()
up.pressed = false
root.push_input(up)
await process_frame
await process_frame
_check(int(main.interaction.get("selected_index")) == 2,
"T2b ECHTER Klick auf die Listenzeile wählt das Objekt in der Szene (Index 2 — hat %s)" % str(main.interaction.get("selected_index")))
func _t3_auswahl_3d_markiert_baum() -> void:
var tree: Tree = tree_mod.get("content_tree")
var imap: Dictionary = tree_mod.get("_obj_index_map")

View file

@ -27,6 +27,13 @@ func _check(cond: bool, what: String) -> void:
print(" FAIL: ", what)
func _count_named(node: Node, wanted: String) -> int:
var n := 1 if str(node.name) == wanted and not (node is BaseButton) else 0
for c in node.get_children():
n += _count_named(c, wanted)
return n
func _init() -> void:
print("Testgruppe: Layout aus der Registry")
var scene: PackedScene = load("res://scenes/main.tscn")
@ -67,6 +74,11 @@ func _init() -> void:
_check(ids.size() == registry_script.PANELS.size(), "Registry: IDs eindeutig (keine Duplikate in der Tabelle)")
_check(not ids.is_empty(), "Registry: mindestens ein Bereich angemeldet")
var embedded_ids: Array = []
for entry in registry_script.PANELS:
if str(entry["gruppe"]) in ["map", "dialog"]:
embedded_ids.append(str(entry["id"]))
# --- 2. Jeder Bereich GENAU EINMAL im Baum, als Tab mit Titel --------------
var tabs_by_name: Dictionary = {} # gruppe → TabContainer
var stack: Array = [main]
@ -91,6 +103,12 @@ func _init() -> void:
_check(tab.get_tab_title(idx) == title,
"Tab '%s' trägt Registry-Titel '%s'" % [cid, title])
for wanted in ids:
if wanted in embedded_ids:
# Map-Abschnitte und Einstellungen-Fenster sind keine Tabs (25.10.):
# GENAU EINMAL im Baum, Registry-Name = Node-Name.
var treffer := _count_named(main, wanted)
_check(treffer == 1, "Bereich '%s' (eingebettet) GENAU EINMAL im Baum (%d)" % [wanted, treffer])
continue
_check(tabs_by_name.get(wanted) != null, "Bereich '%s' aus der Registry im Baum" % wanted)
# --- 3. Tab-Gruppen --------------------------------------------------------
@ -108,6 +126,7 @@ func _init() -> void:
for entry in registry_script.PANELS:
if str(entry["gruppe"]) == "rechts":
expected_right += 1
_check(expected_right == 5, "rechts: genau 5 Tabs (Inspektor, Map, Terrain, Malen, Bauen)")
_check(right.get_child_count() == expected_right,
"rechts: %d Tabs aus der Registry — hat %d" % [expected_right, right.get_child_count()])
_check(right.get_tab_count() == right.get_child_count(),
@ -125,7 +144,7 @@ func _init() -> void:
btab = gc
_check(btab != null, "unten: TabContainer für Assets gefunden")
if btab != null:
_check(btab.get_child_count() == 2, "unten: 2 Tabs (Bibliothek/Werkzeuge) — hat %d" % btab.get_child_count())
_check(btab.get_child_count() == 1, "unten: 1 Tab (Objekt-Bibliothek) — hat %d" % btab.get_child_count())
# --- 4. Assets-Bereich unten vollständig -----------------------------------
var assets: Control = tabs_by_name.get("assets")
@ -136,8 +155,12 @@ func _init() -> void:
"Bibliothek: „Alle auf/zu“ vorhanden")
var lib_box: Node = assets.get("library_box")
_check(lib_box != null and lib_box.get_child_count() > 0, "Bibliothek: Kachel-/Gruppen-Liste gefüllt")
var tools: Control = tabs_by_name.get("werkzeuge")
_check(tools != null and tools.get("grid_check") != null, "Objekt-Werkzeuge: Raster-Snap erhalten (nichts verschwindet)")
_check(tabs_by_name.get("werkzeuge") == null, "Tab „Objekt-Werkzeuge“ ist weg")
var settings: Control = main.find_child("einstellungen", true, false)
_check(settings != null and settings.get("raster_snap_check") != null, "Raster-Snap wohnt im Einstellungen-Fenster")
_check(settings != null and settings.get("grid_btn") != null and settings.get("contours_btn") != null
and settings.get("size_menu") != null and settings.get("sym_menu") != null,
"Einstellungen: Gitter, Höhenlinien, UI-Größe, Symmetrie-Modus")
# --- 5. Theme als einzige Farbquelle ----------------------------------------
var theme_res: Theme = main.get("theme")

View file

@ -128,10 +128,9 @@ func _test_real_ui_scaling() -> void:
_check(int(main.theme.default_font_size) == 16, "Klein: Theme-Schrift 16")
_check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Klein: Undo-Knopf 140×40")
_check(main.redo_btn.custom_minimum_size == Vector2(150, 40), "Klein: Redo-Knopf 150×40")
_check(main.size_menu != null and main.size_menu.text == "UI-Größe", "UI-Größe-Menü in der Top-Bar")
var sp: PopupMenu = main.size_menu.get_popup()
_check(sp.item_count == 3, "Menü hat 3 Einträge")
_check(sp.is_item_checked(0), "Menü-Häkchen auf aktueller Stufe (Klein)")
_check(main.size_menu != null and main.size_menu is OptionButton, "UI-Größe-Auswahl im Einstellungen-Fenster")
_check(main.size_menu.item_count == 3, "Auswahl hat 3 Stufen")
_check(main.size_menu.selected == 0, "Auswahl steht auf aktueller Stufe (Klein)")
# Rebuild-Pfad (derselbe wie beim Menü-Klick) → Mittel:
UIScale.set_step_index(1)
@ -146,7 +145,7 @@ func _test_real_ui_scaling() -> void:
_check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
var thumb := _find_thumb_rect(160)
_check(thumb != null and thumb.custom_minimum_size.x == 160, "Mittel: Thumbnail 128 → 160")
_check(main.size_menu.get_popup().is_item_checked(1), "Menü-Häkchen wandert auf Mittel (neues Popup)")
_check(main.size_menu.selected == 1, "Auswahl wandert auf Mittel (neues Fenster)")
# Groß:
main._on_ui_size_selected(2)
@ -161,7 +160,7 @@ func _test_real_ui_scaling() -> void:
main._on_ui_size_selected(0)
await process_frame
await process_frame
_check(main.size_menu.get_popup().is_item_checked(0), "Menü-Häkchen wieder auf Klein")
_check(main.size_menu.selected == 0, "Auswahl wieder auf Klein")
_check(int(main.theme.default_font_size) == 16, "Zurück auf Klein: Schrift wieder 16")
_check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Zurück auf Klein: Undo-Knopf 140×40")