diff --git a/docs/bauen.md b/docs/bauen.md index bd13f7a..5a41668 100644 --- a/docs/bauen.md +++ b/docs/bauen.md @@ -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. diff --git a/library/kollisions/bau_dach_giebel_4.json b/library/kollisions/bau_dach_giebel_4.json new file mode 100644 index 0000000..3c093ad --- /dev/null +++ b/library/kollisions/bau_dach_giebel_4.json @@ -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]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_giebel_6.json b/library/kollisions/bau_dach_giebel_6.json new file mode 100644 index 0000000..df0415e --- /dev/null +++ b/library/kollisions/bau_dach_giebel_6.json @@ -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]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_pyramide_4.json b/library/kollisions/bau_dach_pyramide_4.json new file mode 100644 index 0000000..a17482a --- /dev/null +++ b/library/kollisions/bau_dach_pyramide_4.json @@ -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]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_pyramide_6.json b/library/kollisions/bau_dach_pyramide_6.json new file mode 100644 index 0000000..98e0f42 --- /dev/null +++ b/library/kollisions/bau_dach_pyramide_6.json @@ -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]]} \ No newline at end of file diff --git a/library/kollisions/bau_giebelwand_4.json b/library/kollisions/bau_giebelwand_4.json new file mode 100644 index 0000000..cf71c33 --- /dev/null +++ b/library/kollisions/bau_giebelwand_4.json @@ -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]]} \ No newline at end of file diff --git a/library/kollisions/bau_giebelwand_6.json b/library/kollisions/bau_giebelwand_6.json new file mode 100644 index 0000000..f1b7980 --- /dev/null +++ b/library/kollisions/bau_giebelwand_6.json @@ -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]]} \ No newline at end of file diff --git a/library/objects.json b/library/objects.json index 5ac4734..1d7cd4f 100644 --- a/library/objects.json +++ b/library/objects.json @@ -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": { diff --git a/src/editor/bau_katalog.gd b/src/editor/bau_katalog.gd index 9490c55..c960e1c 100644 --- a/src/editor/bau_katalog.gd +++ b/src/editor/bau_katalog.gd @@ -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)) diff --git a/src/editor/bau_ops.gd b/src/editor/bau_ops.gd index 02e65ac..3a86fe8 100644 --- a/src/editor/bau_ops.gd +++ b/src/editor/bau_ops.gd @@ -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 diff --git a/src/editor/build_snap.gd b/src/editor/build_snap.gd index 422e631..e9d802e 100644 --- a/src/editor/build_snap.gd +++ b/src/editor/build_snap.gd @@ -43,18 +43,31 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, var q := int(roundf(dreh / 90.0)) var viertel := ((q % 4) + 4) % 4 var p := Vector2(hit.x, hit.z) - # Treffer auf einem Objekt: Zelle auf der Kameraseite wählen (Klick auf die - # Wandoberkante liegt genau auf der Zellgrenze). - if hit_ist_objekt and art != "kante" and kamera_dir != Vector3.ZERO: - var d2 := Vector2(kamera_dir.x, kamera_dir.z) - if d2.length() > 0.01: - p -= d2.normalized() * 0.2 + # Treffer auf einem Objekt: Wände liegen GANZ in einer Zelle — die Quer-Achse + # kommt dann von der Wand (Decke/Dach landet in der Zelle der Wand). Bei anderen + # Objekten die Zelle auf der Kameraseite wählen (Klick liegt auf einer Grenze). + var wand_getroffen := {} + if hit_ist_objekt: + wand_getroffen = _wand_unter_klick(teile, p, hit.y) + if hit_ist_objekt and art != "kante": + if not wand_getroffen.is_empty(): + var wp: Vector3 = wand_getroffen["pos"] + if absf(sin(deg_to_rad(float(wand_getroffen["rot"])))) > 0.5: + p.x = wp.x + else: + p.y = wp.z + elif kamera_dir != Vector3.ZERO: + var d2 := Vector2(kamera_dir.x, kamera_dir.z) + if d2.length() > 0.01: + p -= d2.normalized() * 0.2 var mitte := Vector2.ZERO var rot := 0.0 var r := BauKatalog.RASTER_M match art: "zelle": - mitte = Vector2(floorf(p.x / r) * r + r * 0.5, floorf(p.y / r) * r + r * 0.5) + # Teile über mehrere Zellen (große Dächer): gerade Zellzahl → Mitte auf + # einer Zellecke, ungerade → Mitte einer Zelle. + mitte = Vector2(_raster_mitte(p.x, gr.x), _raster_mitte(p.y, gr.z)) rot = viertel * 90.0 "ecke": mitte = Vector2(roundf(p.x / r) * r, roundf(p.y / r) * r) @@ -65,16 +78,31 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, else: mitte = Vector2(floorf(p.x / r) * r + r * 0.5, roundf(p.y / r) * r) _: # "kante" - var fx := p.x / r - var fz := p.y / r - var h_mitte := Vector2((floorf(fx) + 0.5) * r, roundf(fz) * r) # Kante entlang X - var v_mitte := Vector2(roundf(fx) * r, (floorf(fz) + 0.5) * r) # Kante entlang Z - if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte): - mitte = h_mitte - rot = 0.0 + var vorhanden := wand_getroffen + if not vorhanden.is_empty(): + # Klick auf eine vorhandene Wand (Seite = ersetzen, Oberkante = stapeln): + # GENAU ihre Lage übernehmen, nicht neu aus der Klickstelle raten. + var tp: Vector3 = vorhanden["pos"] + mitte = Vector2(tp.x, tp.z) + rot = 90.0 if absf(sin(deg_to_rad(float(vorhanden["rot"])))) > 0.5 else 0.0 else: - mitte = v_mitte - rot = 90.0 + # Wand liegt GANZ IN der Zelle, in der der Mauszeiger ist: Außenseite + # bündig mit der Zellkante (Boden/Fundament), nicht halb in der Luft. + # Dadurch schließen sich auch Ecken ohne Lücke (Test-Befund 25.10.). + var laenge := gr.x + var halbe_dicke := gr.z * 0.5 + var fx := p.x / r + var fz := p.y / r + var h_mitte := Vector2(_raster_mitte(p.x, laenge), roundf(fz) * r) # Kante entlang X + var v_mitte := Vector2(roundf(fx) * r, _raster_mitte(p.y, laenge)) # Kante entlang Z + if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte): + mitte = h_mitte + mitte.y += halbe_dicke if p.y >= h_mitte.y else -halbe_dicke + rot = 0.0 + else: + mitte = v_mitte + mitte.x += halbe_dicke if p.x >= v_mitte.x else -halbe_dicke + rot = 90.0 rot += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile) rot = fposmod(rot, 360.0) var quer := absf(sin(deg_to_rad(rot))) > 0.5 # 90°/270°: X- und Z-Ausdehnung getauscht @@ -95,7 +123,15 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, if not tr.intersects(kern): continue var top: float = (t["pos"] as Vector3).y + BauKatalog.groesse(troid).y - if BauKatalog.ist_traeger(troid): + if not BauKatalog.ist_traeger(troid): + continue + if (art == "kante" or art == "ecke") and BauKatalog.ist_platte(troid): + # Wände/Pfeiler zählen von der UNTERKANTE einer Platte (Boden/Decke/ + # Fundament): jedes Geschoss liegt dadurch auf Vielfachen der Wandhöhe — + # mit oder ohne Decke darunter sitzen obere Wände gleich hoch + # (Test-Befund 25.10.). Die Platte selbst liegt auf der Wandoberkante. + ebenen.append((t["pos"] as Vector3).y) + else: ebenen.append(top) var ebene := _waehle_ebene(ebenen, hit.y) @@ -123,6 +159,39 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, "ebene": ebene, "grund": ""} +## Mitte eines Teils der Länge `laenge` entlang einer Achse: gerade Zellzahl → +## auf der nächsten Zellgrenze, ungerade → Mitte der Zelle. +static func _raster_mitte(wert: float, laenge: float) -> float: + var r := BauKatalog.RASTER_M + var zellen := maxi(1, int(roundf(laenge / r))) + if zellen % 2 == 0: + return roundf(wert / r) * r + return floorf(wert / r) * r + r * 0.5 + + +## Vorhandene Wand, auf die der Klick trifft (Seite oder Oberkante): Rückgabe das +## Teil-Wörterbuch oder {}. `p` = Klickpunkt in XZ, `y` = Klickhöhe. +static func _wand_unter_klick(teile: Array, p: Vector2, y: float) -> Dictionary: + var bester := {} + var bester_d := INF + for t in teile: + var troid := str(t["oid"]) + if BauKatalog.snap_art(troid) != "kante": + continue + var tr := teil_rect(t).grow(0.06) + if not tr.has_point(p): + continue + var unten: float = (t["pos"] as Vector3).y + var oben := unten + BauKatalog.groesse(troid).y + if y < unten - 0.1 or y > oben + 0.1: + continue + var d := p.distance_squared_to(tr.get_center()) + if d < bester_d: + bester_d = d + bester = t + return bester + + ## Höchste Ebene ≤ hit_y + TOL; gibt es keine (Klick tief unter allen), die tiefste. static func _waehle_ebene(ebenen: Array, hit_y: float) -> float: var best := -INF diff --git a/src/ui/main.gd b/src/ui/main.gd index 8ae6d5c..50e43c4 100644 --- a/src/ui/main.gd +++ b/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 ---- diff --git a/src/ui/panels/assets_panel.gd b/src/ui/panels/assets_panel.gd index ed4cfe5..ad9fc52 100644 --- a/src/ui/panels/assets_panel.gd +++ b/src/ui/panels/assets_panel.gd @@ -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 diff --git a/src/ui/panels/bauen_panel.gd b/src/ui/panels/bauen_panel.gd index 0fb415a..c485d1f 100644 --- a/src/ui/panels/bauen_panel.gd +++ b/src/ui/panels/bauen_panel.gd @@ -1,18 +1,11 @@ extends "res://src/ui/panels/editor_panel.gd" -## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe) plus ein -## paar passende Module aus dem Military-Desert-Pack. Ein Klick auf ein Teil startet +## Bauen-Tab: Bauteile für eigene Gebäude (Fundament, Wände, Dach, Treppe). Fertige +## Pack-Objekte (Container, Hangar-Tore, Sperren …) gehören in die Objekt-Bibliothek +## unten, NICHT hierher. Ein Klick auf ein Teil startet ## das Platzieren (Vorschau rastet an Zelle/Kante/Ebene, siehe build_snap.gd); jedes ## gesetzte Teil ist ein normales Objekt und gehört zum laufenden Gebäude (Gruppe). ## Gebäude kopieren/löschen/auswählen läuft über die Knöpfe unten — alles Undo-fähig. -## Pack-Module, die zum Gebäudebau passen (IDs stabil, siehe Bibliothek). -const PACK_MODULE := [ - {"label": "Container", "oids": ["container_001", "container_002", "container_003", "container_004", - "container_005", "container_006", "container_007", "container_008"]}, - {"label": "Hangar-Tore", "oids": ["hangar_door_001", "hangar_door_002", "hangar_door_003"]}, - {"label": "Betonsperren", "oids": ["barrier_001", "barrier_002", "barrier_003", "barrier_004"]}, -] - var bau_buttons: Dictionary = {} # oid → Button var info_label: Label var _group: ButtonGroup @@ -32,15 +25,7 @@ func build() -> void: add_child(_muted_label("Wähle ein Teil und klicke in die Karte. Wände rasten an die nächste Zellkante, Böden und Dächer in die Zellmitte (Raster 2 m). Klick auf die Oberkante einer Wand stapelt das nächste Geschoss, Klick auf die Seite ersetzt das Teil (z. B. Wand → Tür). R dreht, Esc beendet.")) for gruppe in BauKatalog.GRUPPEN: add_child(_field_label(str(gruppe["label"]))) - add_child(_knopf_raster(gruppe["oids"] as Array, true)) - var pack := _pack_gruppen() - if not pack.is_empty(): - add_child(_separator()) - add_child(_header_label("Pack-Module")) - add_child(_muted_label("Fertige Teile aus dem Military-Desert-Pack — normale Objekte, werden auf den Boden gesetzt.")) - for pg in pack: - add_child(_field_label(str(pg["label"]))) - add_child(_knopf_raster(pg["oids"] as Array, false)) + add_child(_knopf_raster(gruppe["oids"] as Array)) add_child(_separator()) add_child(_header_label("Gebäude")) info_label = _muted_label("") @@ -54,22 +39,10 @@ func build() -> void: add_child(_aktion("🗑 Gebäude löschen", "Löscht alle Teile des Gebäudes (Strg+Z holt es zurück).", func() -> void: main.call("_bau_gebaeude_loeschen"))) refresh_info() + wrap_in_scroll() -func _pack_gruppen() -> Array: - var lib: Dictionary = main.manifest.get("objects", {}) if main != null else {} - var out: Array = [] - for pg in PACK_MODULE: - var oids: Array = [] - for oid in (pg["oids"] as Array): - if lib.has(oid): - oids.append(oid) - if not oids.is_empty(): - out.append({"label": pg["label"], "oids": oids}) - return out - - -func _knopf_raster(oids: Array, bau: bool) -> GridContainer: +func _knopf_raster(oids: Array) -> GridContainer: var grid := GridContainer.new() grid.columns = 2 grid.add_theme_constant_override("h_separation", UIScale.px(8)) @@ -82,14 +55,9 @@ func _knopf_raster(oids: Array, bau: bool) -> GridContainer: btn.custom_minimum_size = UIScale.v2(148, 44) btn.clip_text = true var poly := int((lib.get(oid, {}) as Dictionary).get("poly", 0)) - if bau: - btn.text = BauKatalog.label(oid) - btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid), - str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly] - else: - var name_de := str((lib.get(oid, {}) as Dictionary).get("name_de", oid)) - btn.text = name_de - btn.tooltip_text = "%s — %d Dreiecke" % [name_de, poly] + btn.text = BauKatalog.label(oid) + btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid), + str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly] btn.pressed.connect(_on_teil_gedrueckt.bind(str(oid), btn)) bau_buttons[str(oid)] = btn grid.add_child(btn) diff --git a/src/ui/panels/check_panel.gd b/src/ui/panels/check_panel.gd index 3a62f83..8d8c0f4 100644 --- a/src/ui/panels/check_panel.gd +++ b/src/ui/panels/check_panel.gd @@ -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 = "" diff --git a/src/ui/panels/editor_panel.gd b/src/ui/panels/editor_panel.gd index 501a8f8..8fe4b70 100644 --- a/src/ui/panels/editor_panel.gd +++ b/src/ui/panels/editor_panel.gd @@ -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 diff --git a/src/ui/panels/map_panel.gd b/src/ui/panels/map_panel.gd index 8a44fef..8e2144b 100644 --- a/src/ui/panels/map_panel.gd +++ b/src/ui/panels/map_panel.gd @@ -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: diff --git a/src/ui/panels/object_tools_panel.gd b/src/ui/panels/object_tools_panel.gd deleted file mode 100644 index 12bd66e..0000000 --- a/src/ui/panels/object_tools_panel.gd +++ /dev/null @@ -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) diff --git a/src/ui/panels/object_tools_panel.gd.uid b/src/ui/panels/object_tools_panel.gd.uid deleted file mode 100644 index dc9120e..0000000 --- a/src/ui/panels/object_tools_panel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cs4xao3sf5y7n diff --git a/src/ui/panels/registry.gd b/src/ui/panels/registry.gd index 5c5222e..0af47da 100644 --- a/src/ui/panels/registry.gd +++ b/src/ui/panels/registry.gd @@ -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 diff --git a/src/ui/panels/scene_tree_panel.gd b/src/ui/panels/scene_tree_panel.gd index 80d5400..070170f 100644 --- a/src/ui/panels/scene_tree_panel.gd +++ b/src/ui/panels/scene_tree_panel.gd @@ -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 diff --git a/src/ui/panels/selection_panel.gd b/src/ui/panels/selection_panel.gd index 1621cda..85512ca 100644 --- a/src/ui/panels/selection_panel.gd +++ b/src/ui/panels/selection_panel.gd @@ -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 diff --git a/src/ui/panels/settings_panel.gd b/src/ui/panels/settings_panel.gd index db51fae..f0af29f 100644 --- a/src/ui/panels/settings_panel.gd +++ b/src/ui/panels/settings_panel.gd @@ -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"))) diff --git a/src/ui/panels/symmetry_panel.gd b/src/ui/panels/symmetry_panel.gd index 773fd7b..dac0b80 100644 --- a/src/ui/panels/symmetry_panel.gd +++ b/src/ui/panels/symmetry_panel.gd @@ -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) diff --git a/src/ui/panels/test_start_panel.gd b/src/ui/panels/test_start_panel.gd index 987d7fa..a158926 100644 --- a/src/ui/panels/test_start_panel.gd +++ b/src/ui/panels/test_start_panel.gd @@ -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: diff --git a/src/ui/panels/toolbar.gd b/src/ui/panels/toolbar.gd index 5529a50..288774a 100644 --- a/src/ui/panels/toolbar.gd +++ b/src/ui/panels/toolbar.gd @@ -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" diff --git a/tests/test_bauen.gd b/tests/test_bauen.gd index ee95e69..68fed9d 100644 --- a/tests/test_bauen.gd +++ b/tests/test_bauen.gd @@ -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 diff --git a/tests/test_drag_bibliothek.gd b/tests/test_drag_bibliothek.gd new file mode 100644 index 0000000..3009cf3 --- /dev/null +++ b/tests/test_drag_bibliothek.gd @@ -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) diff --git a/tests/test_inspector.gd b/tests/test_inspector.gd index c957942..7830ebf 100644 --- a/tests/test_inspector.gd +++ b/tests/test_inspector.gd @@ -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: diff --git a/tests/test_scene_tree.gd b/tests/test_scene_tree.gd index 6219d1a..0e9ecff 100644 --- a/tests/test_scene_tree.gd +++ b/tests/test_scene_tree.gd @@ -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") diff --git a/tests/test_ui_layout.gd b/tests/test_ui_layout.gd index e4bd1bc..2695b05 100644 --- a/tests/test_ui_layout.gd +++ b/tests/test_ui_layout.gd @@ -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") diff --git a/tests/test_ui_scale.gd b/tests/test_ui_scale.gd index dd0ca14..a0cfa13 100644 --- a/tests/test_ui_scale.gd +++ b/tests/test_ui_scale.gd @@ -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")