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>
This commit is contained in:
slyphax 2026-10-25 20:32:32 +01:00
parent ab9a798343
commit 3f0d0f3f33
32 changed files with 865 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) | | 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 | | 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) | | Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) |
Dazu im Tab „Pack-Module“: Container, Hangar-Tore, Betonsperren aus dem Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab,
Military-Desert-Pack (normale Objekte). sondern wie alle Objekte in der Objekt-Bibliothek unten.
## Snap (`src/editor/build_snap.gd`) ## Snap (`src/editor/build_snap.gd`)
* **XZ:** Wände auf die nächste Zellkante (Drehung ergibt sich aus der Kante, `R` * **XZ:** Wände auf die nächste Zellkante — und zwar GANZ in die Zelle, in der die
dreht die Wand um), Böden/Dächer/Fundamente in die Zellmitte, Pfeiler auf die Maus ist (Außenseite bündig mit der Kante, kein Überhang, Ecken schließen ohne
Zellecke, Treppen auf eine 4 × 2-m-Fläche. Zellen sind 2 × 2 m, global ab (0, 0). 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 * **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 der Grundfläche) oder Oberkante eines tragenden Teils darunter. **Wände und
Wandoberkante stapelt das nächste Geschoss. 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 * **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene
Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt. 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", "kategorie": "bauteile",
"name_de": "Satteldach" "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": { "bau_dach_pyramide": {
"poly": 64, "poly": 64,
"collision": { "collision": {
@ -3521,6 +3551,36 @@
"kategorie": "bauteile", "kategorie": "bauteile",
"name_de": "Pyramidendach" "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": { "bau_fundament": {
"poly": 24, "poly": 24,
"collision": { "collision": {
@ -3551,6 +3611,36 @@
"kategorie": "bauteile", "kategorie": "bauteile",
"name_de": "Giebelwand" "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": { "bau_pfeiler": {
"poly": 12, "poly": 12,
"collision": { "collision": {

View file

@ -49,11 +49,15 @@ const GRUPPEN := [
{"key": "grund", "label": "Fundament & Boden", "oids": ["bau_fundament", "bau_boden"]}, {"key": "grund", "label": "Fundament & Boden", "oids": ["bau_fundament", "bau_boden"]},
{"key": "waende", "label": "Wände", "oids": ["bau_wand", "bau_wand_tuer", "bau_wand_fenster", {"key": "waende", "label": "Wände", "oids": ["bau_wand", "bau_wand_tuer", "bau_wand_fenster",
"bau_wand_scharte", "bau_bruestung", "bau_pfeiler"]}, "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"]}, {"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 := [[-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). ## Reihenfolge = Anzeige-Reihenfolge (GRUPPEN nutzt dieselben IDs).
## "traeger": darauf darf gestapelt werden (Oberkante zählt als Ebene). ## "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, "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), "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": { "bau_treppe": {
"label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber", "label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber",
"snap": "treppe", "traeger": false, "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")) 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: static func ist_traeger(oid: String) -> bool:
return bool((KINDS.get(oid, {}) as Dictionary).get("traeger", false)) 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_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], # Wände liegen GANZ in der Zelle, in der die Maus ist (Außenseite bündig mit
["bau_wand_fenster", Vector3(ox + 1, mitte.y, oz - 2), 0.0], # der Kante) — deshalb zielen die Klicks 10 cm ins Hausinnere.
["bau_wand_tuer", Vector3(ox - 1, mitte.y, oz + 2), 0.0], ["bau_wand", Vector3(ox - 1, mitte.y, oz - 1.9), 0.0],
["bau_wand", Vector3(ox + 1, mitte.y, oz + 2), 0.0], ["bau_wand_fenster", Vector3(ox + 1, mitte.y, oz - 1.9), 0.0],
["bau_wand", Vector3(ox - 2, mitte.y, oz - 1), 90.0], ["bau_wand_tuer", Vector3(ox - 1, mitte.y, oz + 1.9), 0.0],
["bau_wand_scharte", Vector3(ox - 2, mitte.y, oz + 1), 90.0], ["bau_wand", Vector3(ox + 1, mitte.y, oz + 1.9), 0.0],
["bau_wand", Vector3(ox + 2, mitte.y, oz - 1), 90.0], ["bau_wand", Vector3(ox - 1.9, mitte.y, oz - 1), 90.0],
["bau_wand", Vector3(ox + 2, 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: for st in schritte:
_demo_setzen(doc, str(st[0]), st[1] as Vector3, float(st[2]), gid, terrain) _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) # Ein großes Satteldach (4×4 m) auf die Wandoberkante.
var oben := mitte.y + 0.2 + BauKatalog.WAND_H var oben := mitte.y + BauKatalog.WAND_H
for cx in [-1.0, 1.0]: _demo_setzen(doc, "bau_dach_giebel_4", Vector3(ox, oben, oz), 0.0, gid, terrain)
for cz in [-1.0, 1.0]:
_demo_setzen(doc, "bau_dach_giebel", Vector3(ox + cx, oben, oz + cz), 0.0, gid, terrain)
if history != null: if history != null:
history.commit("Haus gebaut (Demo)", before, history.snapshot(["objects", "group_names"])) history.commit("Haus gebaut (Demo)", before, history.snapshot(["objects", "group_names"]))
return gid 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 q := int(roundf(dreh / 90.0))
var viertel := ((q % 4) + 4) % 4 var viertel := ((q % 4) + 4) % 4
var p := Vector2(hit.x, hit.z) var p := Vector2(hit.x, hit.z)
# Treffer auf einem Objekt: Zelle auf der Kameraseite wählen (Klick auf die # Treffer auf einem Objekt: Wände liegen GANZ in einer Zelle — die Quer-Achse
# Wandoberkante liegt genau auf der Zellgrenze). # kommt dann von der Wand (Decke/Dach landet in der Zelle der Wand). Bei anderen
if hit_ist_objekt and art != "kante" and kamera_dir != Vector3.ZERO: # Objekten die Zelle auf der Kameraseite wählen (Klick liegt auf einer Grenze).
var d2 := Vector2(kamera_dir.x, kamera_dir.z) var wand_getroffen := {}
if d2.length() > 0.01: if hit_ist_objekt:
p -= d2.normalized() * 0.2 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 mitte := Vector2.ZERO
var rot := 0.0 var rot := 0.0
var r := BauKatalog.RASTER_M var r := BauKatalog.RASTER_M
match art: match art:
"zelle": "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 rot = viertel * 90.0
"ecke": "ecke":
mitte = Vector2(roundf(p.x / r) * r, roundf(p.y / r) * r) 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: else:
mitte = Vector2(floorf(p.x / r) * r + r * 0.5, roundf(p.y / r) * r) mitte = Vector2(floorf(p.x / r) * r + r * 0.5, roundf(p.y / r) * r)
_: # "kante" _: # "kante"
var fx := p.x / r var vorhanden := wand_getroffen
var fz := p.y / r if not vorhanden.is_empty():
var h_mitte := Vector2((floorf(fx) + 0.5) * r, roundf(fz) * r) # Kante entlang X # Klick auf eine vorhandene Wand (Seite = ersetzen, Oberkante = stapeln):
var v_mitte := Vector2(roundf(fx) * r, (floorf(fz) + 0.5) * r) # Kante entlang Z # GENAU ihre Lage übernehmen, nicht neu aus der Klickstelle raten.
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte): var tp: Vector3 = vorhanden["pos"]
mitte = h_mitte mitte = Vector2(tp.x, tp.z)
rot = 0.0 rot = 90.0 if absf(sin(deg_to_rad(float(vorhanden["rot"])))) > 0.5 else 0.0
else: else:
mitte = v_mitte # Wand liegt GANZ IN der Zelle, in der der Mauszeiger ist: Außenseite
rot = 90.0 # 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 += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile)
rot = fposmod(rot, 360.0) rot = fposmod(rot, 360.0)
var quer := absf(sin(deg_to_rad(rot))) > 0.5 # 90°/270°: X- und Z-Ausdehnung getauscht 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): if not tr.intersects(kern):
continue continue
var top: float = (t["pos"] as Vector3).y + BauKatalog.groesse(troid).y 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) ebenen.append(top)
var ebene := _waehle_ebene(ebenen, hit.y) 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": ""} "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. ## 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: static func _waehle_ebene(ebenen: Array, hit_y: float) -> float:
var best := -INF 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) # bleiben hier, damit Tests/Tools denselben Vertrag sehen wie vorher)
var toolbar_mod # src/ui/panels/toolbar.gd var toolbar_mod # src/ui/panels/toolbar.gd
var status_mod # src/ui/panels/status_bar.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 map_mod # src/ui/panels/map_panel.gd
var terrain_mod # src/ui/panels/terrain_panel.gd var terrain_mod # src/ui/panels/terrain_panel.gd
var malen_mod # src/ui/panels/malen_panel.gd (Malen-Tab) 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 sym_label: Label # weiter in status_mod
var undo_btn: Button # weiter in toolbar_mod var undo_btn: Button # weiter in toolbar_mod
var redo_btn: Button # weiter in toolbar_mod var redo_btn: Button # weiter in toolbar_mod
var grid_btn: Button # weiter in toolbar_mod var grid_btn: CheckButton # weiter in settings_mod (Einstellungen-Fenster)
var contours_btn: Button # weiter in toolbar_mod var contours_btn: CheckButton # weiter in settings_mod
var size_menu: MenuButton # weiter in toolbar_mod var size_menu: OptionButton # weiter in settings_mod
var sym_menu: MenuButton # weiter in toolbar_mod var sym_menu: OptionButton # weiter in settings_mod
var prop_name: LineEdit # weiter in map_mod var prop_name: LineEdit # weiter in map_mod
var sel_label: Label # weiter in selection_mod var gizmo_step_box: SpinBox # weiter in settings_mod
var gizmo_step_box: SpinBox # weiter in selection_mod
var library_search: LineEdit # weiter in assets_mod var library_search: LineEdit # weiter in assets_mod
var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit) var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit)
var _library_group_rows: Array = [] # 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 ui_right_panel = right_tabs
right_tabs.tab_changed.connect(_on_right_tab_changed) right_tabs.tab_changed.connect(_on_right_tab_changed)
split.add_child(right_tabs) split.add_child(right_tabs)
_build_map_sections()
# --- Assets unten (Registry-Gruppe "unten", U1 Auftragspunkt 4) --- # --- Assets unten (Registry-Gruppe "unten", U1 Auftragspunkt 4) ---
# Die Objekt-Bibliothek wohnt jetzt UNTER dem Hauptbereich (Unity- # 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: func _pull_toolbar_refs() -> void:
undo_btn = toolbar_mod.undo_btn undo_btn = toolbar_mod.undo_btn
redo_btn = toolbar_mod.redo_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: func _pull_status_refs() -> void:
@ -386,8 +382,6 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
match child.name: match child.name:
"inspektor": "inspektor":
inspector = child inspector = child
"auswahl":
selection_mod = child
"map": "map":
map_mod = child map_mod = child
"terrain": "terrain":
@ -396,12 +390,7 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
malen_mod = child malen_mod = child
"bauen": "bauen":
bauen_mod = child 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 prop_name = map_mod.prop_name
spawn_status = check_mod.spawn_status
terrain_info = map_mod.terrain_info terrain_info = map_mod.terrain_info
# Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die # Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die
# alten Namen als Aliase (Tests/Werkzeug-Vertrag). # 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 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: 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 --- # --- Dialoge ---
new_dialog = preload("res://src/ui/new_map_dialog.gd").new() new_dialog = preload("res://src/ui/new_map_dialog.gd").new()
new_dialog.confirmed.connect(_on_new_map_confirmed) new_dialog.confirmed.connect(_on_new_map_confirmed)
@ -558,11 +613,26 @@ func _build_dialogs_and_overlays() -> void:
add_child(help_overlay) 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: func _build_center_viewport() -> SubViewportContainer:
var container := SubViewportContainer.new() var container := SubViewportContainer.new()
container.stretch = true container.stretch = true
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
container.size_flags_vertical = 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() var viewport := SubViewport.new()
viewport.own_world_3d = true viewport.own_world_3d = true
@ -714,10 +784,7 @@ func _on_menu_action(id: int) -> void:
func _sync_size_menu() -> void: func _sync_size_menu() -> void:
if size_menu == null or not is_instance_valid(size_menu): if size_menu == null or not is_instance_valid(size_menu):
return return
var popup := size_menu.get_popup() size_menu.select(UIScale.step_index())
var cur := UIScale.step_index()
for i in popup.item_count:
popup.set_item_checked(i, i == cur)
## Neue Stufe gewählt: speichern, kompletten UI-Neuaufbau mit dem neuen ## 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() _sync_size_menu()
return return
UIScale.set_step_index(index) UIScale.set_step_index(index)
var settings_war_offen := settings_dialog != null and settings_dialog.visible
_rebuild_ui_for_scale() _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)) _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: if sym_menu == null or sym_label == null:
return return
var label := "Symmetrie: %s" % SymmetryCore.mode_label(sym_mode) var label := "Symmetrie: %s" % SymmetryCore.mode_label(sym_mode)
sym_menu.text = label
sym_label.text = label sym_label.text = label
var popup := sym_menu.get_popup() sym_menu.select(sym_mode)
for i in SymmetryCore.MODE_LABELS.size(): if sym_mod != null and sym_mod.has_method("refresh_mode"):
popup.set_item_checked(i, i == sym_mode) sym_mod.call("refresh_mode")
# --------------------------------------------------------------- History ---- # --------------------------------------------------------------- History ----

View file

@ -42,6 +42,8 @@ var _recent_box: VBoxContainer # Zuletzt-benutzt-Ansicht (im Scroll)
var _recent_grid: GridContainer var _recent_grid: GridContainer
var _recent_tiles: Dictionary = {} # id → Button (Zuletzt-Ansicht, frisch gebaut) var _recent_tiles: Dictionary = {} # id → Button (Zuletzt-Ansicht, frisch gebaut)
var _recent_ids: Array = [] # Reihenfolge: neueste zuerst 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 selected_oid: String = "" # per Klick gewähltes Objekt
var _id_font_normal: int = 0 # ID-Schriftgröße (skaliert), Normalzustand var _id_font_normal: int = 0 # ID-Schriftgröße (skaliert), Normalzustand
var _empty_hint: Label # Leerzustand (U7): kein Treffer / leerer Bereich 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.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)"
btn.pressed.connect(_on_tile_pressed.bind(oid)) btn.pressed.connect(_on_tile_pressed.bind(oid))
btn.gui_input.connect(_on_tile_gui_input.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_entered.connect(_on_tile_hover.bind(oid, true))
btn.mouse_exited.connect(_on_tile_hover.bind(oid, false)) btn.mouse_exited.connect(_on_tile_hover.bind(oid, false))
var tex := LibraryBrowser.load_thumb(oid) var tex := LibraryBrowser.load_thumb(oid)
@ -610,6 +615,65 @@ func _on_tile_gui_input(event: InputEvent, oid: String) -> void:
_activate_tile(oid) _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: func _select_tile(oid: String) -> void:
var changed := selected_oid != oid var changed := selected_oid != oid
selected_oid = oid selected_oid = oid

View file

@ -1,18 +1,11 @@
extends "res://src/ui/panels/editor_panel.gd" extends "res://src/ui/panels/editor_panel.gd"
## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe) plus ein ## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe). Fertige
## paar passende Module aus dem Military-Desert-Pack. Ein Klick auf ein Teil startet ## 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 ## 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). ## 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. ## 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 bau_buttons: Dictionary = {} # oid → Button
var info_label: Label var info_label: Label
var _group: ButtonGroup 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.")) 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: for gruppe in BauKatalog.GRUPPEN:
add_child(_field_label(str(gruppe["label"]))) add_child(_field_label(str(gruppe["label"])))
add_child(_knopf_raster(gruppe["oids"] as Array, true)) add_child(_knopf_raster(gruppe["oids"] as Array))
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(_separator()) add_child(_separator())
add_child(_header_label("Gebäude")) add_child(_header_label("Gebäude"))
info_label = _muted_label("") 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).", 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"))) func() -> void: main.call("_bau_gebaeude_loeschen")))
refresh_info() refresh_info()
wrap_in_scroll()
func _pack_gruppen() -> Array: func _knopf_raster(oids: Array) -> GridContainer:
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:
var grid := GridContainer.new() var grid := GridContainer.new()
grid.columns = 2 grid.columns = 2
grid.add_theme_constant_override("h_separation", UIScale.px(8)) 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.custom_minimum_size = UIScale.v2(148, 44)
btn.clip_text = true btn.clip_text = true
var poly := int((lib.get(oid, {}) as Dictionary).get("poly", 0)) var poly := int((lib.get(oid, {}) as Dictionary).get("poly", 0))
if bau: btn.text = BauKatalog.label(oid)
btn.text = BauKatalog.label(oid) btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid),
btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid), str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly]
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.pressed.connect(_on_teil_gedrueckt.bind(str(oid), btn)) btn.pressed.connect(_on_teil_gedrueckt.bind(str(oid), btn))
bau_buttons[str(oid)] = btn bau_buttons[str(oid)] = btn
grid.add_child(btn) grid.add_child(btn)

View file

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

View file

@ -25,6 +25,27 @@ func panel_title() -> String:
return "Panel" 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) ------------------ # --- Farb-Zugriff NUR über das Theme (Karte t_2336c58e/U7) ------------------
## Farben kommen ausschließlich aus theme/editor_theme.tres (Abschnitt ## Farben kommen ausschließlich aus theme/editor_theme.tres (Abschnitt
## Editor/colors/…) — keine Farbwerte mehr im Panel-Code. Der Wert liest ## 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 prop_water: HSlider
var terrain_info: Label 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 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 ## 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_weather.item_selected.connect(_on_weather_changed)
prop_sky.item_selected.connect(_on_sky_changed) prop_sky.item_selected.connect(_on_sky_changed)
prop_water.value_changed.connect(_on_water_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: 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). ## in derselben Datei (gleiche Falle wie früher bei match_hud.gd).
## ##
## Struktur je Eintrag: titel (Tab-/Überschrift), gruppe ("rechts" = rechtes ## 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 ## "status" = Leisten-Aktionen), order (Sortier-Schlüssel), skript (preload-Pfad
## des Panel-Moduls). Ein Bereich erscheint GENAU EINMAL im Baum (test_ui_layout ## des Panel-Moduls). Ein Bereich erscheint GENAU EINMAL im Baum (test_ui_layout
## prüft das gegen Doppel-Namen). ## prüft das gegen Doppel-Namen).
@ -28,8 +28,6 @@ const PANELS: Array = [
# --- rechts: Tab-Gruppe (Unity-Inspector-Spalte) --- # --- rechts: Tab-Gruppe (Unity-Inspector-Spalte) ---
{"id": "inspektor", "titel": "Inspektor", "gruppe": "rechts", "order": 10, {"id": "inspektor", "titel": "Inspektor", "gruppe": "rechts", "order": 10,
"skript": "res://src/ui/inspector_panel.gd"}, "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, {"id": "map", "titel": "Map", "gruppe": "rechts", "order": 30,
"skript": "res://src/ui/panels/map_panel.gd"}, "skript": "res://src/ui/panels/map_panel.gd"},
{"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40, {"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40,
@ -39,28 +37,26 @@ const PANELS: Array = [
"skript": "res://src/ui/panels/malen_panel.gd"}, "skript": "res://src/ui/panels/malen_panel.gd"},
{"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47, {"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47,
"skript": "res://src/ui/panels/bauen_panel.gd"}, "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"}, "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"}, "skript": "res://src/ui/panels/test_start_panel.gd"},
# T3 Komfort (Karte t_aaff5dcc): Maus-Empfindlichkeit u. a. — Registry-Weg {"id": "symmetrie", "titel": "Symmetrie", "gruppe": "map", "order": 30,
# 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,
"skript": "res://src/ui/panels/symmetry_panel.gd"}, "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 --- # --- links: Vorbereitet für U2 (Szenen-Baum); Bibliothek ist UNTEN ---
{"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10, {"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10,
"skript": "res://src/ui/panels/scene_tree_panel.gd"}, "skript": "res://src/ui/panels/scene_tree_panel.gd"},
# --- unten: Objekt-Bibliothek / Assets (Unity-Projekt-Fenster-Platz) --- # --- unten: Objekt-Bibliothek / Assets (Unity-Projekt-Fenster-Platz) ---
{"id": "assets", "titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10, {"id": "assets", "titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10,
"skript": "res://src/ui/panels/assets_panel.gd"}, "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 ## 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. ## "links"), Zustand/Signale wie vorher.
var content_tree: Tree 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_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) var _obj_index_map: Dictionary = {} # obj_index -> TreeItem (schnelle Zwei-Wege-Kopplung)
# U3 (Karte t_a4bf9b8c): Kontextmenü, Umbenennen, Lösch-Rückfrage, Drag&Drop. # 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.allow_reselect = true
content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2) content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2)
content_tree.hide_root = true 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.item_selected.connect(_on_tree_selected)
content_tree.multi_selected.connect(_on_tree_multi_selected)
add_child(content_tree) 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. # U3 (Karte t_a4bf9b8c): Rechtsklick-Kontextmenü + Drag&Drop für Gruppen.
content_tree.gui_input.connect(_on_tree_gui_input) content_tree.gui_input.connect(_on_tree_gui_input)
# Tree.set_drag_forwarding: die drei Virtuals (_get_drag_data/_can_drop_data/ # Tree.set_drag_forwarding: die drei Virtuals (_get_drag_data/_can_drop_data/
@ -61,6 +90,20 @@ func build() -> void:
add_child(_separator()) 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. ## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt.
## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl, ## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl,
## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln ## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln

View file

@ -1,139 +1,21 @@
extends "res://src/ui/panels/editor_panel.gd" extends Node
## Auswahl-Panel (U1, Karte t_eaccbfed — aus main.gd ausgezogen, Logik ## Auswahl-Logik (U1, Karte t_eaccbfed — aus main.gd ausgezogen). Seit 25.10.
## UNVERÄNDERT von dort übernommen): Buttons Drehen/Löschen/Duplizieren/ ## KEIN Tab mehr (Test-Befund): Drehen/Anheben/Duplizieren/Löschen laufen in der
## Spiegeln/Anheben/Absenken + Gruppen-Buttons + Gizmo-Schrittweite. ## 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 ## Verdrahtung: main.gd ruft sync_selection() an allen Auswahl-Wegen
## (vorher: _sync_selection_panel) und selection_status(idx) bei frischer ## (vorher: _sync_selection_panel) und selection_status(idx) bei frischer
## Auswahl (vorher: _selection_status). Alle Objekt-Operationen laufen über ## Auswahl (vorher: _selection_status). Alle Objekt-Operationen laufen über
## main.interaction — der Vertrag zu interaction.gd bleibt identisch. ## main.interaction — der Vertrag zu interaction.gd bleibt identisch.
var sel_label: Label var main # src/ui/main.gd (script-erweitert → untypisiert)
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
static func spec() -> Dictionary: func setup(p_main) -> void:
return {"titel": "Auswahl", "gruppe": "rechts", "order": 20} main = p_main
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
## Objekt-Inspektor + Auswahl-Panel synchron halten (vorher ## Objekt-Inspektor + Auswahl-Panel synchron halten (vorher
@ -145,54 +27,9 @@ func sync_selection() -> void:
# Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad # Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad
# WÄHREND eines Zugs kann sie im Gizmo ändern (cycle_step) — das Panel- # 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). # Feld folgt ohne eigene value_changed-Rückkopplung (set_value_no_signal).
if gizmo_step_box != null and is_instance_valid(gizmo_step_box) \ var step_box: SpinBox = main.gizmo_step_box
and not gizmo_step_box.has_focus(): if step_box != null and is_instance_valid(step_box) and not step_box.has_focus():
gizmo_step_box.set_value_no_signal(main.gizmo_step_m) 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]
## Hilfs-Meldung zur frischen Auswahl (Klartext + Budget-Warnung + nächster ## 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. # 3D-Klick, Rahmen-Ausziehen und Map-Inhalt-Baum.
if main.inspector != null: if main.inspector != null:
main.inspector.call("refresh") 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 ändert bewusst KEINEN Statuszeilen-Text (Stummsync).
sync_selection() sync_selection()
var doc: Dictionary = main.doc var doc: Dictionary = main.doc

View file

@ -1,5 +1,8 @@
extends "res://src/ui/panels/editor_panel.gd" 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 ## 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 ## 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). ## Eintrag wie Taste V (PlaytestSettings, persistiert).
var perspektive_box: OptionButton 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: static func spec() -> Dictionary:
return {"titel": "Einstellungen", "gruppe": "rechts", "order": 70} return {"titel": "Einstellungen", "gruppe": "dialog", "order": 10}
func panel_title() -> String: func panel_title() -> String:
@ -22,6 +35,72 @@ func panel_title() -> String:
func build() -> void: 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(_header_label("Testlauf-Einstellungen"))
add_child(_field_label("Maus-Empfindlichkeit (0,1 – 5,0)")) 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") var body: Variant = pt.get("body")
if body != null: if body != null:
body.call("set_perspektive", dritte) 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: static func spec() -> Dictionary:
return {"titel": "Symmetrie", "gruppe": "rechts", "order": 75} return {"titel": "Symmetrie", "gruppe": "map", "order": 30}
func panel_title() -> String: func panel_title() -> String:
@ -108,6 +108,14 @@ func _refresh_half_option() -> void:
half_option.selected = 0 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: func _on_half_selected(_index: int) -> void:
_refresh_mode_line() _refresh_mode_line()
@ -121,7 +129,7 @@ func _refresh_mode_line() -> void:
func _on_apply_pressed() -> void: func _on_apply_pressed() -> void:
var mode := _current_mode() var mode := _current_mode()
if mode == SymmetryCore.Mode.OFF: 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 return
if main.doc.is_empty(): if main.doc.is_empty():
_say("Keine Karte offen — erst eine Map anlegen oder laden.", true) _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: func _on_check_pressed() -> void:
var mode := _current_mode() var mode := _current_mode()
if mode == SymmetryCore.Mode.OFF: 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 return
_refresh_mode_line() _refresh_mode_line()
var rep := SymmetryOps.check(main.doc, mode) var rep := SymmetryOps.check(main.doc, mode)

View file

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

View file

@ -28,10 +28,7 @@ func panel_title() -> String:
var undo_btn: Button var undo_btn: Button
var redo_btn: Button var redo_btn: Button
var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014) var settings_btn: Button # „⚙ Einstellungen“ öffnet das Einstellungen-Fenster
var contours_btn: Button # „≈ Höhenlinien“ (Karte t_3ebe4086)
var sym_menu: MenuButton
var size_menu: MenuButton # „UI-Größe“ (Karte t_297221b5)
var title_label: Label var title_label: Label
var file_label: Label var file_label: Label
@ -85,45 +82,6 @@ func build() -> void:
redo_btn.pressed.connect(main.call.bind("_do_redo")) redo_btn.pressed.connect(main.call.bind("_do_redo"))
add_child(redo_btn) 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 # Toolbar-Aktionen DATENGETRIEBEN aus der Registry (Operator-Vorgabe
# 20.10.): Karte prüfen, ▶ Karte testen — künftige Karten melden sich # 20.10.): Karte prüfen, ▶ Karte testen — künftige Karten melden sich
# hier an, ohne main.gd anzufassen. Die Buttons bekommen den Registry-Namen # 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) main.set("test_start_btn", btn)
add_child(btn) add_child(btn)
# UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um, # Einstellungen (25.10.): Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe,
# es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt # Raster-Snap … stehen jetzt in einem eigenen Fenster.
# in user://scopedvr_editor.cfg gespeichert. settings_btn = Button.new()
size_menu = MenuButton.new() settings_btn.text = "⚙ Einstellungen"
size_menu.text = "UI-Größe" settings_btn.name = "einstellungen_knopf"
size_menu.flat = false settings_btn.custom_minimum_size = UIScale.v2(170, 40)
size_menu.custom_minimum_size = UIScale.v2(120, 40) settings_btn.tooltip_text = "Gitter, Höhenlinien, Symmetrie, UI-Größe, Raster-Snap und Testlauf-Einstellungen"
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." settings_btn.pressed.connect(func() -> void: main.call("_open_settings"))
var size_popup := size_menu.get_popup() add_child(settings_btn)
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)
var help_btn := Button.new() var help_btn := Button.new()
help_btn.text = "❓ Hilfe" help_btn.text = "❓ Hilfe"

View file

@ -55,7 +55,7 @@ func _run_tests() -> void:
func _test_catalog() -> void: func _test_catalog() -> void:
print("Katalog") print("Katalog")
_check(BauKatalog.KINDS.size() == 12, "12 Bauteile im Katalog") _check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog")
var in_gruppen := {} var in_gruppen := {}
for g in BauKatalog.GRUPPEN: for g in BauKatalog.GRUPPEN:
for oid in (g["oids"] as Array): 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)) 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)") _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)) 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)) 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)) 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)) 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°") _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)) 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") _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)) 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") _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): for k in range(60):
var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k) 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 w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
var wp: Vector3 = w["pos"] var wp: Vector3 = w["pos"]
var gerade_x := int(roundf(wp.x)) % 2 == 0 var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.1) < 0.001 # X nahe einer geraden Linie
var gerade_z := int(roundf(wp.z)) % 2 == 0 var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001
if not (gerade_x != gerade_z): var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
_check(false, "Wand-Mitte liegt auf Kantenmitte (%s)" % str(wp)) 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 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 # 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)) 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)") _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") print("Snap: Ebenen statt Klickhöhe")
var teile: Array = [ var teile: Array = [
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0), _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),
_teil(2, "bau_wand", Vector3(2, 0.2, 1), 90.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 # 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)) 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") _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 # 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)) 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.2) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,2") _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.2, -1.0), "…und in der Zelle auf der Kameraseite (1, −1)") _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")
# Wand auf Wand (Stapeln) 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))
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, 0.0), 0.0, teile, _flach(0.0)) _check(s["pos"].z == 1.0, "Treffer auf Nicht-Wand: Zelle der Kameraseite (Pfeiler an der Ecke)")
_check(absf(float(s["ebene"]) - 3.2) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante") # 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 # 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)) teile.append(_teil(4, "bau_dach_giebel", Vector3(1, 3.0, 1), 0.0))
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.2, 1.0), 0.0, teile, _flach(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.2 + 0.001, "Dachspitze (4,2) ist keine Ebene (Ebene %.2f)" % float(s["ebene"])) _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: func _test_replace_and_stack() -> void:
print("Ersetzen / doppelt") print("Ersetzen / doppelt")
var teile: Array = [ var teile: Array = [
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0), _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)) 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") _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)) 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") _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)) 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") _check(BauOps.teile_zahl(doc) == 1, "teile_zahl = 1")
# Wand setzen, dann Tür ersetzt dieselbe Stelle (Index bleibt) # Wand setzen, dann Tür ersetzt dieselbe Stelle (Index bleibt)
var teile := BuildSnap.sammle_teile(doc) 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 before2 := hist.snapshot(["objects", "group_names"])
var wr := BauOps.setze_teil(doc, "bau_wand", ws, gid, SymmetryCore.Mode.OFF) var wr := BauOps.setze_teil(doc, "bau_wand", ws, gid, SymmetryCore.Mode.OFF)
hist.commit("Wand bauen", before2, hist.snapshot(["objects", "group_names"])) hist.commit("Wand bauen", before2, hist.snapshot(["objects", "group_names"]))
var widx := int(wr["index"]) var widx := int(wr["index"])
teile = BuildSnap.sammle_teile(doc) 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") _check(int(ts["ersetze"]) == widx, "Tür-Klick findet die Wand")
var before3 := hist.snapshot(["objects", "group_names"]) var before3 := hist.snapshot(["objects", "group_names"])
var tr := BauOps.setze_teil(doc, "bau_wand_tuer", ts, gid, SymmetryCore.Mode.OFF) 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 # Haus komplett
var d2 := _neue_doc() var d2 := _neue_doc()
var gid2 := BauOps.haus_demo(d2, null, Vector3(4.0, 0.0, 4.0)) 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) _check(BauOps.gebaeude_von(d2, 0).size() == BauOps.teile_zahl(d2), "alle Teile des Hauses in einer Gruppe (%d)" % gid2)
var doppelt := {} var doppelt := {}
var hat_doppelte := false 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

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

View file

@ -51,6 +51,7 @@ func _init() -> void:
await _t1_baum_fuellt_sich() await _t1_baum_fuellt_sich()
await _t2_klick_setzt_auswahl() await _t2_klick_setzt_auswahl()
await _t2b_echter_klick()
await _t3_auswahl_3d_markiert_baum() await _t3_auswahl_3d_markiert_baum()
await _t4_mehrfach_gespiegelt() await _t4_mehrfach_gespiegelt()
await _t5_gruppen_ebene() 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)) _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: func _t3_auswahl_3d_markiert_baum() -> void:
var tree: Tree = tree_mod.get("content_tree") var tree: Tree = tree_mod.get("content_tree")
var imap: Dictionary = tree_mod.get("_obj_index_map") 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) 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: func _init() -> void:
print("Testgruppe: Layout aus der Registry") print("Testgruppe: Layout aus der Registry")
var scene: PackedScene = load("res://scenes/main.tscn") 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(ids.size() == registry_script.PANELS.size(), "Registry: IDs eindeutig (keine Duplikate in der Tabelle)")
_check(not ids.is_empty(), "Registry: mindestens ein Bereich angemeldet") _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 -------------- # --- 2. Jeder Bereich GENAU EINMAL im Baum, als Tab mit Titel --------------
var tabs_by_name: Dictionary = {} # gruppe → TabContainer var tabs_by_name: Dictionary = {} # gruppe → TabContainer
var stack: Array = [main] var stack: Array = [main]
@ -91,6 +103,12 @@ func _init() -> void:
_check(tab.get_tab_title(idx) == title, _check(tab.get_tab_title(idx) == title,
"Tab '%s' trägt Registry-Titel '%s'" % [cid, title]) "Tab '%s' trägt Registry-Titel '%s'" % [cid, title])
for wanted in ids: 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) _check(tabs_by_name.get(wanted) != null, "Bereich '%s' aus der Registry im Baum" % wanted)
# --- 3. Tab-Gruppen -------------------------------------------------------- # --- 3. Tab-Gruppen --------------------------------------------------------
@ -108,6 +126,7 @@ func _init() -> void:
for entry in registry_script.PANELS: for entry in registry_script.PANELS:
if str(entry["gruppe"]) == "rechts": if str(entry["gruppe"]) == "rechts":
expected_right += 1 expected_right += 1
_check(expected_right == 5, "rechts: genau 5 Tabs (Inspektor, Map, Terrain, Malen, Bauen)")
_check(right.get_child_count() == expected_right, _check(right.get_child_count() == expected_right,
"rechts: %d Tabs aus der Registry — hat %d" % [expected_right, right.get_child_count()]) "rechts: %d Tabs aus der Registry — hat %d" % [expected_right, right.get_child_count()])
_check(right.get_tab_count() == right.get_child_count(), _check(right.get_tab_count() == right.get_child_count(),
@ -125,7 +144,7 @@ func _init() -> void:
btab = gc btab = gc
_check(btab != null, "unten: TabContainer für Assets gefunden") _check(btab != null, "unten: TabContainer für Assets gefunden")
if btab != null: 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 ----------------------------------- # --- 4. Assets-Bereich unten vollständig -----------------------------------
var assets: Control = tabs_by_name.get("assets") var assets: Control = tabs_by_name.get("assets")
@ -136,8 +155,12 @@ func _init() -> void:
"Bibliothek: „Alle auf/zu“ vorhanden") "Bibliothek: „Alle auf/zu“ vorhanden")
var lib_box: Node = assets.get("library_box") 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") _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(tabs_by_name.get("werkzeuge") == null, "Tab „Objekt-Werkzeuge“ ist weg")
_check(tools != null and tools.get("grid_check") != null, "Objekt-Werkzeuge: Raster-Snap erhalten (nichts verschwindet)") 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 ---------------------------------------- # --- 5. Theme als einzige Farbquelle ----------------------------------------
var theme_res: Theme = main.get("theme") 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(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.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.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") _check(main.size_menu != null and main.size_menu is OptionButton, "UI-Größe-Auswahl im Einstellungen-Fenster")
var sp: PopupMenu = main.size_menu.get_popup() _check(main.size_menu.item_count == 3, "Auswahl hat 3 Stufen")
_check(sp.item_count == 3, "Menü hat 3 Einträge") _check(main.size_menu.selected == 0, "Auswahl steht auf aktueller Stufe (Klein)")
_check(sp.is_item_checked(0), "Menü-Häkchen auf aktueller Stufe (Klein)")
# Rebuild-Pfad (derselbe wie beim Menü-Klick) → Mittel: # Rebuild-Pfad (derselbe wie beim Menü-Klick) → Mittel:
UIScale.set_step_index(1) 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") _check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
var thumb := _find_thumb_rect(160) var thumb := _find_thumb_rect(160)
_check(thumb != null and thumb.custom_minimum_size.x == 160, "Mittel: Thumbnail 128 → 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ß: # Groß:
main._on_ui_size_selected(2) main._on_ui_size_selected(2)
@ -161,7 +160,7 @@ func _test_real_ui_scaling() -> void:
main._on_ui_size_selected(0) main._on_ui_size_selected(0)
await process_frame await process_frame
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(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") _check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Zurück auf Klein: Undo-Knopf 140×40")