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:
parent
ab9a798343
commit
3f0d0f3f33
32 changed files with 865 additions and 440 deletions
|
|
@ -25,20 +25,28 @@ Ein Bauteil ist ein **ganz normales Bibliotheks-Objekt** (`bau_*`, Kategorie
|
|||
| --- | --- |
|
||||
| Fundament & Boden | Fundament, Boden/Decke (2 × 2 m, 20 cm) |
|
||||
| Wände (2 m breit, 3 m hoch) | Wand, Wand mit Tür (1,2 × 2,2 m), Wand mit Fenster, Wand mit Schießscharte, Brüstung (1,1 m), Eckpfeiler |
|
||||
| Dach & Giebel | Giebelwand, Satteldach, Pyramidendach |
|
||||
| Dach & Giebel (klein 2 m) | Giebelwand, Satteldach, Pyramidendach |
|
||||
| Dach & Giebel (mittel 4 m) | Giebelwand 4 m, Satteldach 4×4, Pyramidendach 4×4 (Spitze 2 m) |
|
||||
| Dach & Giebel (groß 6 m) | Giebelwand 6 m, Satteldach 6×6, Pyramidendach 6×6 (Spitze 3 m) |
|
||||
| Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) |
|
||||
|
||||
Dazu im Tab „Pack-Module“: Container, Hangar-Tore, Betonsperren aus dem
|
||||
Military-Desert-Pack (normale Objekte).
|
||||
Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab,
|
||||
sondern wie alle Objekte in der Objekt-Bibliothek unten.
|
||||
|
||||
## Snap (`src/editor/build_snap.gd`)
|
||||
|
||||
* **XZ:** Wände auf die nächste Zellkante (Drehung ergibt sich aus der Kante, `R`
|
||||
dreht die Wand um), Böden/Dächer/Fundamente in die Zellmitte, Pfeiler auf die
|
||||
Zellecke, Treppen auf eine 4 × 2-m-Fläche. Zellen sind 2 × 2 m, global ab (0, 0).
|
||||
* **XZ:** Wände auf die nächste Zellkante — und zwar GANZ in die Zelle, in der die
|
||||
Maus ist (Außenseite bündig mit der Kante, kein Überhang, Ecken schließen ohne
|
||||
Lücke). Klick auf eine vorhandene Wand (Seite oder Oberkante) übernimmt deren Lage
|
||||
exakt (ersetzen bzw. stapeln). Böden/Dächer/Fundamente in die Zellmitte, große
|
||||
Dächer (4 m / 6 m) je nach Zellzahl auf Zellecke bzw. Zellmitte, Pfeiler auf die
|
||||
Zellecke, Treppen auf eine 4 × 2-m-Fläche. `R` dreht die Wand um. Zellen sind
|
||||
2 × 2 m, global ab (0, 0).
|
||||
* **Y:** nie die Klickhöhe, sondern eine **Ebene** — Gelände (höchster Punkt unter
|
||||
der Grundfläche) oder Oberkante eines tragenden Teils darunter. Klick auf die
|
||||
Wandoberkante stapelt das nächste Geschoss.
|
||||
der Grundfläche) oder Oberkante eines tragenden Teils darunter. **Wände und
|
||||
Pfeiler zählen von der Unterkante einer Decke/eines Fundaments**, die Decke selbst
|
||||
liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder
|
||||
ohne Decke darunter stehen die oberen Wände gleich hoch.
|
||||
* **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene
|
||||
Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt.
|
||||
|
||||
|
|
|
|||
1
library/kollisions/bau_dach_giebel_4.json
Normal file
1
library/kollisions/bau_dach_giebel_4.json
Normal 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]]}
|
||||
1
library/kollisions/bau_dach_giebel_6.json
Normal file
1
library/kollisions/bau_dach_giebel_6.json
Normal 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]]}
|
||||
1
library/kollisions/bau_dach_pyramide_4.json
Normal file
1
library/kollisions/bau_dach_pyramide_4.json
Normal 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]]}
|
||||
1
library/kollisions/bau_dach_pyramide_6.json
Normal file
1
library/kollisions/bau_dach_pyramide_6.json
Normal 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]]}
|
||||
1
library/kollisions/bau_giebelwand_4.json
Normal file
1
library/kollisions/bau_giebelwand_4.json
Normal 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]]}
|
||||
1
library/kollisions/bau_giebelwand_6.json
Normal file
1
library/kollisions/bau_giebelwand_6.json
Normal 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]]}
|
||||
|
|
@ -3506,6 +3506,36 @@
|
|||
"kategorie": "bauteile",
|
||||
"name_de": "Satteldach"
|
||||
},
|
||||
"bau_dach_giebel_4": {
|
||||
"poly": 48,
|
||||
"collision": {
|
||||
"quelle": "kollisions/bau_dach_giebel_4.json",
|
||||
"shape": "trimesh",
|
||||
"size": [
|
||||
4.141,
|
||||
2.141,
|
||||
4.0
|
||||
]
|
||||
},
|
||||
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel_4.glb",
|
||||
"kategorie": "bauteile",
|
||||
"name_de": "Satteldach 4×4 m"
|
||||
},
|
||||
"bau_dach_giebel_6": {
|
||||
"poly": 48,
|
||||
"collision": {
|
||||
"quelle": "kollisions/bau_dach_giebel_6.json",
|
||||
"shape": "trimesh",
|
||||
"size": [
|
||||
6.141,
|
||||
3.141,
|
||||
6.0
|
||||
]
|
||||
},
|
||||
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel_6.glb",
|
||||
"kategorie": "bauteile",
|
||||
"name_de": "Satteldach 6×6 m"
|
||||
},
|
||||
"bau_dach_pyramide": {
|
||||
"poly": 64,
|
||||
"collision": {
|
||||
|
|
@ -3521,6 +3551,36 @@
|
|||
"kategorie": "bauteile",
|
||||
"name_de": "Pyramidendach"
|
||||
},
|
||||
"bau_dach_pyramide_4": {
|
||||
"poly": 64,
|
||||
"collision": {
|
||||
"quelle": "kollisions/bau_dach_pyramide_4.json",
|
||||
"shape": "trimesh",
|
||||
"size": [
|
||||
4.141,
|
||||
2.141,
|
||||
4.141
|
||||
]
|
||||
},
|
||||
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_pyramide_4.glb",
|
||||
"kategorie": "bauteile",
|
||||
"name_de": "Pyramidendach 4×4 m"
|
||||
},
|
||||
"bau_dach_pyramide_6": {
|
||||
"poly": 64,
|
||||
"collision": {
|
||||
"quelle": "kollisions/bau_dach_pyramide_6.json",
|
||||
"shape": "trimesh",
|
||||
"size": [
|
||||
6.141,
|
||||
3.141,
|
||||
6.141
|
||||
]
|
||||
},
|
||||
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_pyramide_6.glb",
|
||||
"kategorie": "bauteile",
|
||||
"name_de": "Pyramidendach 6×6 m"
|
||||
},
|
||||
"bau_fundament": {
|
||||
"poly": 24,
|
||||
"collision": {
|
||||
|
|
@ -3551,6 +3611,36 @@
|
|||
"kategorie": "bauteile",
|
||||
"name_de": "Giebelwand"
|
||||
},
|
||||
"bau_giebelwand_4": {
|
||||
"poly": 16,
|
||||
"collision": {
|
||||
"quelle": "kollisions/bau_giebelwand_4.json",
|
||||
"shape": "trimesh",
|
||||
"size": [
|
||||
4.0,
|
||||
2.0,
|
||||
0.2
|
||||
]
|
||||
},
|
||||
"glb": "res://assets/objekte/military_desert/bauteile/bau_giebelwand_4.glb",
|
||||
"kategorie": "bauteile",
|
||||
"name_de": "Giebelwand 4 m"
|
||||
},
|
||||
"bau_giebelwand_6": {
|
||||
"poly": 16,
|
||||
"collision": {
|
||||
"quelle": "kollisions/bau_giebelwand_6.json",
|
||||
"shape": "trimesh",
|
||||
"size": [
|
||||
6.0,
|
||||
3.0,
|
||||
0.2
|
||||
]
|
||||
},
|
||||
"glb": "res://assets/objekte/military_desert/bauteile/bau_giebelwand_6.glb",
|
||||
"kategorie": "bauteile",
|
||||
"name_de": "Giebelwand 6 m"
|
||||
},
|
||||
"bau_pfeiler": {
|
||||
"poly": 12,
|
||||
"collision": {
|
||||
|
|
|
|||
|
|
@ -49,11 +49,15 @@ const GRUPPEN := [
|
|||
{"key": "grund", "label": "Fundament & Boden", "oids": ["bau_fundament", "bau_boden"]},
|
||||
{"key": "waende", "label": "Wände", "oids": ["bau_wand", "bau_wand_tuer", "bau_wand_fenster",
|
||||
"bau_wand_scharte", "bau_bruestung", "bau_pfeiler"]},
|
||||
{"key": "dach", "label": "Dach & Giebel", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]},
|
||||
{"key": "dach", "label": "Dach & Giebel (klein 2 m)", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]},
|
||||
{"key": "dach_mittel", "label": "Dach & Giebel (mittel 4 m)", "oids": ["bau_giebelwand_4", "bau_dach_giebel_4", "bau_dach_pyramide_4"]},
|
||||
{"key": "dach_gross", "label": "Dach & Giebel (groß 6 m)", "oids": ["bau_giebelwand_6", "bau_dach_giebel_6", "bau_dach_pyramide_6"]},
|
||||
{"key": "treppe", "label": "Treppe", "oids": ["bau_treppe"]},
|
||||
]
|
||||
|
||||
const QUADRAT := [[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]]
|
||||
const QUADRAT_4 := [[-2.0, 0.0, -2.0], [2.0, 0.0, -2.0], [2.0, 0.0, 2.0], [-2.0, 0.0, 2.0]]
|
||||
const QUADRAT_6 := [[-3.0, 0.0, -3.0], [3.0, 0.0, -3.0], [3.0, 0.0, 3.0], [-3.0, 0.0, 3.0]]
|
||||
|
||||
## Reihenfolge = Anzeige-Reihenfolge (GRUPPEN nutzt dieselben IDs).
|
||||
## "traeger": darauf darf gestapelt werden (Oberkante zählt als Ebene).
|
||||
|
|
@ -107,6 +111,34 @@ const KINDS := {
|
|||
"label": "Pyramidendach", "info": "2×2 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
|
||||
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT, "rise": 1.0, "farbe": Color(0.40, 0.25, 0.18),
|
||||
},
|
||||
# Große Dächer (Test-Befund 25.10.: 2×2 m reichte nur für eine Zelle). Immer 45°
|
||||
# (Spitze = halbe Breite); Giebelwand und Dach gleicher Größe passen zusammen.
|
||||
"bau_giebelwand_4": {
|
||||
"label": "Giebelwand 4 m", "info": "Dreieck-Abschluss, 4 m breit, 2 m Spitze — zum Satteldach 4 m",
|
||||
"snap": "kante", "traeger": false,
|
||||
"typ": T_GIEBEL, "punkte": [[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]], "rise": 2.0, "farbe": Color(0.62, 0.53, 0.38),
|
||||
},
|
||||
"bau_dach_giebel_4": {
|
||||
"label": "Satteldach 4×4 m", "info": "4×4 m, First in Richtung der Vorschau (R dreht)", "snap": "zelle", "traeger": false,
|
||||
"typ": T_DACH_GIEBEL, "punkte": QUADRAT_4, "rise": 2.0, "farbe": Color(0.40, 0.25, 0.18),
|
||||
},
|
||||
"bau_dach_pyramide_4": {
|
||||
"label": "Pyramidendach 4×4 m", "info": "4×4 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
|
||||
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_4, "rise": 2.0, "farbe": Color(0.40, 0.25, 0.18),
|
||||
},
|
||||
"bau_giebelwand_6": {
|
||||
"label": "Giebelwand 6 m", "info": "Dreieck-Abschluss, 6 m breit, 3 m Spitze — zum Satteldach 6 m",
|
||||
"snap": "kante", "traeger": false,
|
||||
"typ": T_GIEBEL, "punkte": [[0.0, 0.0, 0.0], [6.0, 0.0, 0.0]], "rise": 3.0, "farbe": Color(0.62, 0.53, 0.38),
|
||||
},
|
||||
"bau_dach_giebel_6": {
|
||||
"label": "Satteldach 6×6 m", "info": "6×6 m, First in Richtung der Vorschau (R dreht)", "snap": "zelle", "traeger": false,
|
||||
"typ": T_DACH_GIEBEL, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18),
|
||||
},
|
||||
"bau_dach_pyramide_6": {
|
||||
"label": "Pyramidendach 6×6 m", "info": "6×6 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
|
||||
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18),
|
||||
},
|
||||
"bau_treppe": {
|
||||
"label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber",
|
||||
"snap": "treppe", "traeger": false,
|
||||
|
|
@ -134,6 +166,11 @@ static func snap_art(oid: String) -> String:
|
|||
return str((KINDS.get(oid, {}) as Dictionary).get("snap", "zelle"))
|
||||
|
||||
|
||||
## Platte = flaches Zell-Teil, auf dem man steht (Fundament, Boden/Decke).
|
||||
static func ist_platte(oid: String) -> bool:
|
||||
return snap_art(oid) == "zelle" and ist_traeger(oid)
|
||||
|
||||
|
||||
static func ist_traeger(oid: String) -> bool:
|
||||
return bool((KINDS.get(oid, {}) as Dictionary).get("traeger", false))
|
||||
|
||||
|
|
|
|||
|
|
@ -122,22 +122,22 @@ static func haus_demo(doc: Dictionary, history: Variant, mitte: Vector3) -> int:
|
|||
["bau_fundament", Vector3(ox + 1, mitte.y, oz - 1), 0.0],
|
||||
["bau_fundament", Vector3(ox - 1, mitte.y, oz + 1), 0.0],
|
||||
["bau_fundament", Vector3(ox + 1, mitte.y, oz + 1), 0.0],
|
||||
["bau_wand", Vector3(ox - 1, mitte.y, oz - 2), 0.0],
|
||||
["bau_wand_fenster", Vector3(ox + 1, mitte.y, oz - 2), 0.0],
|
||||
["bau_wand_tuer", Vector3(ox - 1, mitte.y, oz + 2), 0.0],
|
||||
["bau_wand", Vector3(ox + 1, mitte.y, oz + 2), 0.0],
|
||||
["bau_wand", Vector3(ox - 2, mitte.y, oz - 1), 90.0],
|
||||
["bau_wand_scharte", Vector3(ox - 2, mitte.y, oz + 1), 90.0],
|
||||
["bau_wand", Vector3(ox + 2, mitte.y, oz - 1), 90.0],
|
||||
["bau_wand", Vector3(ox + 2, mitte.y, oz + 1), 90.0],
|
||||
# Wände liegen GANZ in der Zelle, in der die Maus ist (Außenseite bündig mit
|
||||
# der Kante) — deshalb zielen die Klicks 10 cm ins Hausinnere.
|
||||
["bau_wand", Vector3(ox - 1, mitte.y, oz - 1.9), 0.0],
|
||||
["bau_wand_fenster", Vector3(ox + 1, mitte.y, oz - 1.9), 0.0],
|
||||
["bau_wand_tuer", Vector3(ox - 1, mitte.y, oz + 1.9), 0.0],
|
||||
["bau_wand", Vector3(ox + 1, mitte.y, oz + 1.9), 0.0],
|
||||
["bau_wand", Vector3(ox - 1.9, mitte.y, oz - 1), 90.0],
|
||||
["bau_wand_scharte", Vector3(ox - 1.9, mitte.y, oz + 1), 90.0],
|
||||
["bau_wand", Vector3(ox + 1.9, mitte.y, oz - 1), 90.0],
|
||||
["bau_wand", Vector3(ox + 1.9, mitte.y, oz + 1), 90.0],
|
||||
]
|
||||
for st in schritte:
|
||||
_demo_setzen(doc, str(st[0]), st[1] as Vector3, float(st[2]), gid, terrain)
|
||||
# Decke + Dach auf die Wandoberkante (Klick auf die Oberkante = Ebene Wand oben)
|
||||
var oben := mitte.y + 0.2 + BauKatalog.WAND_H
|
||||
for cx in [-1.0, 1.0]:
|
||||
for cz in [-1.0, 1.0]:
|
||||
_demo_setzen(doc, "bau_dach_giebel", Vector3(ox + cx, oben, oz + cz), 0.0, gid, terrain)
|
||||
# Ein großes Satteldach (4×4 m) auf die Wandoberkante.
|
||||
var oben := mitte.y + BauKatalog.WAND_H
|
||||
_demo_setzen(doc, "bau_dach_giebel_4", Vector3(ox, oben, oz), 0.0, gid, terrain)
|
||||
if history != null:
|
||||
history.commit("Haus gebaut (Demo)", before, history.snapshot(["objects", "group_names"]))
|
||||
return gid
|
||||
|
|
|
|||
|
|
@ -43,9 +43,20 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
|||
var q := int(roundf(dreh / 90.0))
|
||||
var viertel := ((q % 4) + 4) % 4
|
||||
var p := Vector2(hit.x, hit.z)
|
||||
# Treffer auf einem Objekt: Zelle auf der Kameraseite wählen (Klick auf die
|
||||
# Wandoberkante liegt genau auf der Zellgrenze).
|
||||
if hit_ist_objekt and art != "kante" and kamera_dir != Vector3.ZERO:
|
||||
# Treffer auf einem Objekt: Wände liegen GANZ in einer Zelle — die Quer-Achse
|
||||
# kommt dann von der Wand (Decke/Dach landet in der Zelle der Wand). Bei anderen
|
||||
# Objekten die Zelle auf der Kameraseite wählen (Klick liegt auf einer Grenze).
|
||||
var wand_getroffen := {}
|
||||
if hit_ist_objekt:
|
||||
wand_getroffen = _wand_unter_klick(teile, p, hit.y)
|
||||
if hit_ist_objekt and art != "kante":
|
||||
if not wand_getroffen.is_empty():
|
||||
var wp: Vector3 = wand_getroffen["pos"]
|
||||
if absf(sin(deg_to_rad(float(wand_getroffen["rot"])))) > 0.5:
|
||||
p.x = wp.x
|
||||
else:
|
||||
p.y = wp.z
|
||||
elif kamera_dir != Vector3.ZERO:
|
||||
var d2 := Vector2(kamera_dir.x, kamera_dir.z)
|
||||
if d2.length() > 0.01:
|
||||
p -= d2.normalized() * 0.2
|
||||
|
|
@ -54,7 +65,9 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
|||
var r := BauKatalog.RASTER_M
|
||||
match art:
|
||||
"zelle":
|
||||
mitte = Vector2(floorf(p.x / r) * r + r * 0.5, floorf(p.y / r) * r + r * 0.5)
|
||||
# Teile über mehrere Zellen (große Dächer): gerade Zellzahl → Mitte auf
|
||||
# einer Zellecke, ungerade → Mitte einer Zelle.
|
||||
mitte = Vector2(_raster_mitte(p.x, gr.x), _raster_mitte(p.y, gr.z))
|
||||
rot = viertel * 90.0
|
||||
"ecke":
|
||||
mitte = Vector2(roundf(p.x / r) * r, roundf(p.y / r) * r)
|
||||
|
|
@ -65,15 +78,30 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
|||
else:
|
||||
mitte = Vector2(floorf(p.x / r) * r + r * 0.5, roundf(p.y / r) * r)
|
||||
_: # "kante"
|
||||
var vorhanden := wand_getroffen
|
||||
if not vorhanden.is_empty():
|
||||
# Klick auf eine vorhandene Wand (Seite = ersetzen, Oberkante = stapeln):
|
||||
# GENAU ihre Lage übernehmen, nicht neu aus der Klickstelle raten.
|
||||
var tp: Vector3 = vorhanden["pos"]
|
||||
mitte = Vector2(tp.x, tp.z)
|
||||
rot = 90.0 if absf(sin(deg_to_rad(float(vorhanden["rot"])))) > 0.5 else 0.0
|
||||
else:
|
||||
# Wand liegt GANZ IN der Zelle, in der der Mauszeiger ist: Außenseite
|
||||
# bündig mit der Zellkante (Boden/Fundament), nicht halb in der Luft.
|
||||
# Dadurch schließen sich auch Ecken ohne Lücke (Test-Befund 25.10.).
|
||||
var laenge := gr.x
|
||||
var halbe_dicke := gr.z * 0.5
|
||||
var fx := p.x / r
|
||||
var fz := p.y / r
|
||||
var h_mitte := Vector2((floorf(fx) + 0.5) * r, roundf(fz) * r) # Kante entlang X
|
||||
var v_mitte := Vector2(roundf(fx) * r, (floorf(fz) + 0.5) * r) # Kante entlang Z
|
||||
var h_mitte := Vector2(_raster_mitte(p.x, laenge), roundf(fz) * r) # Kante entlang X
|
||||
var v_mitte := Vector2(roundf(fx) * r, _raster_mitte(p.y, laenge)) # Kante entlang Z
|
||||
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte):
|
||||
mitte = h_mitte
|
||||
mitte.y += halbe_dicke if p.y >= h_mitte.y else -halbe_dicke
|
||||
rot = 0.0
|
||||
else:
|
||||
mitte = v_mitte
|
||||
mitte.x += halbe_dicke if p.x >= v_mitte.x else -halbe_dicke
|
||||
rot = 90.0
|
||||
rot += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile)
|
||||
rot = fposmod(rot, 360.0)
|
||||
|
|
@ -95,7 +123,15 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
|||
if not tr.intersects(kern):
|
||||
continue
|
||||
var top: float = (t["pos"] as Vector3).y + BauKatalog.groesse(troid).y
|
||||
if BauKatalog.ist_traeger(troid):
|
||||
if not BauKatalog.ist_traeger(troid):
|
||||
continue
|
||||
if (art == "kante" or art == "ecke") and BauKatalog.ist_platte(troid):
|
||||
# Wände/Pfeiler zählen von der UNTERKANTE einer Platte (Boden/Decke/
|
||||
# Fundament): jedes Geschoss liegt dadurch auf Vielfachen der Wandhöhe —
|
||||
# mit oder ohne Decke darunter sitzen obere Wände gleich hoch
|
||||
# (Test-Befund 25.10.). Die Platte selbst liegt auf der Wandoberkante.
|
||||
ebenen.append((t["pos"] as Vector3).y)
|
||||
else:
|
||||
ebenen.append(top)
|
||||
var ebene := _waehle_ebene(ebenen, hit.y)
|
||||
|
||||
|
|
@ -123,6 +159,39 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
|||
"ebene": ebene, "grund": ""}
|
||||
|
||||
|
||||
## Mitte eines Teils der Länge `laenge` entlang einer Achse: gerade Zellzahl →
|
||||
## auf der nächsten Zellgrenze, ungerade → Mitte der Zelle.
|
||||
static func _raster_mitte(wert: float, laenge: float) -> float:
|
||||
var r := BauKatalog.RASTER_M
|
||||
var zellen := maxi(1, int(roundf(laenge / r)))
|
||||
if zellen % 2 == 0:
|
||||
return roundf(wert / r) * r
|
||||
return floorf(wert / r) * r + r * 0.5
|
||||
|
||||
|
||||
## Vorhandene Wand, auf die der Klick trifft (Seite oder Oberkante): Rückgabe das
|
||||
## Teil-Wörterbuch oder {}. `p` = Klickpunkt in XZ, `y` = Klickhöhe.
|
||||
static func _wand_unter_klick(teile: Array, p: Vector2, y: float) -> Dictionary:
|
||||
var bester := {}
|
||||
var bester_d := INF
|
||||
for t in teile:
|
||||
var troid := str(t["oid"])
|
||||
if BauKatalog.snap_art(troid) != "kante":
|
||||
continue
|
||||
var tr := teil_rect(t).grow(0.06)
|
||||
if not tr.has_point(p):
|
||||
continue
|
||||
var unten: float = (t["pos"] as Vector3).y
|
||||
var oben := unten + BauKatalog.groesse(troid).y
|
||||
if y < unten - 0.1 or y > oben + 0.1:
|
||||
continue
|
||||
var d := p.distance_squared_to(tr.get_center())
|
||||
if d < bester_d:
|
||||
bester_d = d
|
||||
bester = t
|
||||
return bester
|
||||
|
||||
|
||||
## Höchste Ebene ≤ hit_y + TOL; gibt es keine (Klick tief unter allen), die tiefste.
|
||||
static func _waehle_ebene(ebenen: Array, hit_y: float) -> float:
|
||||
var best := -INF
|
||||
|
|
|
|||
127
src/ui/main.gd
127
src/ui/main.gd
|
|
@ -76,7 +76,10 @@ var inspector: Control # Objekt-Inspektor (Karte t_bb0d11c9) — im Insp
|
|||
# bleiben hier, damit Tests/Tools denselben Vertrag sehen wie vorher)
|
||||
var toolbar_mod # src/ui/panels/toolbar.gd
|
||||
var status_mod # src/ui/panels/status_bar.gd
|
||||
var selection_mod # src/ui/panels/selection_panel.gd
|
||||
var selection_mod # src/ui/panels/selection_panel.gd (nur Logik, kein Tab mehr)
|
||||
var settings_mod # src/ui/panels/settings_panel.gd (im Einstellungen-Fenster)
|
||||
var settings_dialog: AcceptDialog # Fenster „Einstellungen“ (Knopf in der Kopfleiste)
|
||||
var sym_mod # src/ui/panels/symmetry_panel.gd (im Map-Tab)
|
||||
var map_mod # src/ui/panels/map_panel.gd
|
||||
var terrain_mod # src/ui/panels/terrain_panel.gd
|
||||
var malen_mod # src/ui/panels/malen_panel.gd (Malen-Tab)
|
||||
|
|
@ -91,13 +94,12 @@ var tool_label: Label # weiter in status_mod
|
|||
var sym_label: Label # weiter in status_mod
|
||||
var undo_btn: Button # weiter in toolbar_mod
|
||||
var redo_btn: Button # weiter in toolbar_mod
|
||||
var grid_btn: Button # weiter in toolbar_mod
|
||||
var contours_btn: Button # weiter in toolbar_mod
|
||||
var size_menu: MenuButton # weiter in toolbar_mod
|
||||
var sym_menu: MenuButton # weiter in toolbar_mod
|
||||
var grid_btn: CheckButton # weiter in settings_mod (Einstellungen-Fenster)
|
||||
var contours_btn: CheckButton # weiter in settings_mod
|
||||
var size_menu: OptionButton # weiter in settings_mod
|
||||
var sym_menu: OptionButton # weiter in settings_mod
|
||||
var prop_name: LineEdit # weiter in map_mod
|
||||
var sel_label: Label # weiter in selection_mod
|
||||
var gizmo_step_box: SpinBox # weiter in selection_mod
|
||||
var gizmo_step_box: SpinBox # weiter in settings_mod
|
||||
var library_search: LineEdit # weiter in assets_mod
|
||||
var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit)
|
||||
var _library_group_rows: Array = [] # weiter in assets_mod (Bequemlichkeit)
|
||||
|
|
@ -273,6 +275,7 @@ func _build_ui() -> void:
|
|||
ui_right_panel = right_tabs
|
||||
right_tabs.tab_changed.connect(_on_right_tab_changed)
|
||||
split.add_child(right_tabs)
|
||||
_build_map_sections()
|
||||
|
||||
# --- Assets unten (Registry-Gruppe "unten", U1 Auftragspunkt 4) ---
|
||||
# Die Objekt-Bibliothek wohnt jetzt UNTER dem Hauptbereich (Unity-
|
||||
|
|
@ -337,13 +340,6 @@ func _make_module(script: GDScript, parent: Control) -> Control:
|
|||
func _pull_toolbar_refs() -> void:
|
||||
undo_btn = toolbar_mod.undo_btn
|
||||
redo_btn = toolbar_mod.redo_btn
|
||||
grid_btn = toolbar_mod.grid_btn
|
||||
contours_btn = toolbar_mod.contours_btn
|
||||
size_menu = toolbar_mod.size_menu
|
||||
sym_menu = toolbar_mod.sym_menu
|
||||
# Häkchen im UI-Größe-Menü auf die aktuelle Stufe (vor U1 rief die
|
||||
# Top-Bar _sync_size_menu während des Baus — jetzt nach dem Referenz-Zug).
|
||||
_sync_size_menu()
|
||||
|
||||
|
||||
func _pull_status_refs() -> void:
|
||||
|
|
@ -386,8 +382,6 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
|
|||
match child.name:
|
||||
"inspektor":
|
||||
inspector = child
|
||||
"auswahl":
|
||||
selection_mod = child
|
||||
"map":
|
||||
map_mod = child
|
||||
"terrain":
|
||||
|
|
@ -396,12 +390,7 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
|
|||
malen_mod = child
|
||||
"bauen":
|
||||
bauen_mod = child
|
||||
"check":
|
||||
check_mod = child
|
||||
sel_label = selection_mod.sel_label
|
||||
gizmo_step_box = selection_mod.gizmo_step_box
|
||||
prop_name = map_mod.prop_name
|
||||
spawn_status = check_mod.spawn_status
|
||||
terrain_info = map_mod.terrain_info
|
||||
# Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die
|
||||
# alten Namen als Aliase (Tests/Werkzeug-Vertrag).
|
||||
|
|
@ -438,7 +427,73 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
|
|||
library_close_all_btn = assets_mod.library_close_all_btn
|
||||
|
||||
|
||||
## Abschnitte im Map-Tab (Registry-Gruppe "map"): Spawn-Zonen, Test-Start-Info,
|
||||
## Symmetrie. Sie hängen UNTER den Map-Eigenschaften im selben Scroll-Bereich.
|
||||
func _build_map_sections() -> void:
|
||||
var host: Control = map_mod.sections_host()
|
||||
for entry in PanelRegistryScript.entries("map"):
|
||||
var script: GDScript = load(str(entry["skript"]))
|
||||
var panel: Control = script.new()
|
||||
panel.name = str(entry["id"])
|
||||
panel.setup(self)
|
||||
if panel.has_method("build"):
|
||||
panel.call("build")
|
||||
host.add_child(HSeparator.new())
|
||||
host.add_child(panel)
|
||||
match str(entry["id"]):
|
||||
"check":
|
||||
check_mod = panel
|
||||
spawn_status = check_mod.spawn_status
|
||||
"symmetrie":
|
||||
sym_mod = panel
|
||||
|
||||
|
||||
## Einstellungen-Fenster (Knopf ⚙ in der Kopfleiste): Inhalt kommt aus der
|
||||
## Registry-Gruppe "dialog". Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe,
|
||||
## Raster-Snap und Gizmo-Schritt wohnen dort (vorher Kopfleiste / Objekt-Werkzeuge).
|
||||
func _build_settings_dialog() -> void:
|
||||
settings_dialog = AcceptDialog.new()
|
||||
settings_dialog.name = "EinstellungenFenster"
|
||||
settings_dialog.title = "Einstellungen"
|
||||
settings_dialog.exclusive = false
|
||||
settings_dialog.ok_button_text = "Schließen"
|
||||
settings_dialog.min_size = UIScale.v2i(520, 420)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.custom_minimum_size = UIScale.v2(500, 380)
|
||||
settings_dialog.add_child(scroll)
|
||||
for entry in PanelRegistryScript.entries("dialog"):
|
||||
var script: GDScript = load(str(entry["skript"]))
|
||||
var panel: Control = script.new()
|
||||
panel.name = str(entry["id"])
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
panel.setup(self)
|
||||
if panel.has_method("build"):
|
||||
panel.call("build")
|
||||
scroll.add_child(panel)
|
||||
settings_mod = panel
|
||||
add_child(settings_dialog)
|
||||
grid_btn = settings_mod.grid_btn
|
||||
contours_btn = settings_mod.contours_btn
|
||||
size_menu = settings_mod.size_menu
|
||||
sym_menu = settings_mod.sym_menu
|
||||
gizmo_step_box = settings_mod.gizmo_step_box
|
||||
settings_mod.call("refresh")
|
||||
|
||||
|
||||
func _open_settings() -> void:
|
||||
if settings_dialog == null:
|
||||
return
|
||||
settings_mod.call("refresh")
|
||||
settings_dialog.popup_centered(UIScale.v2i(560, 520))
|
||||
|
||||
|
||||
func _build_dialogs_and_overlays() -> void:
|
||||
selection_mod = load("res://src/ui/panels/selection_panel.gd").new()
|
||||
selection_mod.name = "AuswahlLogik"
|
||||
selection_mod.setup(self)
|
||||
add_child(selection_mod)
|
||||
_build_settings_dialog()
|
||||
# --- Dialoge ---
|
||||
new_dialog = preload("res://src/ui/new_map_dialog.gd").new()
|
||||
new_dialog.confirmed.connect(_on_new_map_confirmed)
|
||||
|
|
@ -558,11 +613,26 @@ func _build_dialogs_and_overlays() -> void:
|
|||
add_child(help_overlay)
|
||||
|
||||
|
||||
## Ziehen aus der Objekt-Bibliothek: Vorschau folgt der Maus, Loslassen setzt.
|
||||
func _viewport_can_drop(at_position: Vector2, data: Variant) -> bool:
|
||||
if not (data is Dictionary) or not (data as Dictionary).has("svr_oid") or doc.is_empty() or assets_mod == null:
|
||||
return false
|
||||
assets_mod.drag_hover(str(data["svr_oid"]), at_position)
|
||||
return true
|
||||
|
||||
|
||||
func _viewport_drop(at_position: Vector2, data: Variant) -> void:
|
||||
if data is Dictionary and (data as Dictionary).has("svr_oid") and assets_mod != null:
|
||||
assets_mod.drag_drop(str(data["svr_oid"]), at_position)
|
||||
|
||||
|
||||
func _build_center_viewport() -> SubViewportContainer:
|
||||
var container := SubViewportContainer.new()
|
||||
container.stretch = true
|
||||
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
# Objekt-Kachel aus der Bibliothek in die Szene ziehen (25.10.).
|
||||
container.set_drag_forwarding(Callable(), _viewport_can_drop, _viewport_drop)
|
||||
|
||||
var viewport := SubViewport.new()
|
||||
viewport.own_world_3d = true
|
||||
|
|
@ -714,10 +784,7 @@ func _on_menu_action(id: int) -> void:
|
|||
func _sync_size_menu() -> void:
|
||||
if size_menu == null or not is_instance_valid(size_menu):
|
||||
return
|
||||
var popup := size_menu.get_popup()
|
||||
var cur := UIScale.step_index()
|
||||
for i in popup.item_count:
|
||||
popup.set_item_checked(i, i == cur)
|
||||
size_menu.select(UIScale.step_index())
|
||||
|
||||
|
||||
## Neue Stufe gewählt: speichern, kompletten UI-Neuaufbau mit dem neuen
|
||||
|
|
@ -732,7 +799,10 @@ func _on_ui_size_selected(index: int) -> void:
|
|||
_sync_size_menu()
|
||||
return
|
||||
UIScale.set_step_index(index)
|
||||
var settings_war_offen := settings_dialog != null and settings_dialog.visible
|
||||
_rebuild_ui_for_scale()
|
||||
if settings_war_offen:
|
||||
_open_settings.call_deferred()
|
||||
_set_status("UI-Größe: %s — sofort wirksam und gespeichert (bleibt auch nach Neustart)." % UIScale.step_label(index))
|
||||
|
||||
|
||||
|
|
@ -1363,11 +1433,10 @@ func _update_sym_ui() -> void:
|
|||
if sym_menu == null or sym_label == null:
|
||||
return
|
||||
var label := "Symmetrie: %s" % SymmetryCore.mode_label(sym_mode)
|
||||
sym_menu.text = label
|
||||
sym_label.text = label
|
||||
var popup := sym_menu.get_popup()
|
||||
for i in SymmetryCore.MODE_LABELS.size():
|
||||
popup.set_item_checked(i, i == sym_mode)
|
||||
sym_menu.select(sym_mode)
|
||||
if sym_mod != null and sym_mod.has_method("refresh_mode"):
|
||||
sym_mod.call("refresh_mode")
|
||||
|
||||
|
||||
# --------------------------------------------------------------- History ----
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ var _recent_box: VBoxContainer # Zuletzt-benutzt-Ansicht (im Scroll)
|
|||
var _recent_grid: GridContainer
|
||||
var _recent_tiles: Dictionary = {} # id → Button (Zuletzt-Ansicht, frisch gebaut)
|
||||
var _recent_ids: Array = [] # Reihenfolge: neueste zuerst
|
||||
var _drag_oid := "" # Kachel, die gerade gezogen wird
|
||||
var _drag_started_placement := false # Ziehen hat das Platzieren gestartet
|
||||
var selected_oid: String = "" # per Klick gewähltes Objekt
|
||||
var _id_font_normal: int = 0 # ID-Schriftgröße (skaliert), Normalzustand
|
||||
var _empty_hint: Label # Leerzustand (U7): kein Treffer / leerer Bereich
|
||||
|
|
@ -476,6 +478,9 @@ func _build_tile(oid: String) -> Button:
|
|||
btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)"
|
||||
btn.pressed.connect(_on_tile_pressed.bind(oid))
|
||||
btn.gui_input.connect(_on_tile_gui_input.bind(oid))
|
||||
# Ziehen & Loslassen (25.10.): Kachel anklicken, gedrückt halten, in die Szene
|
||||
# ziehen — beim Loslassen wird das Objekt dort gesetzt (Doppelklick geht weiter).
|
||||
btn.set_drag_forwarding(_tile_drag_data.bind(oid, btn), Callable(), Callable())
|
||||
btn.mouse_entered.connect(_on_tile_hover.bind(oid, true))
|
||||
btn.mouse_exited.connect(_on_tile_hover.bind(oid, false))
|
||||
var tex := LibraryBrowser.load_thumb(oid)
|
||||
|
|
@ -610,6 +615,65 @@ func _on_tile_gui_input(event: InputEvent, oid: String) -> void:
|
|||
_activate_tile(oid)
|
||||
|
||||
|
||||
## Ziehen-Start einer Kachel: Vorschau-Etikett an der Maus, Daten = Objekt-ID.
|
||||
func _tile_drag_data(_at_position: Vector2, oid: String, btn: Button) -> Variant:
|
||||
if main.doc.is_empty():
|
||||
return null
|
||||
_select_tile(oid)
|
||||
_drag_oid = oid
|
||||
var prev := PanelContainer.new()
|
||||
var box := HBoxContainer.new()
|
||||
prev.add_child(box)
|
||||
var tex := LibraryBrowser.load_thumb(oid)
|
||||
if tex != null:
|
||||
var tr := TextureRect.new()
|
||||
tr.texture = tex
|
||||
tr.custom_minimum_size = UIScale.v2(64, 64)
|
||||
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
box.add_child(tr)
|
||||
var lbl := Label.new()
|
||||
lbl.text = LibraryBrowser.entry_label(oid, (main.manifest.get("objects", {}) as Dictionary).get(oid, {}))
|
||||
box.add_child(lbl)
|
||||
prev.modulate.a = 0.85
|
||||
if btn.get_viewport().gui_is_dragging():
|
||||
btn.set_drag_preview(prev)
|
||||
else:
|
||||
prev.free()
|
||||
return {"svr_oid": oid}
|
||||
|
||||
|
||||
## Maus mit gezogener Kachel ÜBER der 3D-Ansicht: Platzier-Vorschau folgt der Maus.
|
||||
func drag_hover(oid: String, viewport_pos: Vector2) -> void:
|
||||
if str(main.get("tool_mode")) != "objects":
|
||||
main.call("_set_tool_mode", "objects")
|
||||
if not (main.interaction.placing and main.interaction.place_oid == oid):
|
||||
main.interaction.start_placement(oid)
|
||||
_drag_started_placement = true
|
||||
main.interaction._ghost_update(viewport_pos)
|
||||
|
||||
|
||||
## Loslassen über der 3D-Ansicht: genau wie ein Setz-Klick.
|
||||
func drag_drop(oid: String, viewport_pos: Vector2) -> void:
|
||||
drag_hover(oid, viewport_pos)
|
||||
main.interaction._place_object(viewport_pos, false)
|
||||
if main.interaction.placing and not BauKatalog.ist_bau(oid):
|
||||
main.interaction.cancel_placement() # daneben gezielt: Vorschau nicht hängen lassen
|
||||
_drag_started_placement = false
|
||||
_drag_oid = ""
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what != NOTIFICATION_DRAG_END or _drag_oid.is_empty():
|
||||
return
|
||||
# Losgelassen ÜBER der Szene → drag_drop hat aufgeräumt. Sonst (abgebrochen,
|
||||
# anderswo losgelassen) die Vorschau wieder wegnehmen.
|
||||
if _drag_started_placement and main != null and main.interaction.placing:
|
||||
main.interaction.cancel_placement()
|
||||
_drag_started_placement = false
|
||||
_drag_oid = ""
|
||||
|
||||
|
||||
func _select_tile(oid: String) -> void:
|
||||
var changed := selected_oid != oid
|
||||
selected_oid = oid
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
extends "res://src/ui/panels/editor_panel.gd"
|
||||
## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe) plus ein
|
||||
## paar passende Module aus dem Military-Desert-Pack. Ein Klick auf ein Teil startet
|
||||
## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe). Fertige
|
||||
## Pack-Objekte (Container, Hangar-Tore, Sperren …) gehören in die Objekt-Bibliothek
|
||||
## unten, NICHT hierher. Ein Klick auf ein Teil startet
|
||||
## das Platzieren (Vorschau rastet an Zelle/Kante/Ebene, siehe build_snap.gd); jedes
|
||||
## gesetzte Teil ist ein normales Objekt und gehört zum laufenden Gebäude (Gruppe).
|
||||
## Gebäude kopieren/löschen/auswählen läuft über die Knöpfe unten — alles Undo-fähig.
|
||||
|
||||
## Pack-Module, die zum Gebäudebau passen (IDs stabil, siehe Bibliothek).
|
||||
const PACK_MODULE := [
|
||||
{"label": "Container", "oids": ["container_001", "container_002", "container_003", "container_004",
|
||||
"container_005", "container_006", "container_007", "container_008"]},
|
||||
{"label": "Hangar-Tore", "oids": ["hangar_door_001", "hangar_door_002", "hangar_door_003"]},
|
||||
{"label": "Betonsperren", "oids": ["barrier_001", "barrier_002", "barrier_003", "barrier_004"]},
|
||||
]
|
||||
|
||||
var bau_buttons: Dictionary = {} # oid → Button
|
||||
var info_label: Label
|
||||
var _group: ButtonGroup
|
||||
|
|
@ -32,15 +25,7 @@ func build() -> void:
|
|||
add_child(_muted_label("Wähle ein Teil und klicke in die Karte. Wände rasten an die nächste Zellkante, Böden und Dächer in die Zellmitte (Raster 2 m). Klick auf die Oberkante einer Wand stapelt das nächste Geschoss, Klick auf die Seite ersetzt das Teil (z. B. Wand → Tür). R dreht, Esc beendet."))
|
||||
for gruppe in BauKatalog.GRUPPEN:
|
||||
add_child(_field_label(str(gruppe["label"])))
|
||||
add_child(_knopf_raster(gruppe["oids"] as Array, true))
|
||||
var pack := _pack_gruppen()
|
||||
if not pack.is_empty():
|
||||
add_child(_separator())
|
||||
add_child(_header_label("Pack-Module"))
|
||||
add_child(_muted_label("Fertige Teile aus dem Military-Desert-Pack — normale Objekte, werden auf den Boden gesetzt."))
|
||||
for pg in pack:
|
||||
add_child(_field_label(str(pg["label"])))
|
||||
add_child(_knopf_raster(pg["oids"] as Array, false))
|
||||
add_child(_knopf_raster(gruppe["oids"] as Array))
|
||||
add_child(_separator())
|
||||
add_child(_header_label("Gebäude"))
|
||||
info_label = _muted_label("")
|
||||
|
|
@ -54,22 +39,10 @@ func build() -> void:
|
|||
add_child(_aktion("🗑 Gebäude löschen", "Löscht alle Teile des Gebäudes (Strg+Z holt es zurück).",
|
||||
func() -> void: main.call("_bau_gebaeude_loeschen")))
|
||||
refresh_info()
|
||||
wrap_in_scroll()
|
||||
|
||||
|
||||
func _pack_gruppen() -> Array:
|
||||
var lib: Dictionary = main.manifest.get("objects", {}) if main != null else {}
|
||||
var out: Array = []
|
||||
for pg in PACK_MODULE:
|
||||
var oids: Array = []
|
||||
for oid in (pg["oids"] as Array):
|
||||
if lib.has(oid):
|
||||
oids.append(oid)
|
||||
if not oids.is_empty():
|
||||
out.append({"label": pg["label"], "oids": oids})
|
||||
return out
|
||||
|
||||
|
||||
func _knopf_raster(oids: Array, bau: bool) -> GridContainer:
|
||||
func _knopf_raster(oids: Array) -> GridContainer:
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 2
|
||||
grid.add_theme_constant_override("h_separation", UIScale.px(8))
|
||||
|
|
@ -82,14 +55,9 @@ func _knopf_raster(oids: Array, bau: bool) -> GridContainer:
|
|||
btn.custom_minimum_size = UIScale.v2(148, 44)
|
||||
btn.clip_text = true
|
||||
var poly := int((lib.get(oid, {}) as Dictionary).get("poly", 0))
|
||||
if bau:
|
||||
btn.text = BauKatalog.label(oid)
|
||||
btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid),
|
||||
str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly]
|
||||
else:
|
||||
var name_de := str((lib.get(oid, {}) as Dictionary).get("name_de", oid))
|
||||
btn.text = name_de
|
||||
btn.tooltip_text = "%s — %d Dreiecke" % [name_de, poly]
|
||||
btn.pressed.connect(_on_teil_gedrueckt.bind(str(oid), btn))
|
||||
bau_buttons[str(oid)] = btn
|
||||
grid.add_child(btn)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ var spawn_delete_btn: Button
|
|||
|
||||
|
||||
static func spec() -> Dictionary:
|
||||
return {"titel": "Prüfen", "gruppe": "rechts", "order": 50}
|
||||
return {"titel": "Spawn-Zonen", "gruppe": "map", "order": 10}
|
||||
|
||||
|
||||
func panel_title() -> String:
|
||||
|
|
@ -27,6 +27,7 @@ func panel_title() -> String:
|
|||
## Live-Status IMMER sichtbar + Werkzeug-Button + Bedien-Panel (Team/Radius/
|
||||
## Auswahl/Löschen). Ein/Aus des Werkzeugs bleibt beim Koordinator (main.gd).
|
||||
func build() -> void:
|
||||
add_child(_header_label("Spawn-Zonen"))
|
||||
# --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar ---
|
||||
spawn_status = Label.new()
|
||||
spawn_status.text = ""
|
||||
|
|
|
|||
|
|
@ -25,6 +25,27 @@ func panel_title() -> String:
|
|||
return "Panel"
|
||||
|
||||
|
||||
## Lange Tabs (Map, Bauen): den ganzen Inhalt in einen senkrechten Scroll-Bereich
|
||||
## legen, damit die rechte Spalte nicht mit dem Inhalt wächst. Rückgabe = die
|
||||
## innere Box (dort hängen spätere Abschnitte an).
|
||||
func wrap_in_scroll() -> VBoxContainer:
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.name = "%sScroll" % name
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var inner := VBoxContainer.new()
|
||||
inner.name = "%sInhalt" % name
|
||||
inner.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
inner.add_theme_constant_override("separation", UIScale.px(8))
|
||||
for child in get_children():
|
||||
remove_child(child)
|
||||
inner.add_child(child)
|
||||
scroll.add_child(inner)
|
||||
add_child(scroll)
|
||||
return inner
|
||||
|
||||
|
||||
# --- Farb-Zugriff NUR über das Theme (Karte t_2336c58e/U7) ------------------
|
||||
## Farben kommen ausschließlich aus theme/editor_theme.tres (Abschnitt
|
||||
## Editor/colors/…) — keine Farbwerte mehr im Panel-Code. Der Wert liest
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ var sky_preview: TextureRect # Vorschau der gewählten Skybox (obere Panora
|
|||
var prop_water: HSlider
|
||||
var terrain_info: Label
|
||||
|
||||
var _sections: VBoxContainer # Inhalt des Map-Tabs (im Scroll-Bereich)
|
||||
var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder
|
||||
|
||||
## Karte t_28fdef03: Slider-Stellung für „Kein Wasser“ (Meta-Vertrag für
|
||||
|
|
@ -110,6 +111,13 @@ func build() -> void:
|
|||
prop_weather.item_selected.connect(_on_weather_changed)
|
||||
prop_sky.item_selected.connect(_on_sky_changed)
|
||||
prop_water.value_changed.connect(_on_water_changed)
|
||||
_sections = wrap_in_scroll()
|
||||
|
||||
|
||||
## Hier hängen die Abschnitte der Registry-Gruppe "map" (Spawn-Zonen, Test-Start,
|
||||
## Symmetrie) unter den Map-Eigenschaften.
|
||||
func sections_host() -> Control:
|
||||
return _sections
|
||||
|
||||
|
||||
func _on_prop_changed(text: String, field: String) -> void:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://cs4xao3sf5y7n
|
||||
|
|
@ -12,7 +12,7 @@ extends RefCounted
|
|||
## in derselben Datei (gleiche Falle wie früher bei match_hud.gd).
|
||||
##
|
||||
## Struktur je Eintrag: titel (Tab-/Überschrift), gruppe ("rechts" = rechtes
|
||||
## TabControl, "links" = linke Spalte, "unten" = Assets-Bereich, "toolbar" /
|
||||
## TabControl, "links" = linke Spalte, "unten" = Assets-Bereich, "map" = Abschnitte im Map-Tab, "dialog" = Einstellungen-Fenster, "toolbar" /
|
||||
## "status" = Leisten-Aktionen), order (Sortier-Schlüssel), skript (preload-Pfad
|
||||
## des Panel-Moduls). Ein Bereich erscheint GENAU EINMAL im Baum (test_ui_layout
|
||||
## prüft das gegen Doppel-Namen).
|
||||
|
|
@ -28,8 +28,6 @@ const PANELS: Array = [
|
|||
# --- rechts: Tab-Gruppe (Unity-Inspector-Spalte) ---
|
||||
{"id": "inspektor", "titel": "Inspektor", "gruppe": "rechts", "order": 10,
|
||||
"skript": "res://src/ui/inspector_panel.gd"},
|
||||
{"id": "auswahl", "titel": "Auswahl", "gruppe": "rechts", "order": 20,
|
||||
"skript": "res://src/ui/panels/selection_panel.gd"},
|
||||
{"id": "map", "titel": "Map", "gruppe": "rechts", "order": 30,
|
||||
"skript": "res://src/ui/panels/map_panel.gd"},
|
||||
{"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40,
|
||||
|
|
@ -39,28 +37,26 @@ const PANELS: Array = [
|
|||
"skript": "res://src/ui/panels/malen_panel.gd"},
|
||||
{"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47,
|
||||
"skript": "res://src/ui/panels/bauen_panel.gd"},
|
||||
{"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50,
|
||||
# --- map: Abschnitte UNTER den Map-Eigenschaften (Tab „Map“) ---
|
||||
# 25.10. (Test-Befund): Prüfen/Test-Start/Symmetrie sind keine eigenen Tabs
|
||||
# mehr. Spawn-Zonen, Test-Start-Info und Symmetrie wohnen im Map-Tab.
|
||||
{"id": "check", "titel": "Spawn-Zonen", "gruppe": "map", "order": 10,
|
||||
"skript": "res://src/ui/panels/check_panel.gd"},
|
||||
{"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60,
|
||||
{"id": "teststart", "titel": "Test-Start", "gruppe": "map", "order": 20,
|
||||
"skript": "res://src/ui/panels/test_start_panel.gd"},
|
||||
# T3 Komfort (Karte t_aaff5dcc): Maus-Empfindlichkeit u. a. — Registry-Weg
|
||||
# statt main.gd (Operator-Vorgabe 20.10.).
|
||||
{"id": "einstellungen", "titel": "Einstellungen", "gruppe": "rechts", "order": 70,
|
||||
"skript": "res://src/ui/panels/settings_panel.gd"},
|
||||
# M5 (Karte t_60014669): Ein-Klick „Karte symmetrisch machen“ + Check mit
|
||||
# Klartext-Bericht — ebenfalls Registry-Weg, main.gd bleibt unangetastet.
|
||||
{"id": "symmetrie", "titel": "Symmetrie", "gruppe": "rechts", "order": 80,
|
||||
{"id": "symmetrie", "titel": "Symmetrie", "gruppe": "map", "order": 30,
|
||||
"skript": "res://src/ui/panels/symmetry_panel.gd"},
|
||||
# --- dialog: Inhalt des Einstellungen-Fensters (Knopf in der Kopfleiste) ---
|
||||
# T3 Komfort (Karte t_aaff5dcc) + 25.10.: Gitter, Höhenlinien, Symmetrie-
|
||||
# Modus, UI-Größe, Raster-Snap, Gizmo-Schritt, Karte prüfen.
|
||||
{"id": "einstellungen", "titel": "Einstellungen", "gruppe": "dialog", "order": 10,
|
||||
"skript": "res://src/ui/panels/settings_panel.gd"},
|
||||
# --- links: Vorbereitet für U2 (Szenen-Baum); Bibliothek ist UNTEN ---
|
||||
{"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10,
|
||||
"skript": "res://src/ui/panels/scene_tree_panel.gd"},
|
||||
# --- unten: Objekt-Bibliothek / Assets (Unity-Projekt-Fenster-Platz) ---
|
||||
{"id": "assets", "titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10,
|
||||
"skript": "res://src/ui/panels/assets_panel.gd"},
|
||||
# Objekt-Werkzeuge (Boden-Setzen/Raster-Snap, vor U1 links) — erhalten
|
||||
# geblieben als eigener Tab unten, damit nichts verschwindet.
|
||||
{"id": "werkzeuge", "titel": "Objekt-Werkzeuge", "gruppe": "unten", "order": 20,
|
||||
"skript": "res://src/ui/panels/object_tools_panel.gd"},
|
||||
]
|
||||
|
||||
## Leisten-Aktionen (Toolbar-Buttons/Menüs — KEINE Panels, melden sich ditto
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ extends "res://src/ui/panels/editor_panel.gd"
|
|||
## "links"), Zustand/Signale wie vorher.
|
||||
|
||||
var content_tree: Tree
|
||||
var group_btn: Button
|
||||
var ungroup_btn: Button
|
||||
var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum
|
||||
var _tree_select_pending := false # multi_selected gesammelt, Auswertung steht aus
|
||||
var _obj_index_map: Dictionary = {} # obj_index -> TreeItem (schnelle Zwei-Wege-Kopplung)
|
||||
|
||||
# U3 (Karte t_a4bf9b8c): Kontextmenü, Umbenennen, Lösch-Rückfrage, Drag&Drop.
|
||||
|
|
@ -30,9 +33,35 @@ func build() -> void:
|
|||
content_tree.allow_reselect = true
|
||||
content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2)
|
||||
content_tree.hide_root = true
|
||||
# FIX 25.10. (Test-Befund „Liste anklicken wählt in der Szene nichts aus“):
|
||||
# Im Modus SELECT_MULTI feuert Godot NIE item_selected, sondern nur
|
||||
# multi_selected (pro Zeile). Beide anschließen; das Sammeln übernimmt der
|
||||
# Aufrufer-Merker, damit ein Klick mit mehreren Signalen nur EINMAL wirkt.
|
||||
content_tree.item_selected.connect(_on_tree_selected)
|
||||
content_tree.multi_selected.connect(_on_tree_multi_selected)
|
||||
add_child(content_tree)
|
||||
|
||||
# Gruppen verwalten (25.10.: der Tab „Auswahl“ ist weg): mehrere Objekte in der
|
||||
# Liste (Strg+Klick) oder in der Szene markieren, dann hier gruppieren/lösen.
|
||||
# Rechtsklick auf eine Zeile bietet dieselben Aktionen.
|
||||
var group_row := HBoxContainer.new()
|
||||
group_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(group_row)
|
||||
group_btn = Button.new()
|
||||
group_btn.text = "🔗 Gruppieren"
|
||||
group_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
group_btn.custom_minimum_size = UIScale.v2(0, 40)
|
||||
group_btn.tooltip_text = "Markierte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam. Mehrere markieren: Strg+Klick in der Liste oder Rahmen in der Szene."
|
||||
group_btn.pressed.connect(func() -> void: main.interaction.group_selection())
|
||||
group_row.add_child(group_btn)
|
||||
ungroup_btn = Button.new()
|
||||
ungroup_btn.text = "⛓ Gruppe lösen"
|
||||
ungroup_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
ungroup_btn.custom_minimum_size = UIScale.v2(0, 40)
|
||||
ungroup_btn.tooltip_text = "Die Gruppe der Auswahl auflösen (Strg+Umschalt+G) — die Objekte bleiben, nur die Bindung löst sich"
|
||||
ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
|
||||
group_row.add_child(ungroup_btn)
|
||||
|
||||
# U3 (Karte t_a4bf9b8c): Rechtsklick-Kontextmenü + Drag&Drop für Gruppen.
|
||||
content_tree.gui_input.connect(_on_tree_gui_input)
|
||||
# Tree.set_drag_forwarding: die drei Virtuals (_get_drag_data/_can_drop_data/
|
||||
|
|
@ -61,6 +90,20 @@ func build() -> void:
|
|||
add_child(_separator())
|
||||
|
||||
|
||||
## multi_selected kommt je Zeile (auch beim Abwählen): die Auswahl EINMAL pro
|
||||
## Frame auswerten, nicht pro Signal.
|
||||
func _on_tree_multi_selected(_item: TreeItem, _column: int, _selected: bool) -> void:
|
||||
if _tree_select_lock or _tree_select_pending:
|
||||
return
|
||||
_tree_select_pending = true
|
||||
_apply_tree_selection.call_deferred()
|
||||
|
||||
|
||||
func _apply_tree_selection() -> void:
|
||||
_tree_select_pending = false
|
||||
_on_tree_selected()
|
||||
|
||||
|
||||
## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt.
|
||||
## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl,
|
||||
## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln
|
||||
|
|
|
|||
|
|
@ -1,139 +1,21 @@
|
|||
extends "res://src/ui/panels/editor_panel.gd"
|
||||
## Auswahl-Panel (U1, Karte t_eaccbfed — aus main.gd ausgezogen, Logik
|
||||
## UNVERÄNDERT von dort übernommen): Buttons Drehen/Löschen/Duplizieren/
|
||||
## Spiegeln/Anheben/Absenken + Gruppen-Buttons + Gizmo-Schrittweite.
|
||||
extends Node
|
||||
## Auswahl-Logik (U1, Karte t_eaccbfed — aus main.gd ausgezogen). Seit 25.10.
|
||||
## KEIN Tab mehr (Test-Befund): Drehen/Anheben/Duplizieren/Löschen laufen in der
|
||||
## 3D-Ansicht (Werkzeugleiste, Gizmo, Tasten), Gruppen im Map-Inhalt links, die
|
||||
## Gizmo-Schrittweite im Einstellungen-Fenster. Übrig bleibt die Synchronisation:
|
||||
## Inspektor nachziehen, Gizmo-Schrittweite-Feld folgen, Statuszeile zur frischen
|
||||
## Auswahl (Klartext + Poly-Budget-Warnung).
|
||||
##
|
||||
## Verdrahtung: main.gd ruft sync_selection() an allen Auswahl-Wegen
|
||||
## (vorher: _sync_selection_panel) und selection_status(idx) bei frischer
|
||||
## Auswahl (vorher: _selection_status). Alle Objekt-Operationen laufen über
|
||||
## main.interaction — der Vertrag zu interaction.gd bleibt identisch.
|
||||
|
||||
var sel_label: Label
|
||||
var sel_rotate_btn: Button
|
||||
var sel_delete_btn: Button
|
||||
var sel_dup_btn: Button
|
||||
var sel_mirror_x_btn: Button
|
||||
var sel_mirror_z_btn: Button
|
||||
var sel_raise_btn: Button
|
||||
var sel_lower_btn: Button
|
||||
var sel_group_btn: Button
|
||||
var sel_ungroup_btn: Button
|
||||
var gizmo_step_box: SpinBox
|
||||
var main # src/ui/main.gd (script-erweitert → untypisiert)
|
||||
|
||||
|
||||
static func spec() -> Dictionary:
|
||||
return {"titel": "Auswahl", "gruppe": "rechts", "order": 20}
|
||||
|
||||
|
||||
func panel_title() -> String:
|
||||
return "Auswahl"
|
||||
|
||||
|
||||
func build() -> void:
|
||||
# --- Auswahl (Anfassen/Greifen) — 1:1 aus der alten _build_right_panel ---
|
||||
sel_label = Label.new()
|
||||
sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an."
|
||||
sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
add_child(sel_label)
|
||||
var sel_btns := HBoxContainer.new()
|
||||
sel_btns.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(sel_btns)
|
||||
sel_rotate_btn = Button.new()
|
||||
sel_rotate_btn.text = "Drehen (R)"
|
||||
sel_rotate_btn.custom_minimum_size = UIScale.v2(140, 40)
|
||||
sel_rotate_btn.tooltip_text = "Ausgewähltes Objekt 15° drehen (auch: Mausrad)"
|
||||
sel_rotate_btn.pressed.connect(func() -> void: main.interaction.rotate_selected(15.0))
|
||||
sel_btns.add_child(sel_rotate_btn)
|
||||
sel_delete_btn = Button.new()
|
||||
sel_delete_btn.text = "Löschen (Entf)"
|
||||
sel_delete_btn.custom_minimum_size = UIScale.v2(140, 40)
|
||||
sel_delete_btn.tooltip_text = "Ausgewähltes Objekt löschen (Strg+Z holt es zurück)"
|
||||
sel_delete_btn.pressed.connect(main.interaction.delete_selected)
|
||||
sel_btns.add_child(sel_delete_btn)
|
||||
|
||||
# Objekt-Ops (Karte t_7567a06f): Duplizieren · Spiegeln · Anheben/Absenken.
|
||||
var dup_row := HBoxContainer.new()
|
||||
dup_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(dup_row)
|
||||
sel_dup_btn = _sel_op_button("⧉ Duplizieren (Strg+D)",
|
||||
"Legt eine Kopie daneben — leicht versetzt, direkt angewählt. Strg+D geht auch.",
|
||||
func() -> void: main.interaction.duplicate_selected())
|
||||
dup_row.add_child(sel_dup_btn)
|
||||
|
||||
var mirror_row := HBoxContainer.new()
|
||||
mirror_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(mirror_row)
|
||||
sel_mirror_x_btn = _sel_op_button("⇄ Links↔Rechts",
|
||||
"Spiegelt das Objekt an seiner Stelle (links↔rechts, X-Achse) — die Blickrichtung wird mitgespiegelt.",
|
||||
func() -> void: main.interaction.mirror_selected("x"))
|
||||
mirror_row.add_child(sel_mirror_x_btn)
|
||||
sel_mirror_z_btn = _sel_op_button("⇅ Vor↔Zurück",
|
||||
"Spiegelt das Objekt an seiner Stelle (vor↔zurück, Z-Achse) — die Blickrichtung wird mitgespiegelt.",
|
||||
func() -> void: main.interaction.mirror_selected("z"))
|
||||
mirror_row.add_child(sel_mirror_z_btn)
|
||||
|
||||
var lift_row := HBoxContainer.new()
|
||||
lift_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(lift_row)
|
||||
sel_raise_btn = _sel_op_button("▲ Anheben (Bild↑)",
|
||||
"Objekt 0,25 m anheben — es schwebt dann, auch wenn der Boden darunter anders ist. Bild↑ geht auch.",
|
||||
func() -> void: main.interaction.lift_selected(ObjectOps.LIFT_STEP_M))
|
||||
lift_row.add_child(sel_raise_btn)
|
||||
sel_lower_btn = _sel_op_button("▼ Absenken (Bild↓)",
|
||||
"Objekt 0,25 m absenken — stoppt am Boden (nichts wird in Hügel begraben). Bild↓ geht auch.",
|
||||
func() -> void: main.interaction.lift_selected(-ObjectOps.LIFT_STEP_M))
|
||||
lift_row.add_child(sel_lower_btn)
|
||||
|
||||
# Gruppen-Buttons (Karte t_f6f29d86): Mehrfachauswahl → Gruppe
|
||||
var group_btns := HBoxContainer.new()
|
||||
group_btns.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(group_btns)
|
||||
sel_group_btn = Button.new()
|
||||
sel_group_btn.text = "🔗 Gruppieren"
|
||||
sel_group_btn.custom_minimum_size = UIScale.v2(150, 40)
|
||||
sel_group_btn.tooltip_text = "Ausgewählte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam"
|
||||
sel_group_btn.pressed.connect(func() -> void: main.interaction.group_selection())
|
||||
group_btns.add_child(sel_group_btn)
|
||||
sel_ungroup_btn = Button.new()
|
||||
sel_ungroup_btn.text = "⛓ Gruppe lösen"
|
||||
sel_ungroup_btn.custom_minimum_size = UIScale.v2(150, 40)
|
||||
sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich"
|
||||
sel_ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
|
||||
group_btns.add_child(sel_ungroup_btn)
|
||||
|
||||
# --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen-
|
||||
# Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die
|
||||
# Voreinstellungen (fein/normal/grob); dieses Feld setzt sie exakt und
|
||||
# überlebt UI-Rebuilds, weil der Wert in main.gizmo_step_m liegen bleibt.
|
||||
add_child(_header_label("Gizmo-Einstellungen"))
|
||||
var gizmo_row := HBoxContainer.new()
|
||||
gizmo_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||
add_child(gizmo_row)
|
||||
gizmo_row.add_child(_field_label("Schrittweite"))
|
||||
gizmo_step_box = SpinBox.new()
|
||||
gizmo_step_box.min_value = 0.01
|
||||
gizmo_step_box.max_value = 10.0
|
||||
gizmo_step_box.step = 0.01
|
||||
gizmo_step_box.value = main.gizmo_step_m
|
||||
gizmo_step_box.custom_minimum_size = UIScale.v2(110, 30)
|
||||
gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)."
|
||||
gizmo_row.add_child(gizmo_step_box)
|
||||
gizmo_step_box.value_changed.connect(func(v: float) -> void:
|
||||
main.gizmo_step_m = v
|
||||
if main.gizmo != null and is_instance_valid(main.gizmo):
|
||||
main.gizmo.set_step_m(v)
|
||||
)
|
||||
add_child(_separator())
|
||||
|
||||
|
||||
## Ein Auswahl-Button mit Klartext-Tipp (aus main.gd übernommen).
|
||||
func _sel_op_button(text: String, tip: String, action: Callable) -> Button:
|
||||
var btn := Button.new()
|
||||
btn.text = text
|
||||
btn.custom_minimum_size = UIScale.v2(140, 40)
|
||||
btn.tooltip_text = tip
|
||||
btn.pressed.connect(action)
|
||||
return btn
|
||||
func setup(p_main) -> void:
|
||||
main = p_main
|
||||
|
||||
|
||||
## Objekt-Inspektor + Auswahl-Panel synchron halten (vorher
|
||||
|
|
@ -145,54 +27,9 @@ func sync_selection() -> void:
|
|||
# Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad
|
||||
# WÄHREND eines Zugs kann sie im Gizmo ändern (cycle_step) — das Panel-
|
||||
# Feld folgt ohne eigene value_changed-Rückkopplung (set_value_no_signal).
|
||||
if gizmo_step_box != null and is_instance_valid(gizmo_step_box) \
|
||||
and not gizmo_step_box.has_focus():
|
||||
gizmo_step_box.set_value_no_signal(main.gizmo_step_m)
|
||||
var doc: Dictionary = main.doc
|
||||
var objects: Array = doc.get("objects", [])
|
||||
var sel: Array = main.interaction.selected_indices
|
||||
var valid: Array = []
|
||||
for i in sel:
|
||||
if i is int and i >= 0 and i < objects.size():
|
||||
valid.append(i)
|
||||
var has_sel := not valid.is_empty()
|
||||
for btn in [sel_rotate_btn, sel_delete_btn, sel_dup_btn, sel_mirror_x_btn,
|
||||
sel_mirror_z_btn, sel_raise_btn, sel_lower_btn]:
|
||||
btn.disabled = not has_sel
|
||||
var has_group := false
|
||||
for i in valid:
|
||||
if GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED:
|
||||
has_group = true
|
||||
break
|
||||
sel_group_btn.disabled = valid.size() < 2
|
||||
sel_ungroup_btn.disabled = not has_group
|
||||
if not has_sel:
|
||||
sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an."
|
||||
return
|
||||
if valid.size() == 1:
|
||||
var idx: int = valid[0]
|
||||
var obj: Dictionary = objects[idx]
|
||||
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
|
||||
var lift := ObjectOps.lift_offset(doc, idx)
|
||||
var lift_text := "am Boden" if absf(lift) < 0.02 else ("%+.2f m über dem Boden" % lift)
|
||||
var group_line := ""
|
||||
var gid := GroupOps.group_id(obj)
|
||||
if gid != GroupOps.UNGROUPED:
|
||||
group_line = "\nGruppe %d — zieht & dreht GEMEINSAM (Lösen: Button unten)" % (gid + 1)
|
||||
sel_label.text = "%s (Objekt %d von %d)\nPosition: %.0f, %.0f m · Höhe: %s · Richtung: %.0f°%s\nZiehen: verschieben · R oder Mausrad: drehen (15°)\nBild↑/↓: anheben/absenken · Strg+D: duplizieren\nSpiegeln/Löschen: Buttons oben · Strg+Z: zurück" % [
|
||||
main.object_label(str(obj.get("id", ""))), idx + 1, objects.size(),
|
||||
float(pos[0]), float(pos[2]), lift_text, float(obj.get("rot_y_deg", 0.0)), group_line]
|
||||
return
|
||||
# Mehrfachauswahl: Liste aller gewählten Objekte (max. 8 Zeilen, dann „…“).
|
||||
var names := PackedStringArray()
|
||||
for i in valid:
|
||||
if names.size() < 8:
|
||||
names.append(main.object_label(str((objects[i] as Dictionary).get("id", "?"))))
|
||||
var more := ""
|
||||
if valid.size() > 8:
|
||||
more = "\n… und %d weitere" % (valid.size() - 8)
|
||||
sel_label.text = "%d Objekte ausgewählt:\n%s%s\nGemeinsam: ziehen (Gruppe wandert zusammen), drehen um die Mitte, Entf löscht alle." % [
|
||||
valid.size(), " · ".join(names), more]
|
||||
var step_box: SpinBox = main.gizmo_step_box
|
||||
if step_box != null and is_instance_valid(step_box) and not step_box.has_focus():
|
||||
step_box.set_value_no_signal(main.gizmo_step_m)
|
||||
|
||||
|
||||
## Hilfs-Meldung zur frischen Auswahl (Klartext + Budget-Warnung + nächster
|
||||
|
|
@ -202,9 +39,6 @@ func selection_status(idx: int) -> void:
|
|||
# 3D-Klick, Rahmen-Ausziehen und Map-Inhalt-Baum.
|
||||
if main.inspector != null:
|
||||
main.inspector.call("refresh")
|
||||
# Fix (Vorbestand, beim Inspektor-Einbau gefunden): select_object/
|
||||
# select_many füllten das alte „Auswahl“-Feld NIE — sel_label blieb auf
|
||||
# „Nichts ausgewählt“ stehen, während die Statuszeile das Objekt nannte.
|
||||
# sync_selection ändert bewusst KEINEN Statuszeilen-Text (Stummsync).
|
||||
sync_selection()
|
||||
var doc: Dictionary = main.doc
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
extends "res://src/ui/panels/editor_panel.gd"
|
||||
## Einstellungen-Bereich (rechte Spalte — Karte t_aaff5dcc, T3 Komfort).
|
||||
## Einstellungen (Fenster „⚙ Einstellungen“ in der Kopfleiste — Karte
|
||||
## t_aaff5dcc, T3 Komfort; 25.10. Test-Befund: Gitter, Höhenlinien, Symmetrie-
|
||||
## Modus, UI-Größe, Raster-Snap, Gizmo-Schrittweite und „Karte prüfen“ ziehen
|
||||
## aus Kopfleiste/Objekt-Werkzeugen hierher).
|
||||
##
|
||||
## Registry-Weg (Operator-Vorgabe 20.10.): main.gd bleibt unangetastet, das
|
||||
## Modul meldet sich nur über src/ui/panels/registry.gd an. Der ZUSTAND lebt
|
||||
|
|
@ -12,9 +15,19 @@ var sens_box: SpinBox
|
|||
## Eintrag wie Taste V (PlaytestSettings, persistiert).
|
||||
var perspektive_box: OptionButton
|
||||
|
||||
# Ansicht / Platzieren (vorher Kopfleiste und Tab „Objekt-Werkzeuge“). main
|
||||
# hält dieselben Namen als Referenzen (grid_btn, contours_btn, size_menu, …).
|
||||
var grid_btn: CheckButton
|
||||
var contours_btn: CheckButton
|
||||
var size_menu: OptionButton
|
||||
var sym_menu: OptionButton
|
||||
var raster_snap_check: CheckButton
|
||||
var gizmo_step_box: SpinBox
|
||||
var check_btn: Button
|
||||
|
||||
|
||||
static func spec() -> Dictionary:
|
||||
return {"titel": "Einstellungen", "gruppe": "rechts", "order": 70}
|
||||
return {"titel": "Einstellungen", "gruppe": "dialog", "order": 10}
|
||||
|
||||
|
||||
func panel_title() -> String:
|
||||
|
|
@ -22,6 +35,72 @@ func panel_title() -> String:
|
|||
|
||||
|
||||
func build() -> void:
|
||||
add_child(_header_label("Ansicht"))
|
||||
grid_btn = CheckButton.new()
|
||||
grid_btn.text = "⊞ Maßstabsgitter (100-m-Raster)"
|
||||
grid_btn.custom_minimum_size = UIScale.v2(0, 36)
|
||||
grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus."
|
||||
grid_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_toggled", pressed))
|
||||
add_child(grid_btn)
|
||||
|
||||
contours_btn = CheckButton.new()
|
||||
contours_btn.text = "≈ Höhenlinien fixieren"
|
||||
contours_btn.custom_minimum_size = UIScale.v2(0, 36)
|
||||
contours_btn.tooltip_text = "Höhenlinien (1 m, alle 5 m kräftiger) FIXIERT an/aus. Ohne Fixierung erscheinen sie nur im Terrain-Werkzeug (F1). Taste L schaltet ebenfalls. Bei flachem Terrain (< 2 m) sind sie automatisch aus."
|
||||
contours_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_contours_toggled", pressed))
|
||||
add_child(contours_btn)
|
||||
|
||||
add_child(_field_label("UI-Größe"))
|
||||
size_menu = OptionButton.new()
|
||||
for i in UIScale.step_count():
|
||||
size_menu.add_item(UIScale.step_label(i), i)
|
||||
size_menu.custom_minimum_size = UIScale.v2(0, 36)
|
||||
size_menu.tooltip_text = "Größe der Oberfläche: Klein (wie bisher), Mittel (1,25-fach), Groß (1,5-fach). Wirkt sofort und bleibt gespeichert."
|
||||
size_menu.item_selected.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
|
||||
add_child(size_menu)
|
||||
|
||||
add_child(_separator())
|
||||
add_child(_header_label("Bauen & Platzieren"))
|
||||
raster_snap_check = CheckButton.new()
|
||||
raster_snap_check.text = "Raster-Snap (1 m)"
|
||||
raster_snap_check.custom_minimum_size = UIScale.v2(0, 36)
|
||||
raster_snap_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein (Strg gedrückt halten geht immer)"
|
||||
raster_snap_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed))
|
||||
add_child(raster_snap_check)
|
||||
|
||||
add_child(_field_label("Gizmo-Schrittweite (m)"))
|
||||
gizmo_step_box = SpinBox.new()
|
||||
gizmo_step_box.min_value = 0.01
|
||||
gizmo_step_box.max_value = 10.0
|
||||
gizmo_step_box.step = 0.01
|
||||
gizmo_step_box.value = main.gizmo_step_m
|
||||
gizmo_step_box.custom_minimum_size = UIScale.v2(0, 36)
|
||||
gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)."
|
||||
gizmo_step_box.value_changed.connect(func(v: float) -> void:
|
||||
main.gizmo_step_m = v
|
||||
if main.gizmo != null and is_instance_valid(main.gizmo):
|
||||
main.gizmo.set_step_m(v))
|
||||
add_child(gizmo_step_box)
|
||||
|
||||
add_child(_separator())
|
||||
add_child(_header_label("Symmetrie & Prüfen"))
|
||||
add_child(_field_label("Symmetrie-Modus"))
|
||||
sym_menu = OptionButton.new()
|
||||
for i in SymmetryCore.MODE_LABELS.size():
|
||||
sym_menu.add_item("Symmetrie: %s" % SymmetryCore.MODE_LABELS[i], i)
|
||||
sym_menu.custom_minimum_size = UIScale.v2(0, 36)
|
||||
sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt."
|
||||
sym_menu.item_selected.connect(func(index: int) -> void: main.call("_on_sym_mode_selected", index))
|
||||
add_child(sym_menu)
|
||||
|
||||
check_btn = Button.new()
|
||||
check_btn.text = "✅ Karte prüfen"
|
||||
check_btn.custom_minimum_size = UIScale.v2(0, 40)
|
||||
check_btn.tooltip_text = "Prüft, ob die Map bereit für den Export ist — sagt VORHER, was fehlt"
|
||||
check_btn.pressed.connect(func() -> void: main.call("_on_check_map"))
|
||||
add_child(check_btn)
|
||||
|
||||
add_child(_separator())
|
||||
add_child(_header_label("Testlauf-Einstellungen"))
|
||||
|
||||
add_child(_field_label("Maus-Empfindlichkeit (0,1 – 5,0)"))
|
||||
|
|
@ -67,3 +146,15 @@ func _on_perspektive_changed(index: int) -> void:
|
|||
var body: Variant = pt.get("body")
|
||||
if body != null:
|
||||
body.call("set_perspektive", dritte)
|
||||
|
||||
|
||||
## Schalter aus dem Editor-Zustand nachziehen (beim Öffnen des Fensters).
|
||||
func refresh() -> void:
|
||||
if main == null:
|
||||
return
|
||||
grid_btn.set_pressed_no_signal(bool(main.get("_grid_on")))
|
||||
contours_btn.set_pressed_no_signal(bool(main.get("_contours_on")))
|
||||
raster_snap_check.set_pressed_no_signal(bool(main.get("grid_snap")))
|
||||
gizmo_step_box.set_value_no_signal(float(main.gizmo_step_m))
|
||||
size_menu.select(UIScale.step_index())
|
||||
sym_menu.select(int(main.get("sym_mode")))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ var _pending_high := false # Quell-Wahl des laufenden Widerspruchs-Dialogs
|
|||
|
||||
|
||||
static func spec() -> Dictionary:
|
||||
return {"titel": "Symmetrie", "gruppe": "rechts", "order": 75}
|
||||
return {"titel": "Symmetrie", "gruppe": "map", "order": 30}
|
||||
|
||||
|
||||
func panel_title() -> String:
|
||||
|
|
@ -108,6 +108,14 @@ func _refresh_half_option() -> void:
|
|||
half_option.selected = 0
|
||||
|
||||
|
||||
## Vom Einstellungen-Fenster gerufen, wenn der Modus wechselt.
|
||||
func refresh_mode() -> void:
|
||||
if mode_label == null or half_option == null:
|
||||
return
|
||||
_refresh_half_option()
|
||||
_refresh_mode_line()
|
||||
|
||||
|
||||
func _on_half_selected(_index: int) -> void:
|
||||
_refresh_mode_line()
|
||||
|
||||
|
|
@ -121,7 +129,7 @@ func _refresh_mode_line() -> void:
|
|||
func _on_apply_pressed() -> void:
|
||||
var mode := _current_mode()
|
||||
if mode == SymmetryCore.Mode.OFF:
|
||||
_say("Der Symmetrie-Modus ist Aus — oben in der Leiste erst Punkt oder eine Achse wählen.")
|
||||
_say("Der Symmetrie-Modus ist Aus — erst in den Einstellungen (⚙) Punkt oder eine Achse wählen.")
|
||||
return
|
||||
if main.doc.is_empty():
|
||||
_say("Keine Karte offen — erst eine Map anlegen oder laden.", true)
|
||||
|
|
@ -209,7 +217,7 @@ func _apply(quelle_hoch: bool) -> void:
|
|||
func _on_check_pressed() -> void:
|
||||
var mode := _current_mode()
|
||||
if mode == SymmetryCore.Mode.OFF:
|
||||
_say("Der Symmetrie-Modus ist Aus — erst Punkt oder eine Achse wählen, dann prüfen.")
|
||||
_say("Der Symmetrie-Modus ist Aus — erst in den Einstellungen (⚙) Punkt oder eine Achse wählen, dann prüfen.")
|
||||
return
|
||||
_refresh_mode_line()
|
||||
var rep := SymmetryOps.check(main.doc, mode)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ var info_hint: Label
|
|||
|
||||
|
||||
static func spec() -> Dictionary:
|
||||
return {"titel": "Test-Start", "gruppe": "rechts", "order": 60}
|
||||
return {"titel": "Test-Start", "gruppe": "map", "order": 20}
|
||||
|
||||
|
||||
func panel_title() -> String:
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ func panel_title() -> String:
|
|||
|
||||
var undo_btn: Button
|
||||
var redo_btn: Button
|
||||
var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014)
|
||||
var contours_btn: Button # „≈ Höhenlinien“ (Karte t_3ebe4086)
|
||||
var sym_menu: MenuButton
|
||||
var size_menu: MenuButton # „UI-Größe“ (Karte t_297221b5)
|
||||
var settings_btn: Button # „⚙ Einstellungen“ öffnet das Einstellungen-Fenster
|
||||
var title_label: Label
|
||||
var file_label: Label
|
||||
|
||||
|
|
@ -85,45 +82,6 @@ func build() -> void:
|
|||
redo_btn.pressed.connect(main.call.bind("_do_redo"))
|
||||
add_child(redo_btn)
|
||||
|
||||
# Maßstabsgitter-Toggle (Karte t_7ac70014): über sichtbarem Terrain ist das
|
||||
# Gitter standardmäßig AUS (das Terrain ist die Bodendarstellung); wer das
|
||||
# 100-m-Raster als Planungshilfe will, schaltet es hier ohne Rebuild zu.
|
||||
grid_btn = Button.new()
|
||||
grid_btn.text = "⊞ Gitter"
|
||||
grid_btn.toggle_mode = true
|
||||
grid_btn.button_pressed = false
|
||||
grid_btn.custom_minimum_size = UIScale.v2(120, 40)
|
||||
grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus."
|
||||
grid_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_toggled", pressed))
|
||||
add_child(grid_btn)
|
||||
|
||||
# Höhenlinien-Button (Karte t_3ebe4086, Kopplung t_89c0ec95): drücken =
|
||||
# LINIEN FIXIEREN (immer sichtbar, in jedem Werkzeug). Ohne Fix sind die
|
||||
# Linien NUR im Terrain-Werkzeug (F1) sichtbar; auf flachen Maps (< 2 m
|
||||
# Gesamtrelief) schaltet das Terrain-Mesh zusätzlich selbst auf aus.
|
||||
contours_btn = Button.new()
|
||||
contours_btn.text = "≈ Höhenlinien"
|
||||
contours_btn.toggle_mode = true
|
||||
contours_btn.button_pressed = false
|
||||
contours_btn.custom_minimum_size = UIScale.v2(150, 40)
|
||||
contours_btn.tooltip_text = "Höhenlinien (1 m, alle 5 m kräftiger) FIXIERT an/aus. Ohne Fixierung erscheinen sie nur im Terrain-Werkzeug (F1). Taste L schaltet ebenfalls. Bei flachem Terrain (< 2 m) sind sie automatisch aus."
|
||||
contours_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_contours_toggled", pressed))
|
||||
add_child(contours_btn)
|
||||
|
||||
# Symmetrie-Schalter (Karte t_f5935023, M1): Aus / Punkt / Achse X / Achse Z.
|
||||
# Standard = Punktspiegelung (Sly 20.10.). M1 setzt nur Modus + Anzeige;
|
||||
# das gespiegelte Bauen selbst kommt mit M2–M4.
|
||||
sym_menu = MenuButton.new()
|
||||
sym_menu.text = "Symmetrie: Punkt"
|
||||
sym_menu.flat = false
|
||||
sym_menu.custom_minimum_size = UIScale.v2(160, 40)
|
||||
sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt."
|
||||
var sym_popup := sym_menu.get_popup()
|
||||
for i in SymmetryCore.MODE_LABELS.size():
|
||||
sym_popup.add_check_item(SymmetryCore.MODE_LABELS[i], i)
|
||||
sym_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_sym_mode_selected", index))
|
||||
add_child(sym_menu)
|
||||
|
||||
# Toolbar-Aktionen DATENGETRIEBEN aus der Registry (Operator-Vorgabe
|
||||
# 20.10.): Karte prüfen, ▶ Karte testen — künftige Karten melden sich
|
||||
# hier an, ohne main.gd anzufassen. Die Buttons bekommen den Registry-Namen
|
||||
|
|
@ -147,21 +105,15 @@ func build() -> void:
|
|||
main.set("test_start_btn", btn)
|
||||
add_child(btn)
|
||||
|
||||
# UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um,
|
||||
# es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt
|
||||
# in user://scopedvr_editor.cfg gespeichert.
|
||||
size_menu = MenuButton.new()
|
||||
size_menu.text = "UI-Größe"
|
||||
size_menu.flat = false
|
||||
size_menu.custom_minimum_size = UIScale.v2(120, 40)
|
||||
size_menu.tooltip_text = "Größe der Oberfläche: Klein (wie bisher), Mittel (1,25-fach), Groß (1,5-fach). Wirkt sofort und bleibt gespeichert."
|
||||
var size_popup := size_menu.get_popup()
|
||||
for i in UIScale.step_count():
|
||||
size_popup.add_check_item(UIScale.step_label(i), i)
|
||||
# Häkchen-Sync macht der KOORDINATOR nach _pull_toolbar_refs (hier ist
|
||||
# main.size_menu noch null — alter Reihenfolge-Bug, Karte t_eaccbfed).
|
||||
size_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
|
||||
add_child(size_menu)
|
||||
# Einstellungen (25.10.): Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe,
|
||||
# Raster-Snap … stehen jetzt in einem eigenen Fenster.
|
||||
settings_btn = Button.new()
|
||||
settings_btn.text = "⚙ Einstellungen"
|
||||
settings_btn.name = "einstellungen_knopf"
|
||||
settings_btn.custom_minimum_size = UIScale.v2(170, 40)
|
||||
settings_btn.tooltip_text = "Gitter, Höhenlinien, Symmetrie, UI-Größe, Raster-Snap und Testlauf-Einstellungen"
|
||||
settings_btn.pressed.connect(func() -> void: main.call("_open_settings"))
|
||||
add_child(settings_btn)
|
||||
|
||||
var help_btn := Button.new()
|
||||
help_btn.text = "❓ Hilfe"
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func _run_tests() -> void:
|
|||
|
||||
func _test_catalog() -> void:
|
||||
print("Katalog")
|
||||
_check(BauKatalog.KINDS.size() == 12, "12 Bauteile im Katalog")
|
||||
_check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog")
|
||||
var in_gruppen := {}
|
||||
for g in BauKatalog.GRUPPEN:
|
||||
for oid in (g["oids"] as Array):
|
||||
|
|
@ -187,11 +187,11 @@ func _test_snap_cells_and_edges() -> void:
|
|||
s = BuildSnap.berechne("bau_fundament", Vector3(-0.3, 0.0, -2.9), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(-1.0, 0.0, -3.0), "Fundament: negative Zelle (−1, −3)")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(1.0, 0.0, 0.0) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0")
|
||||
_check(s["pos"] == Vector3(1.0, 0.0, 0.1) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 10 cm in die Zelle gerückt (bündig)")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(0.0, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90")
|
||||
_check(s["pos"] == Vector3(0.1, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 10 cm in die Zelle gerückt")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 90.0, kein, _flach(0.0))
|
||||
_check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.0), "R dreht die Wand um (180°), bleibt auf der Kante")
|
||||
_check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.1), "R dreht die Wand um (180°), bleibt an ihrer Stelle")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 90.0, kein, _flach(0.0))
|
||||
_check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°")
|
||||
s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0))
|
||||
|
|
@ -200,17 +200,38 @@ func _test_snap_cells_and_edges() -> void:
|
|||
_check(s["pos"] == Vector3(4.0, 0.0, 1.0) and float(s["rot"]) == 0.0, "Treppe: 4×2-Fläche entlang X")
|
||||
s = BuildSnap.berechne("bau_treppe", Vector3(1.2, 0.0, 3.3), 90.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(1.0, 0.0, 4.0) and float(s["rot"]) == 90.0, "Treppe: gedreht 90° → entlang Z")
|
||||
# Alle Fälle: gerasterte Mitten (Wände auf Kanten = ein gerader + ein ungerader Meter)
|
||||
# Alle Fälle: Wandmitte = 10 cm neben einer Kantenmitte (Wand liegt in der Zelle)
|
||||
for k in range(60):
|
||||
var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k)
|
||||
var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
|
||||
var wp: Vector3 = w["pos"]
|
||||
var gerade_x := int(roundf(wp.x)) % 2 == 0
|
||||
var gerade_z := int(roundf(wp.z)) % 2 == 0
|
||||
if not (gerade_x != gerade_z):
|
||||
_check(false, "Wand-Mitte liegt auf Kantenmitte (%s)" % str(wp))
|
||||
var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.1) < 0.001 # X nahe einer geraden Linie
|
||||
var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001
|
||||
var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
|
||||
var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001
|
||||
if not ((ex and oz) or (ez and ox)):
|
||||
_check(false, "Wand-Mitte liegt in der Zelle an der Kante (%s)" % str(wp))
|
||||
return
|
||||
_check(true, "60 Zufallsklicks: Wandmitte liegt immer auf einer Kantenmitte")
|
||||
# und zwar in der Zelle, in der die Maus war (Außenseite bündig auf der Kante)
|
||||
if (ez and absf(wp.z - p.z) > 1.01) or (ex and absf(wp.x - p.x) > 1.01):
|
||||
_check(false, "Wand liegt auf der Maus-Seite (%s zu %s)" % [str(wp), str(p)])
|
||||
return
|
||||
_check(true, "60 Zufallsklicks: Wand liegt immer in der Zelle der Maus, bündig an einer Kante")
|
||||
# Ecke: zwei Wände in einer Zellecke schließen sich ohne Lücke (äußere Ecke voll)
|
||||
var wa := BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0))
|
||||
var wb := BuildSnap.berechne("bau_wand", Vector3(0.3, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
||||
var ra := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wa["pos"], "rot": wa["rot"]})
|
||||
var rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]})
|
||||
_check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt")
|
||||
_check(absf(ra.position.x) < 0.001 and absf(ra.position.y) < 0.001 and absf(rb.position.x) < 0.001 and absf(rb.position.y) < 0.001,
|
||||
"Ecke: beide Wände beginnen bündig an der Außenkante (0,0)")
|
||||
# Große Teile: Zellzahl bestimmt die Rasterung
|
||||
s = BuildSnap.berechne("bau_dach_giebel_4", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(4.0, 0.0, 4.0), "Satteldach 4×4: Mitte auf Zellecke (4, 4)")
|
||||
s = BuildSnap.berechne("bau_dach_pyramide_6", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)")
|
||||
s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(4.0, 0.0, 0.1), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt")
|
||||
# Gelände: höchste Höhe unter der Grundfläche zählt
|
||||
s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0))
|
||||
_check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)")
|
||||
|
|
@ -224,33 +245,47 @@ func _test_snap_levels() -> void:
|
|||
print("Snap: Ebenen statt Klickhöhe")
|
||||
var teile: Array = [
|
||||
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0),
|
||||
_teil(1, "bau_wand", Vector3(1, 0.2, 0), 0.0),
|
||||
_teil(2, "bau_wand", Vector3(2, 0.2, 1), 90.0),
|
||||
_teil(1, "bau_wand", Vector3(1, 0, 0.1), 0.0),
|
||||
_teil(2, "bau_wand", Vector3(1.9, 0, 1), 90.0),
|
||||
]
|
||||
# Klick mitten auf die Seite der Wand (y=1,5): Boden darf NICHT in der Luft landen
|
||||
var s := BuildSnap.berechne("bau_boden", Vector3(0.9, 1.5, 0.6), 0.0, teile, _flach(0.0))
|
||||
_check(absf(float(s["ebene"]) - 0.2) < 0.001, "Boden nach Klick auf Wandseite liegt auf dem Fundament (0,2), nicht bei 1,5")
|
||||
# Klick auf die Oberkante der Wand: nächste Geschoss-Ebene
|
||||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 3.2, 0.1), 0.0, teile, _flach(0.0), true, Vector3(0, -0.5, 1))
|
||||
_check(absf(float(s["ebene"]) - 3.2) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,2")
|
||||
_check(s["pos"] == Vector3(1.0, 3.2, -1.0), "…und in der Zelle auf der Kameraseite (1, −1)")
|
||||
# Wand auf Wand (Stapeln)
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, 0.0), 0.0, teile, _flach(0.0))
|
||||
_check(absf(float(s["ebene"]) - 3.2) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante")
|
||||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 3.0, 0.1), 0.0, teile, _flach(0.0), true, Vector3(0, -0.5, 1))
|
||||
_check(absf(float(s["ebene"]) - 3.0) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,0")
|
||||
_check(s["pos"] == Vector3(1.0, 3.0, 1.0), "…und in der Zelle, in der die Wand steht (1, 1) — die Decke liegt auf der Wand")
|
||||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 1.0, 1.0), 0.0, [_teil(9, "bau_pfeiler", Vector3(2, 0, 2), 0.0)], _flach(0.0), true, Vector3(0, -0.5, 1))
|
||||
_check(s["pos"].z == 1.0, "Treffer auf Nicht-Wand: Zelle der Kameraseite (Pfeiler an der Ecke)")
|
||||
# Wand auf Wand (Stapeln): genau über der Wand darunter
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.0, 0.1), 0.0, teile, _flach(0.0), true)
|
||||
_check(absf(float(s["ebene"]) - 3.0) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante (3,0)")
|
||||
_check(s["pos"] == Vector3(1.0, 3.0, 0.1), "…und exakt über der Wand darunter (gleiche Lage)")
|
||||
# Test-Befund 25.10.: obere Wände sitzen gleich hoch, egal ob auf der Nachbarwand
|
||||
# eine Decke liegt oder nicht (Decke liegt AUF der Wand, Wände zählen ab Deckenunterkante).
|
||||
var mit_decke: Array = teile.duplicate()
|
||||
mit_decke.append(_teil(3, "bau_boden", Vector3(1, 3.0, -1), 0.0)) # Decke über der Wand vorn (Zelle −1)
|
||||
var ohne := BuildSnap.berechne("bau_wand", Vector3(1.0, 3.0, 0.1), 0.0, mit_decke, _flach(0.0), true)
|
||||
var mit := BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, -0.1), 0.0, mit_decke, _flach(0.0), true)
|
||||
_check(absf(float(ohne["ebene"]) - 3.0) < 0.001, "Obergeschoss-Wand ohne Decke: Höhe 3,0 (%.2f)" % float(ohne["ebene"]))
|
||||
_check(absf(float(mit["ebene"]) - 3.0) < 0.001, "Obergeschoss-Wand AUF der Decke: ebenfalls 3,0 (%.2f)" % float(mit["ebene"]))
|
||||
# Ein Dach ist kein Träger: nichts darf auf der Dachspitze landen
|
||||
teile.append(_teil(3, "bau_dach_giebel", Vector3(1, 3.2, 1), 0.0))
|
||||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.2, 1.0), 0.0, teile, _flach(0.0))
|
||||
_check(float(s["ebene"]) <= 3.2 + 0.001, "Dachspitze (4,2) ist keine Ebene (Ebene %.2f)" % float(s["ebene"]))
|
||||
teile.append(_teil(4, "bau_dach_giebel", Vector3(1, 3.0, 1), 0.0))
|
||||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.0, 1.0), 0.0, teile, _flach(0.0))
|
||||
_check(float(s["ebene"]) <= 3.0 + 0.001, "Dachspitze (4,0) ist keine Ebene (Ebene %.2f)" % float(s["ebene"]))
|
||||
|
||||
|
||||
func _test_replace_and_stack() -> void:
|
||||
print("Ersetzen / doppelt")
|
||||
var teile: Array = [
|
||||
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0),
|
||||
_teil(1, "bau_wand", Vector3(1, 0.2, 0), 0.0),
|
||||
_teil(1, "bau_wand", Vector3(1, 0, 0.1), 0.0),
|
||||
]
|
||||
var s := BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0))
|
||||
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür in vorhandene Wand → ersetzt Teil 1")
|
||||
# Klick direkt auf die Wand (Objekt-Treffer) übernimmt ihre Lage, auch von der anderen Seite
|
||||
s = BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.0), 0.0, teile, _flach(0.0), true, Vector3(0, 0, 1))
|
||||
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür: Klick auf die Außenseite der Wand ersetzt dieselbe Wand")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0))
|
||||
_check(not bool(s["ok"]) and str(s["grund"]).contains("schon"), "gleiche Wand nochmal → abgelehnt mit Erklärung")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 90.0, teile, _flach(0.0))
|
||||
|
|
@ -283,13 +318,13 @@ func _test_ops_group_undo() -> void:
|
|||
_check(BauOps.teile_zahl(doc) == 1, "teile_zahl = 1")
|
||||
# Wand setzen, dann Tür ersetzt dieselbe Stelle (Index bleibt)
|
||||
var teile := BuildSnap.sammle_teile(doc)
|
||||
var ws := BuildSnap.berechne("bau_wand", Vector3(1, 0.2, 0.05), 0.0, teile, _flach(0.0))
|
||||
var ws := BuildSnap.berechne("bau_wand", Vector3(1, 0.2, 0.15), 0.0, teile, _flach(0.0))
|
||||
var before2 := hist.snapshot(["objects", "group_names"])
|
||||
var wr := BauOps.setze_teil(doc, "bau_wand", ws, gid, SymmetryCore.Mode.OFF)
|
||||
hist.commit("Wand bauen", before2, hist.snapshot(["objects", "group_names"]))
|
||||
var widx := int(wr["index"])
|
||||
teile = BuildSnap.sammle_teile(doc)
|
||||
var ts := BuildSnap.berechne("bau_wand_tuer", Vector3(1, 1.5, 0.05), 0.0, teile, _flach(0.0))
|
||||
var ts := BuildSnap.berechne("bau_wand_tuer", Vector3(1, 1.5, 0.15), 0.0, teile, _flach(0.0))
|
||||
_check(int(ts["ersetze"]) == widx, "Tür-Klick findet die Wand")
|
||||
var before3 := hist.snapshot(["objects", "group_names"])
|
||||
var tr := BauOps.setze_teil(doc, "bau_wand_tuer", ts, gid, SymmetryCore.Mode.OFF)
|
||||
|
|
@ -314,7 +349,7 @@ func _test_ops_group_undo() -> void:
|
|||
# Haus komplett
|
||||
var d2 := _neue_doc()
|
||||
var gid2 := BauOps.haus_demo(d2, null, Vector3(4.0, 0.0, 4.0))
|
||||
_check(BauOps.teile_zahl(d2) >= 16, "Demo-Haus: %d Teile über die echte Snap-Logik" % BauOps.teile_zahl(d2))
|
||||
_check(BauOps.teile_zahl(d2) >= 13, "Demo-Haus: %d Teile über die echte Snap-Logik" % BauOps.teile_zahl(d2))
|
||||
_check(BauOps.gebaeude_von(d2, 0).size() == BauOps.teile_zahl(d2), "alle Teile des Hauses in einer Gruppe (%d)" % gid2)
|
||||
var doppelt := {}
|
||||
var hat_doppelte := false
|
||||
|
|
|
|||
99
tests/test_drag_bibliothek.gd
Normal file
99
tests/test_drag_bibliothek.gd
Normal 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)
|
||||
|
|
@ -113,9 +113,8 @@ func _t1_einzelauswahl() -> void:
|
|||
_check(str(insp.widget("rot_y").edit.text).contains("0.0"), "T1 rot_y zeigt 0.0")
|
||||
# Auswahl-Beweis per Klick im Viewport-Kontext: Panel ist sichtbar.
|
||||
_check(insp.get("_grid").visible, "T1 Felder sichtbar bei Auswahl")
|
||||
# Fix Vorbestand: das alte „Auswahl“-Feld füllt sich jetzt auch bei
|
||||
# select_object (Vorher stand dort dauerhaft „Nichts ausgewählt.“).
|
||||
_check(str(main.get("sel_label").text).contains("Fass"), "T1 altes Auswahl-Feld füllt bei 3D-Klick-Weg")
|
||||
# Das alte „Auswahl“-Feld ist weg (25.10.): der Inspektor ist die einzige Anzeige.
|
||||
_check(main.get("sel_label") == null, "T1 kein altes Auswahl-Feld mehr (Tab Auswahl entfernt)")
|
||||
|
||||
|
||||
func _t2_tippen() -> void:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ func _init() -> void:
|
|||
|
||||
await _t1_baum_fuellt_sich()
|
||||
await _t2_klick_setzt_auswahl()
|
||||
await _t2b_echter_klick()
|
||||
await _t3_auswahl_3d_markiert_baum()
|
||||
await _t4_mehrfach_gespiegelt()
|
||||
await _t5_gruppen_ebene()
|
||||
|
|
@ -118,6 +119,44 @@ func _t2_klick_setzt_auswahl() -> void:
|
|||
_check(int(sel) == 1, "T2 Klick im Baum setzt Auswahl in 3D (Index 1 — hat %s)" % str(sel))
|
||||
|
||||
|
||||
## FIX 25.10.: Im Modus SELECT_MULTI feuert Godot nie item_selected — der Test
|
||||
## oben simulierte einen Weg, den es in der Praxis nicht gab. Hier ein ECHTER
|
||||
## Mausklick (Eingabe-Ereignis) auf eine Zeile.
|
||||
func _t2b_echter_klick() -> void:
|
||||
var tree: Tree = tree_mod.get("content_tree")
|
||||
main.interaction.clear_selection()
|
||||
root.size = Vector2i(1600, 900)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var item0: TreeItem = null
|
||||
for kat in _find_child(tree.get_root(), "Objekte (3)").get_children():
|
||||
for oi in _obj_items_under(kat):
|
||||
if int(oi.get_meta("obj_index")) == 2:
|
||||
item0 = oi
|
||||
_check(item0 != null, "T2b Zeile für Objekt-Index 2 gefunden")
|
||||
if item0 == null:
|
||||
return
|
||||
tree.scroll_to_item(item0)
|
||||
await process_frame
|
||||
var r := tree.get_item_area_rect(item0, 0)
|
||||
var pos := tree.get_global_position() + r.position + Vector2(r.size.x * 0.5, r.size.y * 0.5)
|
||||
_check(tree.get_global_rect().has_point(pos), "T2b Zeile liegt im sichtbaren Baum-Rechteck")
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.button_index = MOUSE_BUTTON_LEFT
|
||||
ev.position = pos
|
||||
ev.global_position = pos
|
||||
ev.pressed = true
|
||||
root.push_input(ev)
|
||||
await process_frame
|
||||
var up := ev.duplicate()
|
||||
up.pressed = false
|
||||
root.push_input(up)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_check(int(main.interaction.get("selected_index")) == 2,
|
||||
"T2b ECHTER Klick auf die Listenzeile wählt das Objekt in der Szene (Index 2 — hat %s)" % str(main.interaction.get("selected_index")))
|
||||
|
||||
|
||||
func _t3_auswahl_3d_markiert_baum() -> void:
|
||||
var tree: Tree = tree_mod.get("content_tree")
|
||||
var imap: Dictionary = tree_mod.get("_obj_index_map")
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ func _check(cond: bool, what: String) -> void:
|
|||
print(" FAIL: ", what)
|
||||
|
||||
|
||||
func _count_named(node: Node, wanted: String) -> int:
|
||||
var n := 1 if str(node.name) == wanted and not (node is BaseButton) else 0
|
||||
for c in node.get_children():
|
||||
n += _count_named(c, wanted)
|
||||
return n
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
print("Testgruppe: Layout aus der Registry")
|
||||
var scene: PackedScene = load("res://scenes/main.tscn")
|
||||
|
|
@ -67,6 +74,11 @@ func _init() -> void:
|
|||
_check(ids.size() == registry_script.PANELS.size(), "Registry: IDs eindeutig (keine Duplikate in der Tabelle)")
|
||||
_check(not ids.is_empty(), "Registry: mindestens ein Bereich angemeldet")
|
||||
|
||||
var embedded_ids: Array = []
|
||||
for entry in registry_script.PANELS:
|
||||
if str(entry["gruppe"]) in ["map", "dialog"]:
|
||||
embedded_ids.append(str(entry["id"]))
|
||||
|
||||
# --- 2. Jeder Bereich GENAU EINMAL im Baum, als Tab mit Titel --------------
|
||||
var tabs_by_name: Dictionary = {} # gruppe → TabContainer
|
||||
var stack: Array = [main]
|
||||
|
|
@ -91,6 +103,12 @@ func _init() -> void:
|
|||
_check(tab.get_tab_title(idx) == title,
|
||||
"Tab '%s' trägt Registry-Titel '%s'" % [cid, title])
|
||||
for wanted in ids:
|
||||
if wanted in embedded_ids:
|
||||
# Map-Abschnitte und Einstellungen-Fenster sind keine Tabs (25.10.):
|
||||
# GENAU EINMAL im Baum, Registry-Name = Node-Name.
|
||||
var treffer := _count_named(main, wanted)
|
||||
_check(treffer == 1, "Bereich '%s' (eingebettet) GENAU EINMAL im Baum (%d)" % [wanted, treffer])
|
||||
continue
|
||||
_check(tabs_by_name.get(wanted) != null, "Bereich '%s' aus der Registry im Baum" % wanted)
|
||||
|
||||
# --- 3. Tab-Gruppen --------------------------------------------------------
|
||||
|
|
@ -108,6 +126,7 @@ func _init() -> void:
|
|||
for entry in registry_script.PANELS:
|
||||
if str(entry["gruppe"]) == "rechts":
|
||||
expected_right += 1
|
||||
_check(expected_right == 5, "rechts: genau 5 Tabs (Inspektor, Map, Terrain, Malen, Bauen)")
|
||||
_check(right.get_child_count() == expected_right,
|
||||
"rechts: %d Tabs aus der Registry — hat %d" % [expected_right, right.get_child_count()])
|
||||
_check(right.get_tab_count() == right.get_child_count(),
|
||||
|
|
@ -125,7 +144,7 @@ func _init() -> void:
|
|||
btab = gc
|
||||
_check(btab != null, "unten: TabContainer für Assets gefunden")
|
||||
if btab != null:
|
||||
_check(btab.get_child_count() == 2, "unten: 2 Tabs (Bibliothek/Werkzeuge) — hat %d" % btab.get_child_count())
|
||||
_check(btab.get_child_count() == 1, "unten: 1 Tab (Objekt-Bibliothek) — hat %d" % btab.get_child_count())
|
||||
|
||||
# --- 4. Assets-Bereich unten vollständig -----------------------------------
|
||||
var assets: Control = tabs_by_name.get("assets")
|
||||
|
|
@ -136,8 +155,12 @@ func _init() -> void:
|
|||
"Bibliothek: „Alle auf/zu“ vorhanden")
|
||||
var lib_box: Node = assets.get("library_box")
|
||||
_check(lib_box != null and lib_box.get_child_count() > 0, "Bibliothek: Kachel-/Gruppen-Liste gefüllt")
|
||||
var tools: Control = tabs_by_name.get("werkzeuge")
|
||||
_check(tools != null and tools.get("grid_check") != null, "Objekt-Werkzeuge: Raster-Snap erhalten (nichts verschwindet)")
|
||||
_check(tabs_by_name.get("werkzeuge") == null, "Tab „Objekt-Werkzeuge“ ist weg")
|
||||
var settings: Control = main.find_child("einstellungen", true, false)
|
||||
_check(settings != null and settings.get("raster_snap_check") != null, "Raster-Snap wohnt im Einstellungen-Fenster")
|
||||
_check(settings != null and settings.get("grid_btn") != null and settings.get("contours_btn") != null
|
||||
and settings.get("size_menu") != null and settings.get("sym_menu") != null,
|
||||
"Einstellungen: Gitter, Höhenlinien, UI-Größe, Symmetrie-Modus")
|
||||
|
||||
# --- 5. Theme als einzige Farbquelle ----------------------------------------
|
||||
var theme_res: Theme = main.get("theme")
|
||||
|
|
|
|||
|
|
@ -128,10 +128,9 @@ func _test_real_ui_scaling() -> void:
|
|||
_check(int(main.theme.default_font_size) == 16, "Klein: Theme-Schrift 16")
|
||||
_check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Klein: Undo-Knopf 140×40")
|
||||
_check(main.redo_btn.custom_minimum_size == Vector2(150, 40), "Klein: Redo-Knopf 150×40")
|
||||
_check(main.size_menu != null and main.size_menu.text == "UI-Größe", "UI-Größe-Menü in der Top-Bar")
|
||||
var sp: PopupMenu = main.size_menu.get_popup()
|
||||
_check(sp.item_count == 3, "Menü hat 3 Einträge")
|
||||
_check(sp.is_item_checked(0), "Menü-Häkchen auf aktueller Stufe (Klein)")
|
||||
_check(main.size_menu != null and main.size_menu is OptionButton, "UI-Größe-Auswahl im Einstellungen-Fenster")
|
||||
_check(main.size_menu.item_count == 3, "Auswahl hat 3 Stufen")
|
||||
_check(main.size_menu.selected == 0, "Auswahl steht auf aktueller Stufe (Klein)")
|
||||
|
||||
# Rebuild-Pfad (derselbe wie beim Menü-Klick) → Mittel:
|
||||
UIScale.set_step_index(1)
|
||||
|
|
@ -146,7 +145,7 @@ func _test_real_ui_scaling() -> void:
|
|||
_check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
|
||||
var thumb := _find_thumb_rect(160)
|
||||
_check(thumb != null and thumb.custom_minimum_size.x == 160, "Mittel: Thumbnail 128 → 160")
|
||||
_check(main.size_menu.get_popup().is_item_checked(1), "Menü-Häkchen wandert auf Mittel (neues Popup)")
|
||||
_check(main.size_menu.selected == 1, "Auswahl wandert auf Mittel (neues Fenster)")
|
||||
|
||||
# Groß:
|
||||
main._on_ui_size_selected(2)
|
||||
|
|
@ -161,7 +160,7 @@ func _test_real_ui_scaling() -> void:
|
|||
main._on_ui_size_selected(0)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_check(main.size_menu.get_popup().is_item_checked(0), "Menü-Häkchen wieder auf Klein")
|
||||
_check(main.size_menu.selected == 0, "Auswahl wieder auf Klein")
|
||||
_check(int(main.theme.default_font_size) == 16, "Zurück auf Klein: Schrift wieder 16")
|
||||
_check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Zurück auf Klein: Undo-Knopf 140×40")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue