diff --git a/ADDON_LIZENZ.txt b/ADDON_LIZENZ.txt index cf93022..78febed 100644 --- a/ADDON_LIZENZ.txt +++ b/ADDON_LIZENZ.txt @@ -3,3 +3,14 @@ Quelle: Godot Asset Store (https://store.godotengine.org/asset/michel-savadim/mo Lizenz: CC0 1.0 Universal (siehe addons/modular_builder/LICENSE) — gilt für Code, Icons und Dokumentation. Die Demo-Szene/Demo-Texturen des Originals sind NICHT CC0 und wurden beim Import ENTFERNT (addons/modular_builder/demo fehlt bewusst). + +Abgleich 25.10.2026: Der Code der Runtime-Geometrie (modular_piece_geometry.gd, +modular_building_detail.gd, modular_piece.gd, modular_pillar_support.gd, +modular_roof_support.gd, modular_vertex_simplifier.gd, modular_mesh_baker.gd) ist +zeichengleich zu ModularBuilder-English-0.2.0-2.zip (Ordner addons/modular_builder_en, +dort CC0 1.0, Copyright (c) 2026 Michel Savadim). Der Bauen-Modus des Map-Editors +(src/editor/bau_katalog.gd) nutzt diese Geometrie nur zum Erzeugen der Bauteile +(Wandhöhe 3 m, 2-m-Raster); die Teile selbst sind normale Bibliotheks-Objekte +(docs/bauen.md). Nicht übernommen: Demo-Szene, Demo-Texturen (nicht CC0). +Hinweis der Vorlage: Teile des Codes wurden mit KI-Unterstützung (OpenAI) erstellt — +vom Autor geprüft; CC0 gilt unverändert. diff --git a/docs/bauen.md b/docs/bauen.md new file mode 100644 index 0000000..bd13f7a --- /dev/null +++ b/docs/bauen.md @@ -0,0 +1,62 @@ +# Bauen — eigene Häuser aus Modulteilen + +Stand 25.10.2026 (Umbau). Vorlage: **Modular Builder 0.2.0** von Michel Savadim, +Godot-Erweiterung, Lizenz **CC0 1.0** (Code, Icons, Doku — siehe +`addons/modular_builder/LICENSE`). Die Demo-Szene/Demo-Texturen der Vorlage sind +nicht CC0 und liegen bewusst nicht im Repo (`ADDON_LIZENZ.txt`). + +## Prinzip + +Ein Bauteil ist ein **ganz normales Bibliotheks-Objekt** (`bau_*`, Kategorie +„bauteile“). Gesetzte Teile stehen in `objects`, ein Gebäude ist eine **Gruppe** +(`group` + `group_names`). Dadurch gelten ohne Sonderfälle: + +* Undo/Redo, Auswahl, Verschieben, Drehen, Duplizieren, Löschen, Symmetrie +* Speichern in der `.svmap` (kein eigener Block mehr, Server-Validierung V1–V17) +* **Kollision serverkonsistent**: jedes Teil hat eine Trimesh-Datei + (`library/kollisions/bau_*.json`) — Türen, Fenster, Schießscharten sind echte + Löcher, die Treppe ist eine Stufenfläche (Stufen 20 cm). Editor-Playtest, Client + und Server lesen dieselbe Datei. +* Poly-Budget zählt wie bei jedem Objekt (`poly` = Dreiecke des Teils). + +## Teile (Katalog: `src/editor/bau_katalog.gd`) + +| Gruppe | Teile | +| --- | --- | +| 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 | +| 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). + +## 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). +* **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. +* **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene + Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt. + +## Dateien erzeugen + +Meshes, GLBs, Kollision und Registry-Fragment kommen aus dem Katalog: + +```bash +godot --headless --path . --script res://tools/gen_bauteile.gd -- \ + --client ../scopedvr-client --server ../scopedvr-match-server +(cd ../scopedvr-match-server && python3 tools/merge_object_library.py) +``` + +Danach `library/objects.json` in Client und Editor angleichen (`tools/sync_objects.sh`). +`tests/test_bauen.gd` prüft, dass die Registry zum Katalog passt. + +## Alte Maps + +Die frühere Sektion `gebaeude` (Bauen-Modus vom 20.10.) wird beim Öffnen in +`bau_*`-Objekte einer Gruppe übernommen (`BauOps.migriere_gebaeude`). Wandhöhe ist +jetzt 3 m statt 2 m — alte Häuser bitte kurz prüfen und neu speichern. diff --git a/docs/tasten.md b/docs/tasten.md index 5647c1f..6912a83 100644 --- a/docs/tasten.md +++ b/docs/tasten.md @@ -56,7 +56,7 @@ alle, Strg+G gruppieren / Strg+Umschalt+G lösen. | Eingabe | Wirkung | | --- | --- | -| **R** | Auswahl 90° drehen (Vierteldrehung; Umschalt+R = −90°). Szene-Objekte drehen um den Auswahl-Mittelpunkt, Bauen-Bauteile um ihren Fußpunkt. Ein Objekt aus einer Gruppe dreht die GANZE Gruppe. Dauerhaft (kein Vorschau-Zustand) | +| **R** | Auswahl 90° drehen (Vierteldrehung; Umschalt+R = −90°). Objekte (auch Bauteile) drehen um den Auswahl-Mittelpunkt. Ein Objekt aus einer Gruppe dreht die GANZE Gruppe. Dauerhaft (kein Vorschau-Zustand) | | **Strg+R / Strg+Umschalt+R** | Auswahl um X **kippen** (+15° / −15°) — auch im Platzier-Modus (Ghost kippt mit). Karte t_e166e656 | | **Alt+R / Alt+Umschalt+R** | Auswahl um Z **kippen** (+15° / −15°) — auch im Platzier-Modus. Karte t_e166e656 | | Mausrad (mit Auswahl) | Feindrehen im 15°-Raster (bleibt wie bisher) | @@ -177,9 +177,9 @@ keinen fokussierten Button auslöst. | --- | --- | | **F1** | Werkzeug: Terrain formen | | **F2** | Werkzeug: Objekte platzieren/auswählen (Standard) | -| **F3** | Werkzeug: Bauen (Häuser) | +| **F3** | Tab „Bauen“ öffnen (Bauteile wählen, dann in die Karte klicken) | | **F4** | Werkzeug: Spawn-Zonen malen | -| Esc (im Terrain-/Spawn-/Bauen-Werkzeug) | Werkzeug beenden — zurück zum Objekt-Werkzeug | +| Esc (im Terrain-/Spawn-Werkzeug) | Werkzeug beenden — zurück zum Objekt-Werkzeug | | Esc (beim Greifen) | Greifen abbrechen, alte Positionen zurück | | Esc (beim Platzieren) | Ghost-Preview abbrechen | | Esc (Kurzhilfe offen) | Kurzhilfe schließen | diff --git a/library/kollisions/bau_boden.json b/library/kollisions/bau_boden.json new file mode 100644 index 0000000..5798e23 --- /dev/null +++ b/library/kollisions/bau_boden.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,0.2,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":24,"oid":"bau_boden","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_boden.glb","vertices":[[-1.0,0.2,-1.0],[1.0,0.2,-1.0],[1.0,0.2,1.0],[1.0,0.2,-1.0],[-1.0,0.2,-1.0],[1.0,0.2,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[-1.0,0.2,-1.0],[1.0,0.2,1.0],[-1.0,0.2,1.0],[1.0,0.2,1.0],[-1.0,0.2,-1.0],[-1.0,0.2,1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.2,-1.0],[1.0,0.0,-1.0],[1.0,0.2,-1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[-1.0,0.0,-1.0],[-1.0,0.2,-1.0],[1.0,0.2,-1.0],[-1.0,0.2,-1.0],[-1.0,0.0,-1.0],[1.0,0.2,-1.0],[1.0,0.0,-1.0],[1.0,0.2,1.0],[1.0,0.0,1.0],[1.0,0.2,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.2,-1.0],[1.0,0.2,1.0],[1.0,0.2,-1.0],[1.0,0.0,-1.0],[1.0,0.2,1.0],[1.0,0.0,1.0],[-1.0,0.2,1.0],[-1.0,0.0,1.0],[-1.0,0.2,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0],[1.0,0.2,1.0],[-1.0,0.2,1.0],[1.0,0.2,1.0],[1.0,0.0,1.0],[-1.0,0.2,1.0],[-1.0,0.0,1.0],[-1.0,0.2,-1.0],[-1.0,0.0,-1.0],[-1.0,0.2,-1.0],[-1.0,0.0,1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[-1.0,0.2,1.0],[-1.0,0.2,-1.0],[-1.0,0.2,1.0],[-1.0,0.0,1.0],[-1.0,0.2,-1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_bruestung.json b/library/kollisions/bau_bruestung.json new file mode 100644 index 0000000..06ef667 --- /dev/null +++ b/library/kollisions/bau_bruestung.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,1.1,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":24,"oid":"bau_bruestung","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_bruestung.glb","vertices":[[-1.0,1.1,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,1.1,0.1],[-1.0,0.0,0.1],[1.0,0.0,-0.1],[-1.0,1.1,-0.1],[-1.0,0.0,-0.1],[-1.0,1.1,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,1.1,0.1],[1.0,1.1,0.1],[-1.0,1.1,0.1],[1.0,0.0,0.1],[1.0,1.1,0.1],[-1.0,1.1,-0.1],[1.0,0.0,-0.1],[1.0,1.1,-0.1],[1.0,0.0,-0.1],[-1.0,1.1,-0.1],[1.0,1.1,-0.1],[-1.0,0.0,-0.1],[-1.0,1.1,0.1],[-1.0,0.0,0.1],[-1.0,1.1,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,1.1,-0.1],[-1.0,1.1,0.1],[-1.0,1.1,-0.1],[-1.0,0.0,-0.1],[-1.0,1.1,0.1],[1.0,0.0,0.1],[1.0,1.1,-0.1],[1.0,0.0,-0.1],[1.0,1.1,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,1.1,0.1],[1.0,1.1,-0.1],[1.0,1.1,0.1],[1.0,0.0,0.1],[1.0,1.1,-0.1],[-1.0,1.1,0.1],[1.0,1.1,-0.1],[1.0,1.1,0.1],[1.0,1.1,-0.1],[-1.0,1.1,0.1],[1.0,1.1,0.1],[-1.0,1.1,0.1],[-1.0,1.1,-0.1],[1.0,1.1,-0.1],[-1.0,1.1,-0.1],[-1.0,1.1,0.1],[1.0,1.1,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_giebel.json b/library/kollisions/bau_dach_giebel.json new file mode 100644 index 0000000..e37d941 --- /dev/null +++ b/library/kollisions/bau_dach_giebel.json @@ -0,0 +1 @@ +{"aabb_max":[1.071,1.141,1.0],"aabb_min":[-1.071,0.0,-1.0],"dreiecke":48,"oid":"bau_dach_giebel","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_giebel.glb","vertices":[[-1.0707,0.1414,-1.0],[-0.0707,1.1414,1.0],[-1.0707,0.1414,1.0],[-0.0707,1.1414,1.0],[-1.0707,0.1414,-1.0],[-1.0707,0.1414,1.0],[0.0707,1.0,1.0],[-0.9293,0.0,-1.0],[-0.9293,0.0,1.0],[-0.9293,0.0,-1.0],[0.0707,1.0,1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,-1.0],[-0.0707,1.1414,-1.0],[-0.0707,1.1414,1.0],[-0.0707,1.1414,-1.0],[-1.0707,0.1414,-1.0],[-0.0707,1.1414,1.0],[0.0707,1.0,-1.0],[-0.9293,0.0,-1.0],[0.0707,1.0,1.0],[-0.9293,0.0,-1.0],[0.0707,1.0,-1.0],[0.0707,1.0,1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,1.0],[-0.9293,0.0,-1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,-1.0],[-1.0707,0.1414,-1.0],[-1.0707,0.1414,1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,1.0],[0.0707,1.0,1.0],[-0.9293,0.0,1.0],[0.0707,1.0,1.0],[-1.0707,0.1414,1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,1.0],[-0.0707,1.1414,1.0],[0.0707,1.0,1.0],[-0.0707,1.1414,1.0],[-1.0707,0.1414,1.0],[0.0707,1.0,1.0],[-0.0707,1.1414,1.0],[0.0707,1.0,-1.0],[0.0707,1.0,1.0],[0.0707,1.0,-1.0],[-0.0707,1.1414,1.0],[0.0707,1.0,1.0],[-0.0707,1.1414,1.0],[-0.0707,1.1414,-1.0],[0.0707,1.0,-1.0],[-0.0707,1.1414,-1.0],[-0.0707,1.1414,1.0],[0.0707,1.0,-1.0],[-0.0707,1.1414,-1.0],[-0.9293,0.0,-1.0],[0.0707,1.0,-1.0],[-0.9293,0.0,-1.0],[-0.0707,1.1414,-1.0],[0.0707,1.0,-1.0],[-0.0707,1.1414,-1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,-1.0],[-1.0707,0.1414,-1.0],[-0.0707,1.1414,-1.0],[-0.9293,0.0,-1.0],[0.0707,1.1414,-1.0],[1.0707,0.1414,1.0],[0.0707,1.1414,1.0],[1.0707,0.1414,1.0],[0.0707,1.1414,-1.0],[0.0707,1.1414,1.0],[0.9293,0.0,1.0],[-0.0707,1.0,-1.0],[-0.0707,1.0,1.0],[-0.0707,1.0,-1.0],[0.9293,0.0,1.0],[-0.0707,1.0,1.0],[0.0707,1.1414,-1.0],[1.0707,0.1414,-1.0],[1.0707,0.1414,1.0],[1.0707,0.1414,-1.0],[0.0707,1.1414,-1.0],[1.0707,0.1414,1.0],[0.9293,0.0,-1.0],[-0.0707,1.0,-1.0],[0.9293,0.0,1.0],[-0.0707,1.0,-1.0],[0.9293,0.0,-1.0],[0.9293,0.0,1.0],[0.0707,1.1414,-1.0],[-0.0707,1.0,1.0],[-0.0707,1.0,-1.0],[-0.0707,1.0,1.0],[0.0707,1.1414,-1.0],[-0.0707,1.0,-1.0],[0.0707,1.1414,-1.0],[0.0707,1.1414,1.0],[-0.0707,1.0,1.0],[0.0707,1.1414,1.0],[0.0707,1.1414,-1.0],[-0.0707,1.0,1.0],[0.0707,1.1414,1.0],[0.9293,0.0,1.0],[-0.0707,1.0,1.0],[0.9293,0.0,1.0],[0.0707,1.1414,1.0],[-0.0707,1.0,1.0],[0.0707,1.1414,1.0],[1.0707,0.1414,1.0],[0.9293,0.0,1.0],[1.0707,0.1414,1.0],[0.0707,1.1414,1.0],[0.9293,0.0,1.0],[1.0707,0.1414,1.0],[0.9293,0.0,-1.0],[0.9293,0.0,1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,1.0],[0.9293,0.0,1.0],[1.0707,0.1414,1.0],[1.0707,0.1414,-1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,-1.0],[1.0707,0.1414,1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,-1.0],[-0.0707,1.0,-1.0],[0.9293,0.0,-1.0],[-0.0707,1.0,-1.0],[1.0707,0.1414,-1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,-1.0],[0.0707,1.1414,-1.0],[-0.0707,1.0,-1.0],[0.0707,1.1414,-1.0],[1.0707,0.1414,-1.0],[-0.0707,1.0,-1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_dach_pyramide.json b/library/kollisions/bau_dach_pyramide.json new file mode 100644 index 0000000..d9e2c50 --- /dev/null +++ b/library/kollisions/bau_dach_pyramide.json @@ -0,0 +1 @@ +{"aabb_max":[1.071,1.141,1.071],"aabb_min":[-1.071,0.0,-1.071],"dreiecke":64,"oid":"bau_dach_pyramide","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_pyramide.glb","vertices":[[-1.0,0.0,-0.9293],[0.0,1.0,0.0707],[1.0,0.0,-0.9293],[0.0,1.0,0.0707],[-1.0,0.0,-0.9293],[1.0,0.0,-0.9293],[0.0,1.1414,-0.0707],[-1.0,0.1414,-1.0707],[1.0,0.1414,-1.0707],[-1.0,0.1414,-1.0707],[0.0,1.1414,-0.0707],[1.0,0.1414,-1.0707],[-1.0,0.0,-0.9293],[1.0,0.1414,-1.0707],[-1.0,0.1414,-1.0707],[1.0,0.1414,-1.0707],[-1.0,0.0,-0.9293],[-1.0,0.1414,-1.0707],[-1.0,0.0,-0.9293],[1.0,0.0,-0.9293],[1.0,0.1414,-1.0707],[1.0,0.0,-0.9293],[-1.0,0.0,-0.9293],[1.0,0.1414,-1.0707],[1.0,0.0,-0.9293],[0.0,1.1414,-0.0707],[1.0,0.1414,-1.0707],[0.0,1.1414,-0.0707],[1.0,0.0,-0.9293],[1.0,0.1414,-1.0707],[1.0,0.0,-0.9293],[0.0,1.0,0.0707],[0.0,1.1414,-0.0707],[0.0,1.0,0.0707],[1.0,0.0,-0.9293],[0.0,1.1414,-0.0707],[0.0,1.0,0.0707],[-1.0,0.1414,-1.0707],[0.0,1.1414,-0.0707],[-1.0,0.1414,-1.0707],[0.0,1.0,0.0707],[0.0,1.1414,-0.0707],[0.0,1.0,0.0707],[-1.0,0.0,-0.9293],[-1.0,0.1414,-1.0707],[-1.0,0.0,-0.9293],[0.0,1.0,0.0707],[-1.0,0.1414,-1.0707],[0.9293,0.0,-1.0],[-0.0707,1.0,0.0],[0.9293,0.0,1.0],[-0.0707,1.0,0.0],[0.9293,0.0,-1.0],[0.9293,0.0,1.0],[0.0707,1.1414,0.0],[1.0707,0.1414,-1.0],[1.0707,0.1414,1.0],[1.0707,0.1414,-1.0],[0.0707,1.1414,0.0],[1.0707,0.1414,1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,1.0],[1.0707,0.1414,-1.0],[1.0707,0.1414,1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,-1.0],[0.9293,0.0,-1.0],[0.9293,0.0,1.0],[1.0707,0.1414,1.0],[0.9293,0.0,1.0],[0.9293,0.0,-1.0],[1.0707,0.1414,1.0],[0.9293,0.0,1.0],[0.0707,1.1414,0.0],[1.0707,0.1414,1.0],[0.0707,1.1414,0.0],[0.9293,0.0,1.0],[1.0707,0.1414,1.0],[0.9293,0.0,1.0],[-0.0707,1.0,0.0],[0.0707,1.1414,0.0],[-0.0707,1.0,0.0],[0.9293,0.0,1.0],[0.0707,1.1414,0.0],[-0.0707,1.0,0.0],[1.0707,0.1414,-1.0],[0.0707,1.1414,0.0],[1.0707,0.1414,-1.0],[-0.0707,1.0,0.0],[0.0707,1.1414,0.0],[-0.0707,1.0,0.0],[0.9293,0.0,-1.0],[1.0707,0.1414,-1.0],[0.9293,0.0,-1.0],[-0.0707,1.0,0.0],[1.0707,0.1414,-1.0],[1.0,0.0,0.9293],[0.0,1.0,-0.0707],[-1.0,0.0,0.9293],[0.0,1.0,-0.0707],[1.0,0.0,0.9293],[-1.0,0.0,0.9293],[0.0,1.1414,0.0707],[1.0,0.1414,1.0707],[-1.0,0.1414,1.0707],[1.0,0.1414,1.0707],[0.0,1.1414,0.0707],[-1.0,0.1414,1.0707],[1.0,0.0,0.9293],[-1.0,0.1414,1.0707],[1.0,0.1414,1.0707],[-1.0,0.1414,1.0707],[1.0,0.0,0.9293],[1.0,0.1414,1.0707],[1.0,0.0,0.9293],[-1.0,0.0,0.9293],[-1.0,0.1414,1.0707],[-1.0,0.0,0.9293],[1.0,0.0,0.9293],[-1.0,0.1414,1.0707],[-1.0,0.0,0.9293],[0.0,1.1414,0.0707],[-1.0,0.1414,1.0707],[0.0,1.1414,0.0707],[-1.0,0.0,0.9293],[-1.0,0.1414,1.0707],[-1.0,0.0,0.9293],[0.0,1.0,-0.0707],[0.0,1.1414,0.0707],[0.0,1.0,-0.0707],[-1.0,0.0,0.9293],[0.0,1.1414,0.0707],[0.0,1.0,-0.0707],[1.0,0.1414,1.0707],[0.0,1.1414,0.0707],[1.0,0.1414,1.0707],[0.0,1.0,-0.0707],[0.0,1.1414,0.0707],[0.0,1.0,-0.0707],[1.0,0.0,0.9293],[1.0,0.1414,1.0707],[1.0,0.0,0.9293],[0.0,1.0,-0.0707],[1.0,0.1414,1.0707],[-0.9293,0.0,1.0],[0.0707,1.0,0.0],[-0.9293,0.0,-1.0],[0.0707,1.0,0.0],[-0.9293,0.0,1.0],[-0.9293,0.0,-1.0],[-0.0707,1.1414,0.0],[-1.0707,0.1414,1.0],[-1.0707,0.1414,-1.0],[-1.0707,0.1414,1.0],[-0.0707,1.1414,0.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,-1.0],[-1.0707,0.1414,1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,1.0],[-0.9293,0.0,1.0],[-0.9293,0.0,-1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,-1.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,-1.0],[-0.0707,1.1414,0.0],[-1.0707,0.1414,-1.0],[-0.0707,1.1414,0.0],[-0.9293,0.0,-1.0],[-1.0707,0.1414,-1.0],[-0.9293,0.0,-1.0],[0.0707,1.0,0.0],[-0.0707,1.1414,0.0],[0.0707,1.0,0.0],[-0.9293,0.0,-1.0],[-0.0707,1.1414,0.0],[0.0707,1.0,0.0],[-1.0707,0.1414,1.0],[-0.0707,1.1414,0.0],[-1.0707,0.1414,1.0],[0.0707,1.0,0.0],[-0.0707,1.1414,0.0],[0.0707,1.0,0.0],[-0.9293,0.0,1.0],[-1.0707,0.1414,1.0],[-0.9293,0.0,1.0],[0.0707,1.0,0.0],[-1.0707,0.1414,1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_fundament.json b/library/kollisions/bau_fundament.json new file mode 100644 index 0000000..30890ec --- /dev/null +++ b/library/kollisions/bau_fundament.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,0.2,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":24,"oid":"bau_fundament","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_fundament.glb","vertices":[[-1.0,0.2,-1.0],[1.0,0.2,-1.0],[1.0,0.2,1.0],[1.0,0.2,-1.0],[-1.0,0.2,-1.0],[1.0,0.2,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[-1.0,0.2,-1.0],[1.0,0.2,1.0],[-1.0,0.2,1.0],[1.0,0.2,1.0],[-1.0,0.2,-1.0],[-1.0,0.2,1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,0.2,-1.0],[1.0,0.0,-1.0],[1.0,0.2,-1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[-1.0,0.0,-1.0],[-1.0,0.2,-1.0],[1.0,0.2,-1.0],[-1.0,0.2,-1.0],[-1.0,0.0,-1.0],[1.0,0.2,-1.0],[1.0,0.0,-1.0],[1.0,0.2,1.0],[1.0,0.0,1.0],[1.0,0.2,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.2,-1.0],[1.0,0.2,1.0],[1.0,0.2,-1.0],[1.0,0.0,-1.0],[1.0,0.2,1.0],[1.0,0.0,1.0],[-1.0,0.2,1.0],[-1.0,0.0,1.0],[-1.0,0.2,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,1.0],[1.0,0.2,1.0],[-1.0,0.2,1.0],[1.0,0.2,1.0],[1.0,0.0,1.0],[-1.0,0.2,1.0],[-1.0,0.0,1.0],[-1.0,0.2,-1.0],[-1.0,0.0,-1.0],[-1.0,0.2,-1.0],[-1.0,0.0,1.0],[-1.0,0.0,-1.0],[-1.0,0.0,1.0],[-1.0,0.2,1.0],[-1.0,0.2,-1.0],[-1.0,0.2,1.0],[-1.0,0.0,1.0],[-1.0,0.2,-1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_giebelwand.json b/library/kollisions/bau_giebelwand.json new file mode 100644 index 0000000..4cb1dfb --- /dev/null +++ b/library/kollisions/bau_giebelwand.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,1.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":16,"oid":"bau_giebelwand","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_giebelwand.glb","vertices":[[-1.0,0.0,0.1],[0.0,1.0,0.1],[1.0,0.0,0.1],[0.0,1.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[0.0,1.0,-0.1],[-1.0,0.0,-0.1],[0.0,1.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[0.0,1.0,-0.1],[1.0,0.0,-0.1],[0.0,1.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[0.0,1.0,0.1],[0.0,1.0,-0.1],[0.0,1.0,0.1],[1.0,0.0,0.1],[0.0,1.0,-0.1],[0.0,1.0,0.1],[-1.0,0.0,-0.1],[0.0,1.0,-0.1],[-1.0,0.0,-0.1],[0.0,1.0,0.1],[0.0,1.0,-0.1],[0.0,1.0,0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[0.0,1.0,0.1],[-1.0,0.0,-0.1]]} \ No newline at end of file diff --git a/library/kollisions/bau_pfeiler.json b/library/kollisions/bau_pfeiler.json new file mode 100644 index 0000000..85bdc32 --- /dev/null +++ b/library/kollisions/bau_pfeiler.json @@ -0,0 +1 @@ +{"aabb_max":[0.15,3.0,0.15],"aabb_min":[-0.15,0.0,-0.15],"dreiecke":12,"oid":"bau_pfeiler","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_pfeiler.glb","vertices":[[-0.15,3.0,0.15],[0.15,3.0,0.15],[-0.15,0.0,0.15],[0.15,3.0,0.15],[0.15,0.0,0.15],[-0.15,0.0,0.15],[0.15,3.0,-0.15],[-0.15,3.0,-0.15],[0.15,0.0,-0.15],[-0.15,3.0,-0.15],[-0.15,0.0,-0.15],[0.15,0.0,-0.15],[0.15,3.0,0.15],[0.15,3.0,-0.15],[0.15,0.0,0.15],[0.15,3.0,-0.15],[0.15,0.0,-0.15],[0.15,0.0,0.15],[-0.15,3.0,-0.15],[-0.15,3.0,0.15],[-0.15,0.0,-0.15],[-0.15,3.0,0.15],[-0.15,0.0,0.15],[-0.15,0.0,-0.15],[0.15,3.0,0.15],[-0.15,3.0,0.15],[0.15,3.0,-0.15],[-0.15,3.0,0.15],[-0.15,3.0,-0.15],[0.15,3.0,-0.15],[-0.15,0.0,0.15],[0.15,0.0,0.15],[-0.15,0.0,-0.15],[0.15,0.0,0.15],[0.15,0.0,-0.15],[-0.15,0.0,-0.15]]} \ No newline at end of file diff --git a/library/kollisions/bau_treppe.json b/library/kollisions/bau_treppe.json new file mode 100644 index 0000000..7084e47 --- /dev/null +++ b/library/kollisions/bau_treppe.json @@ -0,0 +1 @@ +{"aabb_max":[2.0,3.0,1.0],"aabb_min":[-2.0,0.0,-1.0],"dreiecke":248,"oid":"bau_treppe","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_treppe.glb","vertices":[[2.0,0.0,-1.0],[-2.0,0.2,-1.0],[-2.0,0.0,-1.0],[-2.0,0.2,-1.0],[2.0,0.0,-1.0],[-2.0,0.0,-1.0],[1.7333,3.0,-1.0],[2.0,0.0,-1.0],[2.0,3.0,-1.0],[2.0,0.0,-1.0],[1.7333,3.0,-1.0],[2.0,3.0,-1.0],[1.4667,2.6,-1.0],[1.7333,2.8,-1.0],[1.4667,2.8,-1.0],[1.7333,2.8,-1.0],[1.4667,2.6,-1.0],[1.4667,2.8,-1.0],[1.2,2.4,-1.0],[1.4667,2.6,-1.0],[1.2,2.6,-1.0],[1.4667,2.6,-1.0],[1.2,2.4,-1.0],[1.2,2.6,-1.0],[0.9333,2.2,-1.0],[1.2,2.4,-1.0],[0.9333,2.4,-1.0],[1.2,2.4,-1.0],[0.9333,2.2,-1.0],[0.9333,2.4,-1.0],[0.6667,2.0,-1.0],[0.9333,2.2,-1.0],[0.6667,2.2,-1.0],[0.9333,2.2,-1.0],[0.6667,2.0,-1.0],[0.6667,2.2,-1.0],[0.4,1.8,-1.0],[0.6667,2.0,-1.0],[0.4,2.0,-1.0],[0.6667,2.0,-1.0],[0.4,1.8,-1.0],[0.4,2.0,-1.0],[0.1333,1.6,-1.0],[0.4,1.8,-1.0],[0.1333,1.8,-1.0],[0.4,1.8,-1.0],[0.1333,1.6,-1.0],[0.1333,1.8,-1.0],[-0.1333,1.4,-1.0],[0.1333,1.6,-1.0],[-0.1333,1.6,-1.0],[0.1333,1.6,-1.0],[-0.1333,1.4,-1.0],[-0.1333,1.6,-1.0],[-0.4,1.2,-1.0],[-0.1333,1.4,-1.0],[-0.4,1.4,-1.0],[-0.1333,1.4,-1.0],[-0.4,1.2,-1.0],[-0.4,1.4,-1.0],[-0.6667,1.0,-1.0],[-0.4,1.2,-1.0],[-0.6667,1.2,-1.0],[-0.4,1.2,-1.0],[-0.6667,1.0,-1.0],[-0.6667,1.2,-1.0],[-0.9333,0.8,-1.0],[-0.6667,1.0,-1.0],[-0.9333,1.0,-1.0],[-0.6667,1.0,-1.0],[-0.9333,0.8,-1.0],[-0.9333,1.0,-1.0],[-1.2,0.6,-1.0],[-0.9333,0.8,-1.0],[-1.2,0.8,-1.0],[-0.9333,0.8,-1.0],[-1.2,0.6,-1.0],[-1.2,0.8,-1.0],[-1.4667,0.4,-1.0],[-1.2,0.6,-1.0],[-1.4667,0.6,-1.0],[-1.2,0.6,-1.0],[-1.4667,0.4,-1.0],[-1.4667,0.6,-1.0],[-1.7333,0.2,-1.0],[-1.4667,0.4,-1.0],[-1.7333,0.4,-1.0],[-1.4667,0.4,-1.0],[-1.7333,0.2,-1.0],[-1.7333,0.4,-1.0],[2.0,0.0,-1.0],[-1.7333,0.2,-1.0],[-2.0,0.2,-1.0],[-1.7333,0.2,-1.0],[2.0,0.0,-1.0],[-2.0,0.2,-1.0],[1.7333,2.8,-1.0],[2.0,0.0,-1.0],[1.7333,3.0,-1.0],[2.0,0.0,-1.0],[1.7333,2.8,-1.0],[1.7333,3.0,-1.0],[2.0,0.0,-1.0],[-1.4667,0.4,-1.0],[-1.7333,0.2,-1.0],[-1.4667,0.4,-1.0],[2.0,0.0,-1.0],[-1.7333,0.2,-1.0],[1.4667,2.6,-1.0],[2.0,0.0,-1.0],[1.7333,2.8,-1.0],[2.0,0.0,-1.0],[1.4667,2.6,-1.0],[1.7333,2.8,-1.0],[2.0,0.0,-1.0],[-1.2,0.6,-1.0],[-1.4667,0.4,-1.0],[-1.2,0.6,-1.0],[2.0,0.0,-1.0],[-1.4667,0.4,-1.0],[1.2,2.4,-1.0],[2.0,0.0,-1.0],[1.4667,2.6,-1.0],[2.0,0.0,-1.0],[1.2,2.4,-1.0],[1.4667,2.6,-1.0],[2.0,0.0,-1.0],[-0.9333,0.8,-1.0],[-1.2,0.6,-1.0],[-0.9333,0.8,-1.0],[2.0,0.0,-1.0],[-1.2,0.6,-1.0],[0.9333,2.2,-1.0],[2.0,0.0,-1.0],[1.2,2.4,-1.0],[2.0,0.0,-1.0],[0.9333,2.2,-1.0],[1.2,2.4,-1.0],[2.0,0.0,-1.0],[-0.6667,1.0,-1.0],[-0.9333,0.8,-1.0],[-0.6667,1.0,-1.0],[2.0,0.0,-1.0],[-0.9333,0.8,-1.0],[0.6667,2.0,-1.0],[2.0,0.0,-1.0],[0.9333,2.2,-1.0],[2.0,0.0,-1.0],[0.6667,2.0,-1.0],[0.9333,2.2,-1.0],[2.0,0.0,-1.0],[-0.4,1.2,-1.0],[-0.6667,1.0,-1.0],[-0.4,1.2,-1.0],[2.0,0.0,-1.0],[-0.6667,1.0,-1.0],[0.4,1.8,-1.0],[2.0,0.0,-1.0],[0.6667,2.0,-1.0],[2.0,0.0,-1.0],[0.4,1.8,-1.0],[0.6667,2.0,-1.0],[2.0,0.0,-1.0],[-0.1333,1.4,-1.0],[-0.4,1.2,-1.0],[-0.1333,1.4,-1.0],[2.0,0.0,-1.0],[-0.4,1.2,-1.0],[0.1333,1.6,-1.0],[2.0,0.0,-1.0],[0.4,1.8,-1.0],[2.0,0.0,-1.0],[0.1333,1.6,-1.0],[0.4,1.8,-1.0],[2.0,0.0,-1.0],[0.1333,1.6,-1.0],[-0.1333,1.4,-1.0],[0.1333,1.6,-1.0],[2.0,0.0,-1.0],[-0.1333,1.4,-1.0],[-2.0,0.2,1.0],[2.0,0.0,1.0],[-2.0,0.0,1.0],[2.0,0.0,1.0],[-2.0,0.2,1.0],[-2.0,0.0,1.0],[2.0,0.0,1.0],[1.7333,3.0,1.0],[2.0,3.0,1.0],[1.7333,3.0,1.0],[2.0,0.0,1.0],[2.0,3.0,1.0],[1.7333,2.8,1.0],[1.4667,2.6,1.0],[1.4667,2.8,1.0],[1.4667,2.6,1.0],[1.7333,2.8,1.0],[1.4667,2.8,1.0],[1.4667,2.6,1.0],[1.2,2.4,1.0],[1.2,2.6,1.0],[1.2,2.4,1.0],[1.4667,2.6,1.0],[1.2,2.6,1.0],[1.2,2.4,1.0],[0.9333,2.2,1.0],[0.9333,2.4,1.0],[0.9333,2.2,1.0],[1.2,2.4,1.0],[0.9333,2.4,1.0],[0.9333,2.2,1.0],[0.6667,2.0,1.0],[0.6667,2.2,1.0],[0.6667,2.0,1.0],[0.9333,2.2,1.0],[0.6667,2.2,1.0],[0.6667,2.0,1.0],[0.4,1.8,1.0],[0.4,2.0,1.0],[0.4,1.8,1.0],[0.6667,2.0,1.0],[0.4,2.0,1.0],[0.4,1.8,1.0],[0.1333,1.6,1.0],[0.1333,1.8,1.0],[0.1333,1.6,1.0],[0.4,1.8,1.0],[0.1333,1.8,1.0],[0.1333,1.6,1.0],[-0.1333,1.4,1.0],[-0.1333,1.6,1.0],[-0.1333,1.4,1.0],[0.1333,1.6,1.0],[-0.1333,1.6,1.0],[-0.1333,1.4,1.0],[-0.4,1.2,1.0],[-0.4,1.4,1.0],[-0.4,1.2,1.0],[-0.1333,1.4,1.0],[-0.4,1.4,1.0],[-0.4,1.2,1.0],[-0.6667,1.0,1.0],[-0.6667,1.2,1.0],[-0.6667,1.0,1.0],[-0.4,1.2,1.0],[-0.6667,1.2,1.0],[-0.6667,1.0,1.0],[-0.9333,0.8,1.0],[-0.9333,1.0,1.0],[-0.9333,0.8,1.0],[-0.6667,1.0,1.0],[-0.9333,1.0,1.0],[-0.9333,0.8,1.0],[-1.2,0.6,1.0],[-1.2,0.8,1.0],[-1.2,0.6,1.0],[-0.9333,0.8,1.0],[-1.2,0.8,1.0],[-1.2,0.6,1.0],[-1.4667,0.4,1.0],[-1.4667,0.6,1.0],[-1.4667,0.4,1.0],[-1.2,0.6,1.0],[-1.4667,0.6,1.0],[-1.4667,0.4,1.0],[-1.7333,0.2,1.0],[-1.7333,0.4,1.0],[-1.7333,0.2,1.0],[-1.4667,0.4,1.0],[-1.7333,0.4,1.0],[-1.7333,0.2,1.0],[2.0,0.0,1.0],[-2.0,0.2,1.0],[2.0,0.0,1.0],[-1.7333,0.2,1.0],[-2.0,0.2,1.0],[2.0,0.0,1.0],[1.7333,2.8,1.0],[1.7333,3.0,1.0],[1.7333,2.8,1.0],[2.0,0.0,1.0],[1.7333,3.0,1.0],[-1.4667,0.4,1.0],[2.0,0.0,1.0],[-1.7333,0.2,1.0],[2.0,0.0,1.0],[-1.4667,0.4,1.0],[-1.7333,0.2,1.0],[2.0,0.0,1.0],[1.4667,2.6,1.0],[1.7333,2.8,1.0],[1.4667,2.6,1.0],[2.0,0.0,1.0],[1.7333,2.8,1.0],[-1.2,0.6,1.0],[2.0,0.0,1.0],[-1.4667,0.4,1.0],[2.0,0.0,1.0],[-1.2,0.6,1.0],[-1.4667,0.4,1.0],[2.0,0.0,1.0],[1.2,2.4,1.0],[1.4667,2.6,1.0],[1.2,2.4,1.0],[2.0,0.0,1.0],[1.4667,2.6,1.0],[-0.9333,0.8,1.0],[2.0,0.0,1.0],[-1.2,0.6,1.0],[2.0,0.0,1.0],[-0.9333,0.8,1.0],[-1.2,0.6,1.0],[2.0,0.0,1.0],[0.9333,2.2,1.0],[1.2,2.4,1.0],[0.9333,2.2,1.0],[2.0,0.0,1.0],[1.2,2.4,1.0],[-0.6667,1.0,1.0],[2.0,0.0,1.0],[-0.9333,0.8,1.0],[2.0,0.0,1.0],[-0.6667,1.0,1.0],[-0.9333,0.8,1.0],[2.0,0.0,1.0],[0.6667,2.0,1.0],[0.9333,2.2,1.0],[0.6667,2.0,1.0],[2.0,0.0,1.0],[0.9333,2.2,1.0],[-0.4,1.2,1.0],[2.0,0.0,1.0],[-0.6667,1.0,1.0],[2.0,0.0,1.0],[-0.4,1.2,1.0],[-0.6667,1.0,1.0],[2.0,0.0,1.0],[0.4,1.8,1.0],[0.6667,2.0,1.0],[0.4,1.8,1.0],[2.0,0.0,1.0],[0.6667,2.0,1.0],[-0.1333,1.4,1.0],[2.0,0.0,1.0],[-0.4,1.2,1.0],[2.0,0.0,1.0],[-0.1333,1.4,1.0],[-0.4,1.2,1.0],[2.0,0.0,1.0],[0.1333,1.6,1.0],[0.4,1.8,1.0],[0.1333,1.6,1.0],[2.0,0.0,1.0],[0.4,1.8,1.0],[0.1333,1.6,1.0],[2.0,0.0,1.0],[-0.1333,1.4,1.0],[2.0,0.0,1.0],[0.1333,1.6,1.0],[-0.1333,1.4,1.0],[-2.0,0.0,-1.0],[2.0,0.0,1.0],[2.0,0.0,-1.0],[2.0,0.0,1.0],[-2.0,0.0,-1.0],[2.0,0.0,-1.0],[-2.0,0.0,-1.0],[-2.0,0.0,1.0],[2.0,0.0,1.0],[-2.0,0.0,1.0],[-2.0,0.0,-1.0],[2.0,0.0,1.0],[2.0,0.0,-1.0],[2.0,3.0,1.0],[2.0,3.0,-1.0],[2.0,3.0,1.0],[2.0,0.0,-1.0],[2.0,3.0,-1.0],[2.0,0.0,-1.0],[2.0,0.0,1.0],[2.0,3.0,1.0],[2.0,0.0,1.0],[2.0,0.0,-1.0],[2.0,3.0,1.0],[2.0,3.0,-1.0],[1.7333,3.0,1.0],[1.7333,3.0,-1.0],[1.7333,3.0,1.0],[2.0,3.0,-1.0],[1.7333,3.0,-1.0],[2.0,3.0,-1.0],[2.0,3.0,1.0],[1.7333,3.0,1.0],[2.0,3.0,1.0],[2.0,3.0,-1.0],[1.7333,3.0,1.0],[1.7333,3.0,-1.0],[1.7333,2.8,1.0],[1.7333,2.8,-1.0],[1.7333,2.8,1.0],[1.7333,3.0,-1.0],[1.7333,2.8,-1.0],[1.7333,3.0,-1.0],[1.7333,3.0,1.0],[1.7333,2.8,1.0],[1.7333,3.0,1.0],[1.7333,3.0,-1.0],[1.7333,2.8,1.0],[1.7333,2.8,-1.0],[1.4667,2.8,1.0],[1.4667,2.8,-1.0],[1.4667,2.8,1.0],[1.7333,2.8,-1.0],[1.4667,2.8,-1.0],[1.7333,2.8,-1.0],[1.7333,2.8,1.0],[1.4667,2.8,1.0],[1.7333,2.8,1.0],[1.7333,2.8,-1.0],[1.4667,2.8,1.0],[1.4667,2.8,-1.0],[1.4667,2.6,1.0],[1.4667,2.6,-1.0],[1.4667,2.6,1.0],[1.4667,2.8,-1.0],[1.4667,2.6,-1.0],[1.4667,2.8,-1.0],[1.4667,2.8,1.0],[1.4667,2.6,1.0],[1.4667,2.8,1.0],[1.4667,2.8,-1.0],[1.4667,2.6,1.0],[1.4667,2.6,-1.0],[1.2,2.6,1.0],[1.2,2.6,-1.0],[1.2,2.6,1.0],[1.4667,2.6,-1.0],[1.2,2.6,-1.0],[1.4667,2.6,-1.0],[1.4667,2.6,1.0],[1.2,2.6,1.0],[1.4667,2.6,1.0],[1.4667,2.6,-1.0],[1.2,2.6,1.0],[1.2,2.6,-1.0],[1.2,2.4,1.0],[1.2,2.4,-1.0],[1.2,2.4,1.0],[1.2,2.6,-1.0],[1.2,2.4,-1.0],[1.2,2.6,-1.0],[1.2,2.6,1.0],[1.2,2.4,1.0],[1.2,2.6,1.0],[1.2,2.6,-1.0],[1.2,2.4,1.0],[1.2,2.4,-1.0],[0.9333,2.4,1.0],[0.9333,2.4,-1.0],[0.9333,2.4,1.0],[1.2,2.4,-1.0],[0.9333,2.4,-1.0],[1.2,2.4,-1.0],[1.2,2.4,1.0],[0.9333,2.4,1.0],[1.2,2.4,1.0],[1.2,2.4,-1.0],[0.9333,2.4,1.0],[0.9333,2.4,-1.0],[0.9333,2.2,1.0],[0.9333,2.2,-1.0],[0.9333,2.2,1.0],[0.9333,2.4,-1.0],[0.9333,2.2,-1.0],[0.9333,2.4,-1.0],[0.9333,2.4,1.0],[0.9333,2.2,1.0],[0.9333,2.4,1.0],[0.9333,2.4,-1.0],[0.9333,2.2,1.0],[0.9333,2.2,-1.0],[0.6667,2.2,1.0],[0.6667,2.2,-1.0],[0.6667,2.2,1.0],[0.9333,2.2,-1.0],[0.6667,2.2,-1.0],[0.9333,2.2,-1.0],[0.9333,2.2,1.0],[0.6667,2.2,1.0],[0.9333,2.2,1.0],[0.9333,2.2,-1.0],[0.6667,2.2,1.0],[0.6667,2.2,-1.0],[0.6667,2.0,1.0],[0.6667,2.0,-1.0],[0.6667,2.0,1.0],[0.6667,2.2,-1.0],[0.6667,2.0,-1.0],[0.6667,2.2,-1.0],[0.6667,2.2,1.0],[0.6667,2.0,1.0],[0.6667,2.2,1.0],[0.6667,2.2,-1.0],[0.6667,2.0,1.0],[0.6667,2.0,-1.0],[0.4,2.0,1.0],[0.4,2.0,-1.0],[0.4,2.0,1.0],[0.6667,2.0,-1.0],[0.4,2.0,-1.0],[0.6667,2.0,-1.0],[0.6667,2.0,1.0],[0.4,2.0,1.0],[0.6667,2.0,1.0],[0.6667,2.0,-1.0],[0.4,2.0,1.0],[0.4,2.0,-1.0],[0.4,1.8,1.0],[0.4,1.8,-1.0],[0.4,1.8,1.0],[0.4,2.0,-1.0],[0.4,1.8,-1.0],[0.4,2.0,-1.0],[0.4,2.0,1.0],[0.4,1.8,1.0],[0.4,2.0,1.0],[0.4,2.0,-1.0],[0.4,1.8,1.0],[0.4,1.8,-1.0],[0.1333,1.8,1.0],[0.1333,1.8,-1.0],[0.1333,1.8,1.0],[0.4,1.8,-1.0],[0.1333,1.8,-1.0],[0.4,1.8,-1.0],[0.4,1.8,1.0],[0.1333,1.8,1.0],[0.4,1.8,1.0],[0.4,1.8,-1.0],[0.1333,1.8,1.0],[0.1333,1.8,-1.0],[0.1333,1.6,1.0],[0.1333,1.6,-1.0],[0.1333,1.6,1.0],[0.1333,1.8,-1.0],[0.1333,1.6,-1.0],[0.1333,1.8,-1.0],[0.1333,1.8,1.0],[0.1333,1.6,1.0],[0.1333,1.8,1.0],[0.1333,1.8,-1.0],[0.1333,1.6,1.0],[0.1333,1.6,-1.0],[-0.1333,1.6,1.0],[-0.1333,1.6,-1.0],[-0.1333,1.6,1.0],[0.1333,1.6,-1.0],[-0.1333,1.6,-1.0],[0.1333,1.6,-1.0],[0.1333,1.6,1.0],[-0.1333,1.6,1.0],[0.1333,1.6,1.0],[0.1333,1.6,-1.0],[-0.1333,1.6,1.0],[-0.1333,1.6,-1.0],[-0.1333,1.4,1.0],[-0.1333,1.4,-1.0],[-0.1333,1.4,1.0],[-0.1333,1.6,-1.0],[-0.1333,1.4,-1.0],[-0.1333,1.6,-1.0],[-0.1333,1.6,1.0],[-0.1333,1.4,1.0],[-0.1333,1.6,1.0],[-0.1333,1.6,-1.0],[-0.1333,1.4,1.0],[-0.1333,1.4,-1.0],[-0.4,1.4,1.0],[-0.4,1.4,-1.0],[-0.4,1.4,1.0],[-0.1333,1.4,-1.0],[-0.4,1.4,-1.0],[-0.1333,1.4,-1.0],[-0.1333,1.4,1.0],[-0.4,1.4,1.0],[-0.1333,1.4,1.0],[-0.1333,1.4,-1.0],[-0.4,1.4,1.0],[-0.4,1.4,-1.0],[-0.4,1.2,1.0],[-0.4,1.2,-1.0],[-0.4,1.2,1.0],[-0.4,1.4,-1.0],[-0.4,1.2,-1.0],[-0.4,1.4,-1.0],[-0.4,1.4,1.0],[-0.4,1.2,1.0],[-0.4,1.4,1.0],[-0.4,1.4,-1.0],[-0.4,1.2,1.0],[-0.4,1.2,-1.0],[-0.6667,1.2,1.0],[-0.6667,1.2,-1.0],[-0.6667,1.2,1.0],[-0.4,1.2,-1.0],[-0.6667,1.2,-1.0],[-0.4,1.2,-1.0],[-0.4,1.2,1.0],[-0.6667,1.2,1.0],[-0.4,1.2,1.0],[-0.4,1.2,-1.0],[-0.6667,1.2,1.0],[-0.6667,1.2,-1.0],[-0.6667,1.0,1.0],[-0.6667,1.0,-1.0],[-0.6667,1.0,1.0],[-0.6667,1.2,-1.0],[-0.6667,1.0,-1.0],[-0.6667,1.2,-1.0],[-0.6667,1.2,1.0],[-0.6667,1.0,1.0],[-0.6667,1.2,1.0],[-0.6667,1.2,-1.0],[-0.6667,1.0,1.0],[-0.6667,1.0,-1.0],[-0.9333,1.0,1.0],[-0.9333,1.0,-1.0],[-0.9333,1.0,1.0],[-0.6667,1.0,-1.0],[-0.9333,1.0,-1.0],[-0.6667,1.0,-1.0],[-0.6667,1.0,1.0],[-0.9333,1.0,1.0],[-0.6667,1.0,1.0],[-0.6667,1.0,-1.0],[-0.9333,1.0,1.0],[-0.9333,1.0,-1.0],[-0.9333,0.8,1.0],[-0.9333,0.8,-1.0],[-0.9333,0.8,1.0],[-0.9333,1.0,-1.0],[-0.9333,0.8,-1.0],[-0.9333,1.0,-1.0],[-0.9333,1.0,1.0],[-0.9333,0.8,1.0],[-0.9333,1.0,1.0],[-0.9333,1.0,-1.0],[-0.9333,0.8,1.0],[-0.9333,0.8,-1.0],[-1.2,0.8,1.0],[-1.2,0.8,-1.0],[-1.2,0.8,1.0],[-0.9333,0.8,-1.0],[-1.2,0.8,-1.0],[-0.9333,0.8,-1.0],[-0.9333,0.8,1.0],[-1.2,0.8,1.0],[-0.9333,0.8,1.0],[-0.9333,0.8,-1.0],[-1.2,0.8,1.0],[-1.2,0.8,-1.0],[-1.2,0.6,1.0],[-1.2,0.6,-1.0],[-1.2,0.6,1.0],[-1.2,0.8,-1.0],[-1.2,0.6,-1.0],[-1.2,0.8,-1.0],[-1.2,0.8,1.0],[-1.2,0.6,1.0],[-1.2,0.8,1.0],[-1.2,0.8,-1.0],[-1.2,0.6,1.0],[-1.2,0.6,-1.0],[-1.4667,0.6,1.0],[-1.4667,0.6,-1.0],[-1.4667,0.6,1.0],[-1.2,0.6,-1.0],[-1.4667,0.6,-1.0],[-1.2,0.6,-1.0],[-1.2,0.6,1.0],[-1.4667,0.6,1.0],[-1.2,0.6,1.0],[-1.2,0.6,-1.0],[-1.4667,0.6,1.0],[-1.4667,0.6,-1.0],[-1.4667,0.4,1.0],[-1.4667,0.4,-1.0],[-1.4667,0.4,1.0],[-1.4667,0.6,-1.0],[-1.4667,0.4,-1.0],[-1.4667,0.6,-1.0],[-1.4667,0.6,1.0],[-1.4667,0.4,1.0],[-1.4667,0.6,1.0],[-1.4667,0.6,-1.0],[-1.4667,0.4,1.0],[-1.4667,0.4,-1.0],[-1.7333,0.4,1.0],[-1.7333,0.4,-1.0],[-1.7333,0.4,1.0],[-1.4667,0.4,-1.0],[-1.7333,0.4,-1.0],[-1.4667,0.4,-1.0],[-1.4667,0.4,1.0],[-1.7333,0.4,1.0],[-1.4667,0.4,1.0],[-1.4667,0.4,-1.0],[-1.7333,0.4,1.0],[-1.7333,0.4,-1.0],[-1.7333,0.2,1.0],[-1.7333,0.2,-1.0],[-1.7333,0.2,1.0],[-1.7333,0.4,-1.0],[-1.7333,0.2,-1.0],[-1.7333,0.4,-1.0],[-1.7333,0.4,1.0],[-1.7333,0.2,1.0],[-1.7333,0.4,1.0],[-1.7333,0.4,-1.0],[-1.7333,0.2,1.0],[-1.7333,0.2,-1.0],[-2.0,0.2,1.0],[-2.0,0.2,-1.0],[-2.0,0.2,1.0],[-1.7333,0.2,-1.0],[-2.0,0.2,-1.0],[-1.7333,0.2,-1.0],[-1.7333,0.2,1.0],[-2.0,0.2,1.0],[-1.7333,0.2,1.0],[-1.7333,0.2,-1.0],[-2.0,0.2,1.0],[-2.0,0.2,-1.0],[-2.0,0.0,1.0],[-2.0,0.0,-1.0],[-2.0,0.0,1.0],[-2.0,0.2,-1.0],[-2.0,0.0,-1.0],[-2.0,0.2,-1.0],[-2.0,0.2,1.0],[-2.0,0.0,1.0],[-2.0,0.2,1.0],[-2.0,0.2,-1.0],[-2.0,0.0,1.0]]} \ No newline at end of file diff --git a/library/kollisions/bau_wand.json b/library/kollisions/bau_wand.json new file mode 100644 index 0000000..5bf58c6 --- /dev/null +++ b/library/kollisions/bau_wand.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,3.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":24,"oid":"bau_wand","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_wand.glb","vertices":[[-1.0,3.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,3.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,-0.1],[-1.0,3.0,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,3.0,0.1],[1.0,3.0,0.1],[-1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,0.1],[-1.0,3.0,-0.1],[1.0,0.0,-0.1],[1.0,3.0,-0.1],[1.0,0.0,-0.1],[-1.0,3.0,-0.1],[1.0,3.0,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[-1.0,0.0,0.1],[-1.0,3.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[1.0,0.0,-0.1],[1.0,3.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[1.0,3.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1]]} \ No newline at end of file diff --git a/library/kollisions/bau_wand_fenster.json b/library/kollisions/bau_wand_fenster.json new file mode 100644 index 0000000..76e02c9 --- /dev/null +++ b/library/kollisions/bau_wand_fenster.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,3.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":96,"oid":"bau_wand_fenster","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_wand_fenster.glb","vertices":[[-0.6,0.0,0.1],[-1.0,1.1,0.1],[-0.6,1.1,0.1],[-1.0,1.1,0.1],[-0.6,0.0,0.1],[-0.6,1.1,0.1],[-1.0,1.1,-0.1],[-0.6,0.0,-0.1],[-0.6,1.1,-0.1],[-0.6,0.0,-0.1],[-1.0,1.1,-0.1],[-0.6,1.1,-0.1],[-1.0,1.1,0.1],[-0.6,0.0,0.1],[-1.0,0.0,0.1],[-0.6,0.0,0.1],[-1.0,1.1,0.1],[-1.0,0.0,0.1],[-0.6,0.0,-0.1],[-1.0,1.1,-0.1],[-1.0,0.0,-0.1],[-1.0,1.1,-0.1],[-0.6,0.0,-0.1],[-1.0,0.0,-0.1],[-0.6,1.1,0.1],[-1.0,2.3,0.1],[-0.6,2.3,0.1],[-1.0,2.3,0.1],[-0.6,1.1,0.1],[-0.6,2.3,0.1],[-1.0,2.3,-0.1],[-0.6,1.1,-0.1],[-0.6,2.3,-0.1],[-0.6,1.1,-0.1],[-1.0,2.3,-0.1],[-0.6,2.3,-0.1],[-1.0,2.3,0.1],[-0.6,1.1,0.1],[-1.0,1.1,0.1],[-0.6,1.1,0.1],[-1.0,2.3,0.1],[-1.0,1.1,0.1],[-0.6,1.1,-0.1],[-1.0,2.3,-0.1],[-1.0,1.1,-0.1],[-1.0,2.3,-0.1],[-0.6,1.1,-0.1],[-1.0,1.1,-0.1],[-0.6,2.3,0.1],[-1.0,3.0,0.1],[-0.6,3.0,0.1],[-1.0,3.0,0.1],[-0.6,2.3,0.1],[-0.6,3.0,0.1],[-1.0,3.0,-0.1],[-0.6,2.3,-0.1],[-0.6,3.0,-0.1],[-0.6,2.3,-0.1],[-1.0,3.0,-0.1],[-0.6,3.0,-0.1],[-1.0,3.0,0.1],[-0.6,2.3,0.1],[-1.0,2.3,0.1],[-0.6,2.3,0.1],[-1.0,3.0,0.1],[-1.0,2.3,0.1],[-0.6,2.3,-0.1],[-1.0,3.0,-0.1],[-1.0,2.3,-0.1],[-1.0,3.0,-0.1],[-0.6,2.3,-0.1],[-1.0,2.3,-0.1],[0.6,0.0,0.1],[-0.6,1.1,0.1],[0.6,1.1,0.1],[-0.6,1.1,0.1],[0.6,0.0,0.1],[0.6,1.1,0.1],[-0.6,1.1,-0.1],[0.6,0.0,-0.1],[0.6,1.1,-0.1],[0.6,0.0,-0.1],[-0.6,1.1,-0.1],[0.6,1.1,-0.1],[-0.6,1.1,0.1],[0.6,0.0,0.1],[-0.6,0.0,0.1],[0.6,0.0,0.1],[-0.6,1.1,0.1],[-0.6,0.0,0.1],[0.6,0.0,-0.1],[-0.6,1.1,-0.1],[-0.6,0.0,-0.1],[-0.6,1.1,-0.1],[0.6,0.0,-0.1],[-0.6,0.0,-0.1],[0.6,2.3,0.1],[-0.6,3.0,0.1],[0.6,3.0,0.1],[-0.6,3.0,0.1],[0.6,2.3,0.1],[0.6,3.0,0.1],[-0.6,3.0,-0.1],[0.6,2.3,-0.1],[0.6,3.0,-0.1],[0.6,2.3,-0.1],[-0.6,3.0,-0.1],[0.6,3.0,-0.1],[-0.6,3.0,0.1],[0.6,2.3,0.1],[-0.6,2.3,0.1],[0.6,2.3,0.1],[-0.6,3.0,0.1],[-0.6,2.3,0.1],[0.6,2.3,-0.1],[-0.6,3.0,-0.1],[-0.6,2.3,-0.1],[-0.6,3.0,-0.1],[0.6,2.3,-0.1],[-0.6,2.3,-0.1],[1.0,0.0,0.1],[0.6,1.1,0.1],[1.0,1.1,0.1],[0.6,1.1,0.1],[1.0,0.0,0.1],[1.0,1.1,0.1],[0.6,1.1,-0.1],[1.0,0.0,-0.1],[1.0,1.1,-0.1],[1.0,0.0,-0.1],[0.6,1.1,-0.1],[1.0,1.1,-0.1],[0.6,1.1,0.1],[1.0,0.0,0.1],[0.6,0.0,0.1],[1.0,0.0,0.1],[0.6,1.1,0.1],[0.6,0.0,0.1],[1.0,0.0,-0.1],[0.6,1.1,-0.1],[0.6,0.0,-0.1],[0.6,1.1,-0.1],[1.0,0.0,-0.1],[0.6,0.0,-0.1],[1.0,1.1,0.1],[0.6,2.3,0.1],[1.0,2.3,0.1],[0.6,2.3,0.1],[1.0,1.1,0.1],[1.0,2.3,0.1],[0.6,2.3,-0.1],[1.0,1.1,-0.1],[1.0,2.3,-0.1],[1.0,1.1,-0.1],[0.6,2.3,-0.1],[1.0,2.3,-0.1],[0.6,2.3,0.1],[1.0,1.1,0.1],[0.6,1.1,0.1],[1.0,1.1,0.1],[0.6,2.3,0.1],[0.6,1.1,0.1],[1.0,1.1,-0.1],[0.6,2.3,-0.1],[0.6,1.1,-0.1],[0.6,2.3,-0.1],[1.0,1.1,-0.1],[0.6,1.1,-0.1],[1.0,2.3,0.1],[0.6,3.0,0.1],[1.0,3.0,0.1],[0.6,3.0,0.1],[1.0,2.3,0.1],[1.0,3.0,0.1],[0.6,3.0,-0.1],[1.0,2.3,-0.1],[1.0,3.0,-0.1],[1.0,2.3,-0.1],[0.6,3.0,-0.1],[1.0,3.0,-0.1],[0.6,3.0,0.1],[1.0,2.3,0.1],[0.6,2.3,0.1],[1.0,2.3,0.1],[0.6,3.0,0.1],[0.6,2.3,0.1],[1.0,2.3,-0.1],[0.6,3.0,-0.1],[0.6,2.3,-0.1],[0.6,3.0,-0.1],[1.0,2.3,-0.1],[0.6,2.3,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[-1.0,0.0,0.1],[-1.0,3.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[1.0,0.0,-0.1],[1.0,3.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[1.0,3.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[0.6,2.3,0.1],[-0.6,2.3,-0.1],[0.6,2.3,-0.1],[-0.6,2.3,-0.1],[0.6,2.3,0.1],[0.6,2.3,-0.1],[0.6,2.3,0.1],[-0.6,2.3,0.1],[-0.6,2.3,-0.1],[-0.6,2.3,0.1],[0.6,2.3,0.1],[-0.6,2.3,-0.1],[-0.6,2.3,0.1],[-0.6,1.1,-0.1],[-0.6,2.3,-0.1],[-0.6,1.1,-0.1],[-0.6,2.3,0.1],[-0.6,2.3,-0.1],[-0.6,2.3,0.1],[-0.6,1.1,0.1],[-0.6,1.1,-0.1],[-0.6,1.1,0.1],[-0.6,2.3,0.1],[-0.6,1.1,-0.1],[-0.6,1.1,0.1],[0.6,1.1,-0.1],[-0.6,1.1,-0.1],[0.6,1.1,-0.1],[-0.6,1.1,0.1],[-0.6,1.1,-0.1],[-0.6,1.1,0.1],[0.6,1.1,0.1],[0.6,1.1,-0.1],[0.6,1.1,0.1],[-0.6,1.1,0.1],[0.6,1.1,-0.1],[0.6,1.1,0.1],[0.6,2.3,-0.1],[0.6,1.1,-0.1],[0.6,2.3,-0.1],[0.6,1.1,0.1],[0.6,1.1,-0.1],[0.6,1.1,0.1],[0.6,2.3,0.1],[0.6,2.3,-0.1],[0.6,2.3,0.1],[0.6,1.1,0.1],[0.6,2.3,-0.1]]} \ No newline at end of file diff --git a/library/kollisions/bau_wand_scharte.json b/library/kollisions/bau_wand_scharte.json new file mode 100644 index 0000000..853c6a3 --- /dev/null +++ b/library/kollisions/bau_wand_scharte.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,3.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":96,"oid":"bau_wand_scharte","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_wand_scharte.glb","vertices":[[-0.5,0.0,0.1],[-1.0,1.3,0.1],[-0.5,1.3,0.1],[-1.0,1.3,0.1],[-0.5,0.0,0.1],[-0.5,1.3,0.1],[-1.0,1.3,-0.1],[-0.5,0.0,-0.1],[-0.5,1.3,-0.1],[-0.5,0.0,-0.1],[-1.0,1.3,-0.1],[-0.5,1.3,-0.1],[-1.0,1.3,0.1],[-0.5,0.0,0.1],[-1.0,0.0,0.1],[-0.5,0.0,0.1],[-1.0,1.3,0.1],[-1.0,0.0,0.1],[-0.5,0.0,-0.1],[-1.0,1.3,-0.1],[-1.0,0.0,-0.1],[-1.0,1.3,-0.1],[-0.5,0.0,-0.1],[-1.0,0.0,-0.1],[-0.5,1.3,0.1],[-1.0,1.7,0.1],[-0.5,1.7,0.1],[-1.0,1.7,0.1],[-0.5,1.3,0.1],[-0.5,1.7,0.1],[-1.0,1.7,-0.1],[-0.5,1.3,-0.1],[-0.5,1.7,-0.1],[-0.5,1.3,-0.1],[-1.0,1.7,-0.1],[-0.5,1.7,-0.1],[-1.0,1.7,0.1],[-0.5,1.3,0.1],[-1.0,1.3,0.1],[-0.5,1.3,0.1],[-1.0,1.7,0.1],[-1.0,1.3,0.1],[-0.5,1.3,-0.1],[-1.0,1.7,-0.1],[-1.0,1.3,-0.1],[-1.0,1.7,-0.1],[-0.5,1.3,-0.1],[-1.0,1.3,-0.1],[-0.5,1.7,0.1],[-1.0,3.0,0.1],[-0.5,3.0,0.1],[-1.0,3.0,0.1],[-0.5,1.7,0.1],[-0.5,3.0,0.1],[-1.0,3.0,-0.1],[-0.5,1.7,-0.1],[-0.5,3.0,-0.1],[-0.5,1.7,-0.1],[-1.0,3.0,-0.1],[-0.5,3.0,-0.1],[-1.0,3.0,0.1],[-0.5,1.7,0.1],[-1.0,1.7,0.1],[-0.5,1.7,0.1],[-1.0,3.0,0.1],[-1.0,1.7,0.1],[-0.5,1.7,-0.1],[-1.0,3.0,-0.1],[-1.0,1.7,-0.1],[-1.0,3.0,-0.1],[-0.5,1.7,-0.1],[-1.0,1.7,-0.1],[0.5,0.0,0.1],[-0.5,1.3,0.1],[0.5,1.3,0.1],[-0.5,1.3,0.1],[0.5,0.0,0.1],[0.5,1.3,0.1],[-0.5,1.3,-0.1],[0.5,0.0,-0.1],[0.5,1.3,-0.1],[0.5,0.0,-0.1],[-0.5,1.3,-0.1],[0.5,1.3,-0.1],[-0.5,1.3,0.1],[0.5,0.0,0.1],[-0.5,0.0,0.1],[0.5,0.0,0.1],[-0.5,1.3,0.1],[-0.5,0.0,0.1],[0.5,0.0,-0.1],[-0.5,1.3,-0.1],[-0.5,0.0,-0.1],[-0.5,1.3,-0.1],[0.5,0.0,-0.1],[-0.5,0.0,-0.1],[0.5,1.7,0.1],[-0.5,3.0,0.1],[0.5,3.0,0.1],[-0.5,3.0,0.1],[0.5,1.7,0.1],[0.5,3.0,0.1],[-0.5,3.0,-0.1],[0.5,1.7,-0.1],[0.5,3.0,-0.1],[0.5,1.7,-0.1],[-0.5,3.0,-0.1],[0.5,3.0,-0.1],[-0.5,3.0,0.1],[0.5,1.7,0.1],[-0.5,1.7,0.1],[0.5,1.7,0.1],[-0.5,3.0,0.1],[-0.5,1.7,0.1],[0.5,1.7,-0.1],[-0.5,3.0,-0.1],[-0.5,1.7,-0.1],[-0.5,3.0,-0.1],[0.5,1.7,-0.1],[-0.5,1.7,-0.1],[1.0,0.0,0.1],[0.5,1.3,0.1],[1.0,1.3,0.1],[0.5,1.3,0.1],[1.0,0.0,0.1],[1.0,1.3,0.1],[0.5,1.3,-0.1],[1.0,0.0,-0.1],[1.0,1.3,-0.1],[1.0,0.0,-0.1],[0.5,1.3,-0.1],[1.0,1.3,-0.1],[0.5,1.3,0.1],[1.0,0.0,0.1],[0.5,0.0,0.1],[1.0,0.0,0.1],[0.5,1.3,0.1],[0.5,0.0,0.1],[1.0,0.0,-0.1],[0.5,1.3,-0.1],[0.5,0.0,-0.1],[0.5,1.3,-0.1],[1.0,0.0,-0.1],[0.5,0.0,-0.1],[1.0,1.3,0.1],[0.5,1.7,0.1],[1.0,1.7,0.1],[0.5,1.7,0.1],[1.0,1.3,0.1],[1.0,1.7,0.1],[0.5,1.7,-0.1],[1.0,1.3,-0.1],[1.0,1.7,-0.1],[1.0,1.3,-0.1],[0.5,1.7,-0.1],[1.0,1.7,-0.1],[0.5,1.7,0.1],[1.0,1.3,0.1],[0.5,1.3,0.1],[1.0,1.3,0.1],[0.5,1.7,0.1],[0.5,1.3,0.1],[1.0,1.3,-0.1],[0.5,1.7,-0.1],[0.5,1.3,-0.1],[0.5,1.7,-0.1],[1.0,1.3,-0.1],[0.5,1.3,-0.1],[1.0,1.7,0.1],[0.5,3.0,0.1],[1.0,3.0,0.1],[0.5,3.0,0.1],[1.0,1.7,0.1],[1.0,3.0,0.1],[0.5,3.0,-0.1],[1.0,1.7,-0.1],[1.0,3.0,-0.1],[1.0,1.7,-0.1],[0.5,3.0,-0.1],[1.0,3.0,-0.1],[0.5,3.0,0.1],[1.0,1.7,0.1],[0.5,1.7,0.1],[1.0,1.7,0.1],[0.5,3.0,0.1],[0.5,1.7,0.1],[1.0,1.7,-0.1],[0.5,3.0,-0.1],[0.5,1.7,-0.1],[0.5,3.0,-0.1],[1.0,1.7,-0.1],[0.5,1.7,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[-1.0,0.0,0.1],[-1.0,3.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[1.0,0.0,-0.1],[1.0,3.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[1.0,3.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[0.5,1.7,0.1],[-0.5,1.7,-0.1],[0.5,1.7,-0.1],[-0.5,1.7,-0.1],[0.5,1.7,0.1],[0.5,1.7,-0.1],[0.5,1.7,0.1],[-0.5,1.7,0.1],[-0.5,1.7,-0.1],[-0.5,1.7,0.1],[0.5,1.7,0.1],[-0.5,1.7,-0.1],[-0.5,1.7,0.1],[-0.5,1.3,-0.1],[-0.5,1.7,-0.1],[-0.5,1.3,-0.1],[-0.5,1.7,0.1],[-0.5,1.7,-0.1],[-0.5,1.7,0.1],[-0.5,1.3,0.1],[-0.5,1.3,-0.1],[-0.5,1.3,0.1],[-0.5,1.7,0.1],[-0.5,1.3,-0.1],[-0.5,1.3,0.1],[0.5,1.3,-0.1],[-0.5,1.3,-0.1],[0.5,1.3,-0.1],[-0.5,1.3,0.1],[-0.5,1.3,-0.1],[-0.5,1.3,0.1],[0.5,1.3,0.1],[0.5,1.3,-0.1],[0.5,1.3,0.1],[-0.5,1.3,0.1],[0.5,1.3,-0.1],[0.5,1.3,0.1],[0.5,1.7,-0.1],[0.5,1.3,-0.1],[0.5,1.7,-0.1],[0.5,1.3,0.1],[0.5,1.3,-0.1],[0.5,1.3,0.1],[0.5,1.7,0.1],[0.5,1.7,-0.1],[0.5,1.7,0.1],[0.5,1.3,0.1],[0.5,1.7,-0.1]]} \ No newline at end of file diff --git a/library/kollisions/bau_wand_tuer.json b/library/kollisions/bau_wand_tuer.json new file mode 100644 index 0000000..defe884 --- /dev/null +++ b/library/kollisions/bau_wand_tuer.json @@ -0,0 +1 @@ +{"aabb_max":[1.0,3.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":72,"oid":"bau_wand_tuer","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_wand_tuer.glb","vertices":[[-0.6,0.0,0.1],[-1.0,2.2,0.1],[-0.6,2.2,0.1],[-1.0,2.2,0.1],[-0.6,0.0,0.1],[-0.6,2.2,0.1],[-1.0,2.2,-0.1],[-0.6,0.0,-0.1],[-0.6,2.2,-0.1],[-0.6,0.0,-0.1],[-1.0,2.2,-0.1],[-0.6,2.2,-0.1],[-1.0,2.2,0.1],[-0.6,0.0,0.1],[-1.0,0.0,0.1],[-0.6,0.0,0.1],[-1.0,2.2,0.1],[-1.0,0.0,0.1],[-0.6,0.0,-0.1],[-1.0,2.2,-0.1],[-1.0,0.0,-0.1],[-1.0,2.2,-0.1],[-0.6,0.0,-0.1],[-1.0,0.0,-0.1],[-0.6,2.2,0.1],[-1.0,3.0,0.1],[-0.6,3.0,0.1],[-1.0,3.0,0.1],[-0.6,2.2,0.1],[-0.6,3.0,0.1],[-1.0,3.0,-0.1],[-0.6,2.2,-0.1],[-0.6,3.0,-0.1],[-0.6,2.2,-0.1],[-1.0,3.0,-0.1],[-0.6,3.0,-0.1],[-1.0,3.0,0.1],[-0.6,2.2,0.1],[-1.0,2.2,0.1],[-0.6,2.2,0.1],[-1.0,3.0,0.1],[-1.0,2.2,0.1],[-0.6,2.2,-0.1],[-1.0,3.0,-0.1],[-1.0,2.2,-0.1],[-1.0,3.0,-0.1],[-0.6,2.2,-0.1],[-1.0,2.2,-0.1],[0.6,2.2,0.1],[-0.6,3.0,0.1],[0.6,3.0,0.1],[-0.6,3.0,0.1],[0.6,2.2,0.1],[0.6,3.0,0.1],[-0.6,3.0,-0.1],[0.6,2.2,-0.1],[0.6,3.0,-0.1],[0.6,2.2,-0.1],[-0.6,3.0,-0.1],[0.6,3.0,-0.1],[-0.6,3.0,0.1],[0.6,2.2,0.1],[-0.6,2.2,0.1],[0.6,2.2,0.1],[-0.6,3.0,0.1],[-0.6,2.2,0.1],[0.6,2.2,-0.1],[-0.6,3.0,-0.1],[-0.6,2.2,-0.1],[-0.6,3.0,-0.1],[0.6,2.2,-0.1],[-0.6,2.2,-0.1],[1.0,0.0,0.1],[0.6,2.2,0.1],[1.0,2.2,0.1],[0.6,2.2,0.1],[1.0,0.0,0.1],[1.0,2.2,0.1],[0.6,2.2,-0.1],[1.0,0.0,-0.1],[1.0,2.2,-0.1],[1.0,0.0,-0.1],[0.6,2.2,-0.1],[1.0,2.2,-0.1],[0.6,2.2,0.1],[1.0,0.0,0.1],[0.6,0.0,0.1],[1.0,0.0,0.1],[0.6,2.2,0.1],[0.6,0.0,0.1],[1.0,0.0,-0.1],[0.6,2.2,-0.1],[0.6,0.0,-0.1],[0.6,2.2,-0.1],[1.0,0.0,-0.1],[0.6,0.0,-0.1],[1.0,2.2,0.1],[0.6,3.0,0.1],[1.0,3.0,0.1],[0.6,3.0,0.1],[1.0,2.2,0.1],[1.0,3.0,0.1],[0.6,3.0,-0.1],[1.0,2.2,-0.1],[1.0,3.0,-0.1],[1.0,2.2,-0.1],[0.6,3.0,-0.1],[1.0,3.0,-0.1],[0.6,3.0,0.1],[1.0,2.2,0.1],[0.6,2.2,0.1],[1.0,2.2,0.1],[0.6,3.0,0.1],[0.6,2.2,0.1],[1.0,2.2,-0.1],[0.6,3.0,-0.1],[0.6,2.2,-0.1],[0.6,3.0,-0.1],[1.0,2.2,-0.1],[0.6,2.2,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[-1.0,0.0,0.1],[-1.0,3.0,0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[-1.0,0.0,-0.1],[-1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[1.0,0.0,-0.1],[1.0,3.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,0.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,0.1],[-1.0,3.0,0.1],[-1.0,3.0,-0.1],[1.0,3.0,-0.1],[-1.0,3.0,-0.1],[-1.0,3.0,0.1],[1.0,3.0,-0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,0.0,-0.1],[-1.0,0.0,0.1],[1.0,0.0,0.1],[-1.0,0.0,0.1],[-1.0,0.0,-0.1],[1.0,0.0,0.1],[0.6,2.2,0.1],[-0.6,2.2,-0.1],[0.6,2.2,-0.1],[-0.6,2.2,-0.1],[0.6,2.2,0.1],[0.6,2.2,-0.1],[0.6,2.2,0.1],[-0.6,2.2,0.1],[-0.6,2.2,-0.1],[-0.6,2.2,0.1],[0.6,2.2,0.1],[-0.6,2.2,-0.1],[-0.6,2.2,0.1],[-0.6,0.0,-0.1],[-0.6,2.2,-0.1],[-0.6,0.0,-0.1],[-0.6,2.2,0.1],[-0.6,2.2,-0.1],[-0.6,2.2,0.1],[-0.6,0.0,0.1],[-0.6,0.0,-0.1],[-0.6,0.0,0.1],[-0.6,2.2,0.1],[-0.6,0.0,-0.1],[-0.6,0.0,0.1],[0.6,0.0,-0.1],[-0.6,0.0,-0.1],[0.6,0.0,-0.1],[-0.6,0.0,0.1],[-0.6,0.0,-0.1],[-0.6,0.0,0.1],[0.6,0.0,0.1],[0.6,0.0,-0.1],[0.6,0.0,0.1],[-0.6,0.0,0.1],[0.6,0.0,-0.1],[0.6,0.0,0.1],[0.6,2.2,-0.1],[0.6,0.0,-0.1],[0.6,2.2,-0.1],[0.6,0.0,0.1],[0.6,0.0,-0.1],[0.6,0.0,0.1],[0.6,2.2,0.1],[0.6,2.2,-0.1],[0.6,2.2,0.1],[0.6,0.0,0.1],[0.6,2.2,-0.1]]} \ No newline at end of file diff --git a/library/objects.json b/library/objects.json index 5467bd8..302d580 100644 --- a/library/objects.json +++ b/library/objects.json @@ -628,6 +628,186 @@ ] } }, + "bau_boden": { + "poly": 24, + "collision": { + "quelle": "kollisions/bau_boden.json", + "shape": "trimesh", + "size": [ + 2.0, + 0.2, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_boden.glb", + "kategorie": "bauteile", + "name_de": "Boden / Decke" + }, + "bau_bruestung": { + "poly": 24, + "collision": { + "quelle": "kollisions/bau_bruestung.json", + "shape": "trimesh", + "size": [ + 2.0, + 1.1, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_bruestung.glb", + "kategorie": "bauteile", + "name_de": "Brüstung" + }, + "bau_dach_giebel": { + "poly": 48, + "collision": { + "quelle": "kollisions/bau_dach_giebel.json", + "shape": "trimesh", + "size": [ + 2.141, + 1.141, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel.glb", + "kategorie": "bauteile", + "name_de": "Satteldach" + }, + "bau_dach_pyramide": { + "poly": 64, + "collision": { + "quelle": "kollisions/bau_dach_pyramide.json", + "shape": "trimesh", + "size": [ + 2.141, + 1.141, + 2.141 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_dach_pyramide.glb", + "kategorie": "bauteile", + "name_de": "Pyramidendach" + }, + "bau_fundament": { + "poly": 24, + "collision": { + "quelle": "kollisions/bau_fundament.json", + "shape": "trimesh", + "size": [ + 2.0, + 0.2, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_fundament.glb", + "kategorie": "bauteile", + "name_de": "Fundament" + }, + "bau_giebelwand": { + "poly": 16, + "collision": { + "quelle": "kollisions/bau_giebelwand.json", + "shape": "trimesh", + "size": [ + 2.0, + 1.0, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_giebelwand.glb", + "kategorie": "bauteile", + "name_de": "Giebelwand" + }, + "bau_pfeiler": { + "poly": 12, + "collision": { + "quelle": "kollisions/bau_pfeiler.json", + "shape": "trimesh", + "size": [ + 0.3, + 3.0, + 0.3 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_pfeiler.glb", + "kategorie": "bauteile", + "name_de": "Eckpfeiler" + }, + "bau_treppe": { + "poly": 248, + "collision": { + "quelle": "kollisions/bau_treppe.json", + "shape": "trimesh", + "size": [ + 4.0, + 3.0, + 2.0 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_treppe.glb", + "kategorie": "bauteile", + "name_de": "Treppe" + }, + "bau_wand": { + "poly": 24, + "collision": { + "quelle": "kollisions/bau_wand.json", + "shape": "trimesh", + "size": [ + 2.0, + 3.0, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_wand.glb", + "kategorie": "bauteile", + "name_de": "Wand" + }, + "bau_wand_fenster": { + "poly": 96, + "collision": { + "quelle": "kollisions/bau_wand_fenster.json", + "shape": "trimesh", + "size": [ + 2.0, + 3.0, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_wand_fenster.glb", + "kategorie": "bauteile", + "name_de": "Wand mit Fenster" + }, + "bau_wand_scharte": { + "poly": 96, + "collision": { + "quelle": "kollisions/bau_wand_scharte.json", + "shape": "trimesh", + "size": [ + 2.0, + 3.0, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_wand_scharte.glb", + "kategorie": "bauteile", + "name_de": "Wand mit Schießscharte" + }, + "bau_wand_tuer": { + "poly": 72, + "collision": { + "quelle": "kollisions/bau_wand_tuer.json", + "shape": "trimesh", + "size": [ + 2.0, + 3.0, + 0.2 + ] + }, + "glb": "res://assets/objekte/military_desert/bauteile/bau_wand_tuer.glb", + "kategorie": "bauteile", + "name_de": "Wand mit Tür" + }, "baum_kiefer_1": { "poly": 728, "collision": { diff --git a/library/thumbs/bau_boden.png b/library/thumbs/bau_boden.png new file mode 100644 index 0000000..553f004 Binary files /dev/null and b/library/thumbs/bau_boden.png differ diff --git a/library/thumbs/bau_bruestung.png b/library/thumbs/bau_bruestung.png new file mode 100644 index 0000000..ef51bc7 Binary files /dev/null and b/library/thumbs/bau_bruestung.png differ diff --git a/library/thumbs/bau_dach_giebel.png b/library/thumbs/bau_dach_giebel.png new file mode 100644 index 0000000..385910f Binary files /dev/null and b/library/thumbs/bau_dach_giebel.png differ diff --git a/library/thumbs/bau_dach_pyramide.png b/library/thumbs/bau_dach_pyramide.png new file mode 100644 index 0000000..548e550 Binary files /dev/null and b/library/thumbs/bau_dach_pyramide.png differ diff --git a/library/thumbs/bau_fundament.png b/library/thumbs/bau_fundament.png new file mode 100644 index 0000000..6a80337 Binary files /dev/null and b/library/thumbs/bau_fundament.png differ diff --git a/library/thumbs/bau_giebelwand.png b/library/thumbs/bau_giebelwand.png new file mode 100644 index 0000000..c85449d Binary files /dev/null and b/library/thumbs/bau_giebelwand.png differ diff --git a/library/thumbs/bau_pfeiler.png b/library/thumbs/bau_pfeiler.png new file mode 100644 index 0000000..4f8b898 Binary files /dev/null and b/library/thumbs/bau_pfeiler.png differ diff --git a/library/thumbs/bau_treppe.png b/library/thumbs/bau_treppe.png new file mode 100644 index 0000000..e20cf3e Binary files /dev/null and b/library/thumbs/bau_treppe.png differ diff --git a/library/thumbs/bau_wand.png b/library/thumbs/bau_wand.png new file mode 100644 index 0000000..ed18606 Binary files /dev/null and b/library/thumbs/bau_wand.png differ diff --git a/library/thumbs/bau_wand_fenster.png b/library/thumbs/bau_wand_fenster.png new file mode 100644 index 0000000..d154316 Binary files /dev/null and b/library/thumbs/bau_wand_fenster.png differ diff --git a/library/thumbs/bau_wand_scharte.png b/library/thumbs/bau_wand_scharte.png new file mode 100644 index 0000000..9f08d68 Binary files /dev/null and b/library/thumbs/bau_wand_scharte.png differ diff --git a/library/thumbs/bau_wand_tuer.png b/library/thumbs/bau_wand_tuer.png new file mode 100644 index 0000000..39cc930 Binary files /dev/null and b/library/thumbs/bau_wand_tuer.png differ diff --git a/src/editor/bau_katalog.gd b/src/editor/bau_katalog.gd new file mode 100644 index 0000000..9490c55 --- /dev/null +++ b/src/editor/bau_katalog.gd @@ -0,0 +1,307 @@ +extends RefCounted +class_name BauKatalog +## Bauteil-Katalog des Bauen-Modus (Karte "Bauen sauber integrieren", 25.10.). +## +## Ein Bauteil ist ein GANZ NORMALES Bibliotheks-Objekt (`bau_*`, Kategorie +## "bauteile"): gesetzte Teile stehen in doc["objects"], tragen ein +## Gruppen-Feld (= ein Gebäude) und laufen damit durch dieselbe Kette wie jedes +## Objekt — Undo, Kopieren, Löschen, Symmetrie, Speichern, Server-Registry, +## Client-Loader, Trimesh-Kollision. Es gibt keinen Sonderblock "gebaeude" mehr. +## +## Geometrie: die CC0-Runtime des Modular Builder (addons/modular_builder, +## Michel Savadim, v0.2.0 — Lizenz siehe ADDON_LIZENZ.txt). Hier wird sie nur +## PARAMETRIERT (Wandhöhe 3 m, 2-m-Raster) und auf die Fußpunkt-Konvention der +## Bibliothek normalisiert: Unterkante y = 0, XZ um die Mitte zentriert +## (docs/objekt_fragmente.md). Die Farbe ist die Material-Grundfarbe (glTF +## baseColorFactor) — keine Texturen, keine Lizenzfragen, 1 Draw-Call je Teil; +## Editor, Client und Thumbnails laden sie dadurch identisch. +## +## Meshes entstehen aus diesem Katalog (tools/gen_bauteile.gd schreibt GLB + +## Trimesh-JSON + Registry-Fragment), der Editor zeigt die GLBs wie jedes andere +## Objekt. mesh() ist die Quelle für Generator UND Tests. + +const GEO_SCRIPT := "res://addons/modular_builder/modular_piece_geometry.gd" +const DETAIL_SCRIPT := "res://addons/modular_builder/modular_building_detail.gd" +const KATEGORIE := "bauteile" +const PREFIX := "bau_" + +const RASTER_M := 2.0 # Zellgröße (XZ) +const WAND_H := 3.0 # Wandhöhe eines Geschosses +const DICKE_M := 0.2 # Wand-/Plattendicke + +# Geometrie-Typen der Addon-Runtime (PieceType-Enum, Kopie der Werte) +const T_FUNDAMENT := 0 +const T_BODEN := 2 +const T_WAND := 3 +const T_GIEBEL := 5 +const T_DACH_GIEBEL := 6 +const T_DACH_PYRAMIDE := 9 +const T_TREPPE := 10 +const D_FENSTER := 0 +const D_TUER := 1 + +## Snap-Arten (build_snap.gd): +## "zelle" Mitte einer 2×2-Zelle (Fundament, Boden, Dächer) +## "kante" Mitte einer Zellkante (Wände) — Drehung ergibt sich aus der Kante +## "ecke" Zell-Ecke (Pfeiler) +## "treppe" 4×2-m-Fläche über zwei Zellen +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": "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]] + +## Reihenfolge = Anzeige-Reihenfolge (GRUPPEN nutzt dieselben IDs). +## "traeger": darauf darf gestapelt werden (Oberkante zählt als Ebene). +const KINDS := { + "bau_fundament": { + "label": "Fundament", "info": "2×2 m, 20 cm dick — Basis für Wände", "snap": "zelle", "traeger": true, + "typ": T_FUNDAMENT, "punkte": QUADRAT, "farbe": Color(0.42, 0.42, 0.40), + }, + "bau_boden": { + "label": "Boden / Decke", "info": "2×2 m Platte — Fußboden oder Decke fürs nächste Geschoss", + "snap": "zelle", "traeger": true, + "typ": T_BODEN, "punkte": QUADRAT, "farbe": Color(0.50, 0.46, 0.40), + }, + "bau_wand": { + "label": "Wand", "info": "2 m breit, 3 m hoch", "snap": "kante", "traeger": true, + "typ": T_WAND, "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "farbe": Color(0.62, 0.53, 0.38), + }, + "bau_wand_tuer": { + "label": "Wand mit Tür", "info": "Türöffnung 1,2 × 2,2 m — man läuft hindurch", "snap": "kante", "traeger": true, + "typ": T_WAND, "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "farbe": Color(0.62, 0.53, 0.38), + "details": [{"typ": D_TUER, "pos": [1.0, 1.1, 0.0], "w": 1.2, "h": 2.2}], + }, + "bau_wand_fenster": { + "label": "Wand mit Fenster", "info": "Fensteröffnung 1,2 × 1,2 m, Brüstung 1,1 m", "snap": "kante", "traeger": true, + "typ": T_WAND, "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "farbe": Color(0.62, 0.53, 0.38), + "details": [{"typ": D_FENSTER, "pos": [1.0, 1.7, 0.0], "w": 1.2, "h": 1.2}], + }, + "bau_wand_scharte": { + "label": "Wand mit Schießscharte", "info": "Schlitz 1,0 × 0,4 m auf Schulterhöhe", "snap": "kante", "traeger": true, + "typ": T_WAND, "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "farbe": Color(0.62, 0.53, 0.38), + "details": [{"typ": D_FENSTER, "pos": [1.0, 1.5, 0.0], "w": 1.0, "h": 0.4}], + }, + "bau_bruestung": { + "label": "Brüstung", "info": "2 m breit, 1,1 m hoch — Deckung, Dachrand", "snap": "kante", "traeger": true, + "typ": T_WAND, "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "farbe": Color(0.56, 0.48, 0.35), "hoehe": 1.1, + }, + "bau_pfeiler": { + "label": "Eckpfeiler", "info": "Schließt die Ecke, wo zwei Wände zusammenstoßen", "snap": "ecke", "traeger": true, + "box": Vector3(0.3, 3.0, 0.3), "farbe": Color(0.42, 0.42, 0.40), + }, + "bau_giebelwand": { + "label": "Giebelwand", "info": "Dreieck-Abschluss unter dem Satteldach (2 m breit, 1 m Spitze)", + "snap": "kante", "traeger": false, + "typ": T_GIEBEL, "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "rise": 1.0, "farbe": Color(0.62, 0.53, 0.38), + }, + "bau_dach_giebel": { + "label": "Satteldach", "info": "2×2 m, First in Richtung der Vorschau (R dreht)", "snap": "zelle", "traeger": false, + "typ": T_DACH_GIEBEL, "punkte": QUADRAT, "rise": 1.0, "farbe": Color(0.40, 0.25, 0.18), + }, + "bau_dach_pyramide": { + "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), + }, + "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, + "typ": T_TREPPE, "punkte": [[0.0, 0.0, 0.0], [4.0, 3.0, 0.0]], "stufen": 15, "breite": 2.0, + "farbe": Color(0.46, 0.45, 0.42), + }, +} + +static var _mesh_cache: Dictionary = {} + + +static func ist_bau(oid: String) -> bool: + return KINDS.has(oid) + + +static func oids() -> Array: + return KINDS.keys() + + +static func label(oid: String) -> String: + return str((KINDS.get(oid, {}) as Dictionary).get("label", oid)) + + +static func snap_art(oid: String) -> String: + return str((KINDS.get(oid, {}) as Dictionary).get("snap", "zelle")) + + +static func ist_traeger(oid: String) -> bool: + return bool((KINDS.get(oid, {}) as Dictionary).get("traeger", false)) + + +## Ausmaße (m) des normalisierten Meshes: x = Länge/Breite, y = Höhe, z = Tiefe. +static func groesse(oid: String) -> Vector3: + var m := mesh(oid) + return m.get_aabb().size if m != null else Vector3.ZERO + + +# ------------------------------------------------------------------- Mesh --- + +## Normalisiertes Mesh (Fußpunkt unten-Mitte, Vertex-Farben) oder null. +static func mesh(oid: String) -> ArrayMesh: + if _mesh_cache.has(oid): + return _mesh_cache[oid] + var kind: Dictionary = KINDS.get(oid, {}) + if kind.is_empty(): + return null + var roh: ArrayMesh = _box_mesh(kind["box"]) if kind.has("box") else _addon_mesh(kind) + var fertig := _normalisieren(roh, kind["farbe"] as Color) + _mesh_cache[oid] = fertig + return fertig + + +static func _box_mesh(size: Vector3) -> ArrayMesh: + var bm := BoxMesh.new() + bm.size = size + var out := ArrayMesh.new() + out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, bm.get_mesh_arrays()) + return out + + +static func _addon_mesh(kind: Dictionary) -> ArrayMesh: + var geo := Node3D.new() + geo.set_script(load(GEO_SCRIPT)) + geo.set("piece_type", int(kind["typ"])) + geo.set("unit_size", float(kind.get("hoehe", WAND_H))) + geo.set("thickness", DICKE_M) + if kind.has("rise"): + geo.set("rise", float(kind["rise"])) + if kind.has("stufen"): + geo.set("stair_count", int(kind["stufen"])) + geo.set("stair_width", float(kind["breite"])) + var pts := PackedVector3Array() + for p in (kind["punkte"] as Array): + pts.append(Vector3(float(p[0]), float(p[1]), float(p[2]))) + geo.set("custom_points", pts) + for d in (kind.get("details", []) as Array): + var detail := Node3D.new() + detail.set_script(load(DETAIL_SCRIPT)) + detail.set("detail_type", int(d["typ"])) + detail.set("width", float(d["w"])) + detail.set("height", float(d["h"])) + detail.position = Vector3(float(d["pos"][0]), float(d["pos"][1]), float(d["pos"][2])) + geo.add_child(detail) + var m: ArrayMesh = geo.call("_make_mesh") + geo.free() + return m + + +## Alle Flächen zu EINER Fläche verschmelzen, Fußpunkt-normalisieren, Material- +## Farbe setzen. +static func _normalisieren(roh: ArrayMesh, farbe: Color) -> ArrayMesh: + var aabb := roh.get_aabb() + var versatz := Vector3(aabb.position.x + aabb.size.x * 0.5, aabb.position.y, + aabb.position.z + aabb.size.z * 0.5) + var pos := PackedVector3Array() + var nrm := PackedVector3Array() + for s in roh.get_surface_count(): + var arr := roh.surface_get_arrays(s) + var v: PackedVector3Array = arr[Mesh.ARRAY_VERTEX] + var n: Variant = arr[Mesh.ARRAY_NORMAL] + var idx: Variant = arr[Mesh.ARRAY_INDEX] + var order: Array = [] + if idx is PackedInt32Array and not (idx as PackedInt32Array).is_empty(): + for i in (idx as PackedInt32Array): + order.append(i) + else: + for i in v.size(): + order.append(i) + for i in order: + pos.append(v[i] - versatz) + nrm.append((n as PackedVector3Array)[i] if n is PackedVector3Array else Vector3.UP) + # Die Addon-Geometrie hat Dreiecke gegen die Normalenrichtung gewickelt (sie + # rendert ohne Backface-Culling). Godot culled Rückseiten → Wicklung an die + # Normale anpassen, sonst sieht man Innenseiten und Dächer werden schwarz. + for t in range(0, pos.size() - 2, 3): + var gn := (pos[t + 2] - pos[t]).cross(pos[t + 1] - pos[t]) + if gn.dot(nrm[t]) < 0.0: + var tmp := pos[t + 1] + pos[t + 1] = pos[t + 2] + pos[t + 2] = tmp + var tn := nrm[t + 1] + nrm[t + 1] = nrm[t + 2] + nrm[t + 2] = tn + var arrays := [] + arrays.resize(Mesh.ARRAY_MAX) + arrays[Mesh.ARRAY_VERTEX] = pos + arrays[Mesh.ARRAY_NORMAL] = nrm + var out := ArrayMesh.new() + out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) + var mat := StandardMaterial3D.new() + mat.albedo_color = farbe + mat.roughness = 0.9 + out.surface_set_material(0, mat) + return out + + +static func dreiecke(oid: String) -> int: + var m := mesh(oid) + return int(m.get_faces().size() / 3) if m != null else 0 + + +# ------------------------------------------------- Kollision / Registry ------ + +## Trimesh-Datei (library/kollisions/.json) — gleiche Felder wie die +## Pack-Dateien: Dreiecks-Suppe in Fußpunkt-Koordinaten. Türen, Fenster und +## Scharten sind echte Löcher (die Öffnung hat keine Dreiecke). +static func kollisions_json(oid: String) -> Dictionary: + var m := mesh(oid) + if m == null: + return {} + var verts: Array = [] + for v in m.get_faces(): + verts.append([snappedf(v.x, 0.0001), snappedf(v.y, 0.0001), snappedf(v.z, 0.0001)]) + var aabb := m.get_aabb() + return { + "oid": oid, + "quelle_glb": "res://assets/objekte/military_desert/%s/%s.glb" % [KATEGORIE, oid], + "dreiecke": int(verts.size() / 3), + "aabb_min": [snappedf(aabb.position.x, 0.001), snappedf(aabb.position.y, 0.001), snappedf(aabb.position.z, 0.001)], + "aabb_max": [snappedf(aabb.end.x, 0.001), snappedf(aabb.end.y, 0.001), snappedf(aabb.end.z, 0.001)], + "vertices": verts, + } + + +## Registry-Eintrag (Fragment library/objects.d/bauteile.json). +static func registry_eintrag(oid: String) -> Dictionary: + var g := groesse(oid) + return { + "poly": dreiecke(oid), + "collision": { + "quelle": "kollisions/%s.json" % oid, + "shape": "trimesh", + "size": [snappedf(g.x, 0.001), snappedf(g.y, 0.001), snappedf(g.z, 0.001)], + }, + "glb": "res://assets/objekte/military_desert/%s/%s.glb" % [KATEGORIE, oid], + "kategorie": KATEGORIE, + "name_de": label(oid), + } + + +## GLB schreiben (eigenes Mesh mit Vertex-Farben). Rückgabe: Godot-Fehlercode. +static func glb_schreiben(oid: String, pfad: String) -> int: + var m := mesh(oid) + if m == null: + return ERR_DOES_NOT_EXIST + var root := Node3D.new() + root.name = oid + var mi := MeshInstance3D.new() + mi.name = oid + mi.mesh = m + root.add_child(mi) + var gdoc := GLTFDocument.new() + var gstate := GLTFState.new() + var err := gdoc.append_from_scene(root, gstate) + if err == OK: + err = gdoc.write_to_filesystem(gstate, pfad) + root.free() + return err diff --git a/src/editor/bau_katalog.gd.uid b/src/editor/bau_katalog.gd.uid new file mode 100644 index 0000000..3ab1224 --- /dev/null +++ b/src/editor/bau_katalog.gd.uid @@ -0,0 +1 @@ +uid://defuinyvy5j8i diff --git a/src/editor/bau_ops.gd b/src/editor/bau_ops.gd new file mode 100644 index 0000000..02e65ac --- /dev/null +++ b/src/editor/bau_ops.gd @@ -0,0 +1,204 @@ +extends RefCounted +class_name BauOps +## Dokument-Operationen des Bauen-Modus (pure Funktionen, headless testbar). +## +## Ein Gebäude = eine Gruppe (GroupOps, Feld "group" + group_names) aus `bau_*`- +## Objekten. Alles Weitere — Verschieben, Drehen, Kopieren, Löschen, Symmetrie, +## Speichern — läuft über die normalen Objekt-Funktionen. Dieser Teil kennt nur, +## was speziell am Bauen ist: Teil setzen/ersetzen, Gebäude-Gruppen, Migration +## der alten "gebaeude"-Sektion (Karte t_97ea59c5 → Umbau 25.10.). + + +static func teile_zahl(doc: Dictionary) -> int: + var n := 0 + for o in (doc.get("objects", []) as Array): + if o is Dictionary and BauKatalog.ist_bau(str((o as Dictionary).get("id", ""))): + n += 1 + return n + + +## Dreiecke aller Bauteile (grobe Poly-Übersicht für die Statuszeile). +static func dreiecke_gesamt(doc: Dictionary) -> int: + var n := 0 + for o in (doc.get("objects", []) as Array): + if o is Dictionary: + n += BauKatalog.dreiecke(str((o as Dictionary).get("id", ""))) + return n + + +## Neue Gebäude-Gruppe anlegen und benennen. Rückgabe: Gruppen-ID. +static func gruppe_neu(doc: Dictionary) -> int: + var gid := GroupOps.next_group_id(doc) + GroupState.set_group_name(doc, gid, "Gebäude %d" % (gebaeude_zahl(doc) + 1)) + return gid + + +## Anzahl Gebäude-Gruppen (Gruppen, deren erstes Mitglied ein Bauteil ist). +static func gebaeude_zahl(doc: Dictionary) -> int: + return gebaeude_gruppen(doc).size() + + +## [{gid, name, indizes, dreiecke}] für alle Gebäude-Gruppen. +static func gebaeude_gruppen(doc: Dictionary) -> Array: + var out: Array = [] + var objects: Array = doc.get("objects", []) + for gid in GroupOps.active_group_ids(doc): + var members := GroupOps.members_of(doc, int(gid)) + if members.is_empty(): + continue + var bau := false + var tris := 0 + for i in members: + var oid := str((objects[i] as Dictionary).get("id", "")) + if BauKatalog.ist_bau(oid): + bau = true + tris += BauKatalog.dreiecke(oid) + if bau: + out.append({"gid": int(gid), "name": GroupState.display_name(doc, int(gid)), + "indizes": members, "dreiecke": tris}) + return out + + +## Teil setzen oder ersetzen (snap = BuildSnap.berechne-Ergebnis, ok=true). +## Mutiert doc["objects"]; der Aufrufer macht snapshot/commit. +## gid: Zielgruppe (Gebäude); bei Symmetrie bekommt der Zwilling ein eigenes +## Spiegel-Gebäude (spiegel_gid, vom Aufrufer gemerkt: 1 Original-Gebäude ↔ 1 Spiegel). +## Rückgabe: {index, twin, ersetzt}. +static func setze_teil(doc: Dictionary, oid: String, snap: Dictionary, gid: int, + sym_mode: int, spiegel_gid := -1) -> Dictionary: + var objects: Array = doc["objects"] + var pos: Vector3 = snap["pos"] + var rot := float(snap["rot"]) + var ersetze := int(snap.get("ersetze", -1)) + if ersetze >= 0 and ersetze < objects.size(): + var o: Dictionary = objects[ersetze] + o["id"] = oid + o["pos"] = [pos.x, pos.y, pos.z] + SvmapIO.rot_setzen(o, Vector3(0.0, rot, 0.0)) + # Symmetrie: der Zwilling bekommt dasselbe Teil an seiner (gespiegelten) Stelle. + var key := SymmetryCore.sym_of(o) + if sym_mode != SymmetryCore.Mode.OFF and not key.is_empty(): + var partner := SymmetryCore.find_partner_index(objects, key, ersetze) + if partner >= 0 and objects[partner] is Dictionary: + (objects[partner] as Dictionary)["id"] = oid + return {"index": ersetze, "twin": -1, "ersetzt": true} + var neu := {"id": oid, "pos": [pos.x, pos.y, pos.z], "scale": 1.0, "rot_y_deg": rot, "group": gid} + objects.append(neu) + var index := objects.size() - 1 + var twin := -1 + if sym_mode != SymmetryCore.Mode.OFF: + var res: Dictionary = ObjectOps.symmetry_place_twin(doc, index, sym_mode) + twin = int(res.get("twin", -1)) + if twin >= 0 and spiegel_gid >= 0: + (objects[twin] as Dictionary)["group"] = spiegel_gid + return {"index": index, "twin": twin, "ersetzt": false} + + +## Eine ganze Gebäude-Gruppe als Indizes (für „Ganzes Gebäude wählen“). +static func gebaeude_von(doc: Dictionary, index: int) -> Array: + var objects: Array = doc.get("objects", []) + if index < 0 or index >= objects.size(): + return [] + var gid := GroupOps.group_id(objects[index]) + if gid == GroupOps.UNGROUPED: + return [index] + return GroupOps.members_of(doc, gid) + + +# ----------------------------------------------------------- Demo / Test ---- + +## Kleines Haus über die echte Klick-Logik setzen (Fundament, Wände, Tür, Fenster, +## Giebel, Dach, Treppe) — für Demo und Tests. Jeder Schritt = BuildSnap-Treffer +## wie im Editor. Rückgabe: Gruppen-ID. history kann null sein. +static func haus_demo(doc: Dictionary, history: Variant, mitte: Vector3) -> int: + var gid := gruppe_neu(doc) + var terrain := func(_x: float, _z: float) -> float: return mitte.y + var before: Dictionary = history.snapshot(["objects", "group_names"]) if history != null else {} + # 2×2 Zellen (4×4 m): Zellmitten bei (±1, ±1) relativ zur Mitte. + var ox := floorf(mitte.x / 2.0) * 2.0 + var oz := floorf(mitte.z / 2.0) * 2.0 + var schritte := [ + ["bau_fundament", Vector3(ox - 1, mitte.y, oz - 1), 0.0], + ["bau_fundament", Vector3(ox + 1, mitte.y, oz - 1), 0.0], + ["bau_fundament", Vector3(ox - 1, mitte.y, oz + 1), 0.0], + ["bau_fundament", Vector3(ox + 1, mitte.y, oz + 1), 0.0], + ["bau_wand", Vector3(ox - 1, mitte.y, oz - 2), 0.0], + ["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], + ] + 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) + if history != null: + history.commit("Haus gebaut (Demo)", before, history.snapshot(["objects", "group_names"])) + return gid + + +static func _demo_setzen(doc: Dictionary, oid: String, hit: Vector3, dreh: float, gid: int, + gelaende: Callable) -> void: + var teile := BuildSnap.sammle_teile(doc) + var snap := BuildSnap.berechne(oid, hit, dreh, teile, gelaende) + if bool(snap["ok"]): + setze_teil(doc, oid, snap, gid, SymmetryCore.Mode.OFF) + + +# --------------------------------------------------------------- Migration -- + +## Alte Sektion doc["gebaeude"] (Karte t_97ea59c5, 10 prozedurale Teile ohne +## Registry) → bau_*-Objekte in EINER Gruppe, dann Sektion entfernen. Beste +## Näherung: Teil-Ursprünge der alten Teile lagen an Wandanfang/Zellmitte, die +## neuen Bauteile an der Mitte der Grundfläche; Höhen (2 m → 3 m) ändern sich. +## Rückgabe: Anzahl übernommener Teile. +const _ALT := { + "fundament": "bau_fundament", "boden": "bau_boden", "wand": "bau_wand", + "tuer": "bau_wand_tuer", "fenster": "bau_wand_fenster", + "giebelwand": "bau_giebelwand", "giebelwand_rechts": "bau_giebelwand", + "dach_giebel": "bau_dach_giebel", "dach_pyramide": "bau_dach_pyramide", + "treppe": "bau_treppe", +} + +static func migriere_gebaeude(doc: Dictionary) -> int: + if not doc.has("gebaeude"): + return 0 + var alt: Variant = doc["gebaeude"] + doc.erase("gebaeude") + if not (alt is Array) or (alt as Array).is_empty(): + return 0 + if not doc.has("objects") or not (doc["objects"] is Array): + doc["objects"] = [] + var objects: Array = doc["objects"] + var gid := gruppe_neu(doc) + var n := 0 + for teil in alt: + if not (teil is Dictionary): + continue + var oid: String = _ALT.get(str((teil as Dictionary).get("id", "")), "") + if oid.is_empty(): + continue + var p: Array = (teil as Dictionary).get("pos", [0.0, 0.0, 0.0]) + var rot := float((teil as Dictionary).get("rot", 0.0)) + var pos := Vector3(float(p[0]), float(p[1]), float(p[2])) + var local := Vector3.ZERO + match str((teil as Dictionary).get("id", "")): + "wand", "tuer", "fenster", "giebelwand", "giebelwand_rechts": + local = Vector3(1.0, 0.0, 0.0) # alt: Ursprung am Wandanfang + "treppe": + local = Vector3(2.0, 0.0, 0.0) # alt: 2 m lang ab Ursprung + "boden": + pos.y -= BauKatalog.DICKE_M # alt: Oberkante am Ursprung + pos += Basis(Vector3.UP, deg_to_rad(rot)) * local + objects.append({"id": oid, "pos": [pos.x, pos.y, pos.z], "scale": 1.0, + "rot_y_deg": fposmod(rot, 360.0), "group": gid}) + n += 1 + if n == 0: + GroupState.set_group_name(doc, gid, "") + return n diff --git a/src/editor/bau_ops.gd.uid b/src/editor/bau_ops.gd.uid new file mode 100644 index 0000000..1bd8d65 --- /dev/null +++ b/src/editor/bau_ops.gd.uid @@ -0,0 +1 @@ +uid://bh4sybddgu3mt diff --git a/src/editor/build_ops.gd b/src/editor/build_ops.gd deleted file mode 100644 index b5251c4..0000000 --- a/src/editor/build_ops.gd +++ /dev/null @@ -1,454 +0,0 @@ -extends RefCounted -class_name BuildOps -## Bauen-Modus (Karte t_97ea59c5): Modular-Builder-Pieces im App-Skelett. -## -## Architektur-Entscheidung (dokumentiert, Karte t_97ea59c5): -## 1. WRAPPER statt Plugin-UI: Das Godot-Editor-Plugin (addons/modular_builder, -## aktivierbar) bleibt unangetastet für den Godot-Editor. Die App bekommt -## einen EIGENEN „Bauen“-Modus, der dieselbe Runtime-Geometrie benutzt -## (modular_piece_geometry.gd + modular_building_detail.gd — CC0, reines -## Node3D/ArrayMesh, keine Editor-Abhängigkeit). Keine Godot-Editor-API -## leakt ins App-UI. -## 2. UNDO: EIN ActionHistory-Eintrag PRO BAUTEIL (Snapshot des Blocks -## "gebaeude" — wie Objekte platzieren). Der Block ist klein (Positionen + -## Preset-Punkte), tiefe Kopien sind billig. Kein Terrain-Stroke-Muster -## nötig, weil ein Bauteil eine atomare Klick-Aktion ist (kein Drag). -## 3. SPEICHERN: Die BAUTEILE sind die Quelle der Wahrheit und wandern als -## optionale Sektion "gebaeude" ins .svmap ( checksum-neutral: der -## Server-Loader prüft Pflichtblöcke und die Prüfsumme ÜBER alles — -## unbekannte Zusatzblöcke stören nicht). Das gebackene Mesh wird NICHT -## in der Datei abgelegt: Baken ist deterministisch aus den Teilen -## (Editor-Ansicht backt beim Laden, Match-Export in Karte 2 backt beim -## Export). Weiterbauen nach Laden bleibt damit möglich — Empfehlung -## „beim Speichern backen“ wäre nur Doppeldaten. -## 4. RASTER: 2-m-Modulraster. "ecke"-Teile (Wand/Treppe/Tür/Fenster/Giebel) -## rasten auf gerade Meter (Zellen-Ecken), "mitte"-Teile (Fundament, -## Boden, Dach) auf ungerade Meter (Zellen-Mitte) — so greifen Fundamente -## und Wände bündig ineinander. Drehen in 90°-Schritten bleibt rastergetreu. -## -## Teile-Format (im doc["gebaeude"]-Array): -## {"id": "wand", "pos": [x, y, z], "rot": 90.0, -## "punkte": [[x,y,z], ...], # lokale custom_points des Pieces -## "details": [{"typ": "tuer", "pos": [x,y,z], "w": 1.0, "h": 2.0}]} -## "punkte"/"details" kommen aus dem Preset (make_piece) — sie bleiben in der -## Datei, damit Laden → Weiterbauen → Baken identisch funktioniert. - -const GEO_SCRIPT := "res://addons/modular_builder/modular_piece_geometry.gd" -const DETAIL_SCRIPT := "res://addons/modular_builder/modular_building_detail.gd" - -# PieceType (addons/modular_builder/modular_piece_geometry.gd, Kopie der Werte — -# die Typen-Anmerkung Array[ModularBuilderPieceGeometry] im Plugin-Code braucht -# den globalen Klassen-Cache, der hier bewusst NICHT angefasst wird). -const T_FOUNDATION_SQUARE := 0 -const T_FOUNDATION_TRIANGLE := 1 -const T_FLOOR := 2 -const T_WALL := 3 -const T_GABLE_WALL := 5 -const T_ROOF_GABLE := 6 -const T_ROOF_SHED := 7 -const T_GABLE_WALL_RIGHT := 8 -const T_ROOF_PYRAMID := 9 -const T_STAIRS := 10 - -const DETAIL_WINDOW := 0 -const DETAIL_DOOR := 1 - -const GRID_M := 2.0 # Modulraster (unit_size des Plugins) -const WALL_H := 2.0 # Wandhöhe = unit_size -const SNAP_Y_M := 0.05 # Y-Snap: verhindert Float-Staub beim Stapeln - -## Katalog: id → Preset. Reihenfolge = Anzeige-Reihenfolge im Bauen-Panel. -const KINDS := { - "fundament": { - "label": "Fundament", "info": "2×2 m, Basis", "typ": T_FOUNDATION_SQUARE, "raster": "mitte", - "farbe": Color(0.62, 0.62, 0.60), - "punkte": [[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]], - }, - "boden": { - "label": "Boden", "info": "2×2 m Platte", "typ": T_FLOOR, "raster": "mitte", - "farbe": Color(0.72, 0.60, 0.42), - "punkte": [[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]], - }, - "wand": { - "label": "Wand", "info": "2 m lang, 2 m hoch", "typ": T_WALL, "raster": "ecke", - "farbe": Color(0.88, 0.86, 0.80), - "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], - }, - "tuer": { - "label": "Wand mit Tür", "info": "2 m, Öffnung 1×2 m", "typ": T_WALL, "raster": "ecke", - "farbe": Color(0.88, 0.86, 0.80), - "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], - "details": [{"typ": DETAIL_DOOR, "pos": [1.0, 1.0, 0.0], "w": 1.0, "h": 2.0}], - }, - "fenster": { - "label": "Wand mit Fenster", "info": "2 m, Fenster 1×1 m", "typ": T_WALL, "raster": "ecke", - "farbe": Color(0.88, 0.86, 0.80), - "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], - "details": [{"typ": DETAIL_WINDOW, "pos": [1.0, 1.0, 0.0], "w": 1.0, "h": 1.0}], - }, - "giebelwand": { - "label": "Giebelwand", "info": "Spitze in der Mitte", "typ": T_GABLE_WALL, "raster": "ecke", - "farbe": Color(0.88, 0.86, 0.80), - "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "rise": 1.0, - }, - "giebelwand_rechts": { - "label": "Giebel rechts", "info": "Spitze rechts", "typ": T_GABLE_WALL_RIGHT, "raster": "ecke", - "farbe": Color(0.88, 0.86, 0.80), - "punkte": [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], "rise": 1.0, - }, - "dach_giebel": { - "label": "Dach (Giebel)", "info": "2×2 m, Satteldach", "typ": T_ROOF_GABLE, "raster": "mitte", - "farbe": Color(0.66, 0.28, 0.20), - "punkte": [[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]], - "rise": 1.0, - }, - "dach_pyramide": { - "label": "Dach (Pyramide)", "info": "2×2 m, Spitze mitte", "typ": T_ROOF_PYRAMID, "raster": "mitte", - "farbe": Color(0.66, 0.28, 0.20), - "punkte": [[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]], - "rise": 1.0, - }, - "treppe": { - "label": "Treppe", "info": "2 m lang, 1 m hoch", "typ": T_STAIRS, "raster": "ecke", - "farbe": Color(0.55, 0.42, 0.30), - "punkte": [[0.0, 0.0, 0.0], [2.0, 1.0, 0.0]], "stufen": 10, "breite": 1.2, - }, -} - - -# ------------------------------------------------------------------ Blöcke --- - -## "gebaeude"-Block garantieren (alte Maps haben ihn nicht — Snapshot/Undo -## braucht den Block NUR, wenn er existiert oder beim ersten Teil entsteht). -static func ensure_block(doc: Dictionary) -> Array: - if not doc.has("gebaeude") or not (doc["gebaeude"] is Array): - doc["gebaeude"] = [] - return doc["gebaeude"] - - -static func pieces(doc: Dictionary) -> Array: - var block = doc.get("gebaeude", []) - return block if block is Array else [] - - -static func piece_count(doc: Dictionary) -> int: - return pieces(doc).size() - - -static func kind_label(kind_id: String) -> String: - var kind: Dictionary = KINDS.get(kind_id, {}) - return str(kind.get("label", kind_id)) - - -## Gesamt-Dreiecke aller Teile (grob, für Statusleiste/Budget-Blick). -static func tri_count(doc: Dictionary) -> int: - var total := 0 - for teil in pieces(doc): - if not (teil is Dictionary): - continue - var mesh := piece_mesh(teil) - if mesh != null: - total += int(mesh.get_faces().size() / 3) - return total - - -# ------------------------------------------------------------------- Teile --- - -## Ein Bauteil-Dict aus dem Preset bauen (Punkte/Details als Kopien — -## sonst aliasen sie mit dem Katalog-Konstanten). -static func make_piece(kind_id: String, pos: Vector3, rot_deg: float) -> Dictionary: - var kind: Dictionary = KINDS.get(kind_id, {}) - if kind.is_empty(): - return {} - var punkte: Array = [] - for p in (kind.get("punkte", []) as Array): - punkte.append([float(p[0]), float(p[1]), float(p[2])]) - var teil := { - "id": kind_id, - "pos": [pos.x, pos.y, pos.z], - "rot": rot_deg, - "punkte": punkte, - } - if kind.has("details"): - var details: Array = [] - for d in (kind["details"] as Array): - details.append({ - "typ": int(d["typ"]), - "pos": [float(d["pos"][0]), float(d["pos"][1]), float(d["pos"][2])], - "w": float(d["w"]), - "h": float(d["h"]), - }) - teil["details"] = details - return teil - - -## 2-m-Raster-Snap je Teil-Typ ("ecke" = gerade Meter, "mitte" = ungerade Meter). -static func snap_xz(kind_id: String, x: float, z: float) -> Vector2: - var kind: Dictionary = KINDS.get(kind_id, {}) - var half := GRID_M * 0.5 - if str(kind.get("raster", "ecke")) == "mitte": - return Vector2(floorf(x / GRID_M) * GRID_M + half, floorf(z / GRID_M) * GRID_M + half) - return Vector2(roundf(x / GRID_M) * GRID_M, roundf(z / GRID_M) * GRID_M) - - -## Y-Snap: 5-cm-Raster gegen Float-Staub beim Stapeln. -static func snap_y(y: float) -> float: - return roundf(y / SNAP_Y_M) * SNAP_Y_M - - -## Komplett gerasterte Teil-Position aus einem Trefferpunkt (Boden oder -## Bauteil-Oberfläche): XZ aufs Modulraster, Y aufs 5-cm-Raster. -static func snapped_pos(kind_id: String, hit: Vector3) -> Vector3: - var xz := snap_xz(kind_id, hit.x, hit.z) - return Vector3(xz.x, snap_y(hit.y), xz.y) - - -## Mesh EINES Teils (lokal, Ursprung = Teil-Position). Nutzt die Plugin- -## Runtime-Geometrie — identische Meshes wie im Godot-Editor-Plugin. -static func piece_mesh(teil: Dictionary) -> ArrayMesh: - var kind_id := str(teil.get("id", "")) - var kind: Dictionary = KINDS.get(kind_id, {}) - if kind.is_empty(): - return null - var geo := Node3D.new() - geo.set_script(load(GEO_SCRIPT)) - geo.set("piece_type", int(kind["typ"])) - geo.set("unit_size", WALL_H) - geo.set("thickness", 0.2) - if kind.has("rise"): - geo.set("rise", float(kind["rise"])) - if kind.has("stufen"): - geo.set("stair_count", int(kind["stufen"])) - geo.set("stair_width", float(kind["breite"])) - # custom_points: vom Preset (Datei) oder (Rückfall) aus dem Katalog. - var pts_src: Array = (teil.get("punkte", kind.get("punkte", [])) as Array) - if pts_src.size() >= 2: - var pts := PackedVector3Array() - for p in pts_src: - pts.append(Vector3(float(p[0]), float(p[1]), float(p[2]))) - geo.set("custom_points", pts) - # Detail-Öffnungen (Tür/Fenster) als Kind-Knoten — die Wand-Geometrie - # schneidet sie aus (genau wie im Plugin). - for d in (teil.get("details", []) as Array): - var detail := Node3D.new() - detail.set_script(load(DETAIL_SCRIPT)) - detail.set("detail_type", int(d["typ"])) - detail.set("width", float(d["w"])) - detail.set("height", float(d["h"])) - detail.position = Vector3(float(d["pos"][0]), float(d["pos"][1]), float(d["pos"][2])) - geo.add_child(detail) - var mesh: ArrayMesh = geo.call("_make_mesh") - geo.free() - return mesh - - -## Material je Teil-Typ (flache Farben, eigene CC0-freie Fallbacks statt der -## nicht-CC0-Demo-Texturen — Sly-Look kommt später als echtes Texturenpaket). -static func piece_material(kind_id: String, cache: Dictionary) -> StandardMaterial3D: - if cache.has(kind_id): - return cache[kind_id] - var mat := StandardMaterial3D.new() - mat.albedo_color = KINDS.get(kind_id, {}).get("farbe", Color(0.8, 0.8, 0.8)) - mat.roughness = 0.9 - cache[kind_id] = mat - return mat - - -# ------------------------------------------------------------------- Baken --- - -## ALLE Teile in WELT-Koordinaten backen (für Kollisions-Raycast + späterer -## Match-Export in Karte 2). Rückgabe: faces (Dreiecke, Welt-Koordinaten) + -## offsets[i] = Index des ERSTEN Dreiecks von Teil i im faces-Array. -static func bake_faces(doc: Dictionary) -> Dictionary: - var faces := PackedVector3Array() - var offsets: Array = [] - for teil in pieces(doc): - if not (teil is Dictionary): - offsets.append(-1) - continue - offsets.append(faces.size() / 3) - var mesh := piece_mesh(teil) - if mesh == null: - continue - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var xf := Transform3D( - Basis(Vector3.UP, deg_to_rad(float(teil.get("rot", 0.0)))), - Vector3(float(pos[0]), float(pos[1]), float(pos[2]))) - for v in mesh.get_faces(): - faces.append(xf * v) - return {"faces": faces, "offsets": offsets} - - -## Aßerhalb-Test-loser AABB aller Teile (für Auswahl-Rahmen im Viewport). -static func piece_aabb(teil: Dictionary) -> AABB: - var mesh := piece_mesh(teil) - if mesh == null: - return AABB() - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var xf := Transform3D( - Basis(Vector3.UP, deg_to_rad(float(teil.get("rot", 0.0)))), - Vector3(float(pos[0]), float(pos[1]), float(pos[2]))) - var aabb := AABB() - var first := true - for v in mesh.get_faces(): - var wp := xf * v - if first: - aabb = AABB(wp, Vector3.ZERO) - first = false - else: - aabb = aabb.expand(wp) - return aabb - - -# ---------------------------------------------------------------- Ray-Pick --- - -## Analytischer Ray-Treffer auf den gebackenen Flächen (Möller–Trumbore) — -## kein Physik-Space nötig (SubViewport ohne Physik-Tick, headless-testbar). -## Rückgabe: {"piece": Teil-Index, "punkt": Vector3} oder {}. -## max_dist ist nur pro Aufruf begrenzbar (Default: unbegrenzt) — ein Cap von -## 500 m war hier früher die Ursache dafür, dass Umschalt+Klick im Default- -## Framing (~614 m Kamera-Distanz) schlicht nichts traf. Performance-unkritisch: -## der Tri-Loop läuft über alle gebackenen Faces, egal wie der Cap steht. -static func ray_pick(baked: Dictionary, origin: Vector3, dir: Vector3, max_dist: float = INF) -> Dictionary: - var faces: PackedVector3Array = baked.get("faces", PackedVector3Array()) - var offsets: Array = baked.get("offsets", []) - var tri_count := int(faces.size() / 3) - var best_t := max_dist - var best_tri := -1 - for i in tri_count: - var a := faces[i * 3] - var b := faces[i * 3 + 1] - var c := faces[i * 3 + 2] - var t := _ray_tri(origin, dir, a, b, c) - if t >= 0.0 and t < best_t: - best_t = t - best_tri = i - if best_tri < 0: - return {} - var piece := -1 - for j in offsets.size(): - var off := int(offsets[j]) - if off >= 0 and best_tri >= off: - piece = j # offsets aufsteigend → letzter Treffer gewinnt - return {"piece": piece, "punkt": origin + dir * best_t} - - -static func _ray_tri(origin: Vector3, dir: Vector3, a: Vector3, b: Vector3, c: Vector3) -> float: - var e1 := b - a - var e2 := c - a - var p := dir.cross(e2) - var det := e1.dot(p) - if absf(det) < 0.000001: - return -1.0 - var inv := 1.0 / det - var tv := origin - a - var u := tv.dot(p) * inv - if u < 0.0 or u > 1.0: - return -1.0 - var q := tv.cross(e1) - var v := dir.dot(q) * inv - if v < 0.0 or u + v > 1.0: - return -1.0 - return e2.dot(q) * inv - - -# ------------------------------------------------------ Symmetrie (M4) ------ -## Karte t_ffa207b1 (Spec /opt/data/scopedvr/KARTEN_MIRROR_vc27.md): Im -## Symmetrie-Modus bekommt jedes gesetzte Bau-Teil automatisch einen Zwilling -## auf der anderen Kartenseite — gleiche Snap-Regeln (Position wird 1:1 -## gespiegelt, Parität „ecke"/„mitte" bleibt bei zentrierter Karte erhalten), -## gleiche kind_id, gleiche Punkte/Details. Gekoppelt über das `sym`-Feld aus -## M1 (SymmetryCore). ALLE Funktionen sind pure Statics → headless testbar. -## Aufrufer ist build_tool.gd (dort leben die Klick-Mutationen); die Historie -## snapshotet den Block „gebaeude" um den Aufruf herum → EIN Undo-Schritt -## nimmt Original UND Zwilling zurück. - -## V9-Grenzen (M4): dieselbe Regel wie der Loader-Kern (svmap_io.gd, -## _inside_bounds mit ±1 m Toleranz). true = gespiegeltes Teil darf gespeichert -## werden; false → Aufrufer meldet V9 im Status statt still zu speichern. -static func inside_match_bounds(pos: Vector3, doc: Dictionary) -> bool: - var bounds: Dictionary = doc.get("bounds", {}) - if bounds.is_empty(): - return true - return SvmapIO._inside_bounds([pos.x, pos.y, pos.z], bounds) - - -## Zwilling eines frisch gesetzten Bauteils anlegen (Symmetrie an). `teil` muss -## bereits im doc hängen (der sym-Link wird auf beide Einträge gesetzt). -## Rückgabe: Klartext-Zusatz für die Statuszeile ("" = still gespiegelt). -static func place_mirrored(doc: Dictionary, teil: Dictionary, mode: int) -> String: - if mode == SymmetryCore.Mode.OFF: - return "" - var label := kind_label(str(teil.get("id", ""))) - var pos := _pos_of(teil) - var center := SymmetryCore.map_center(doc) - if SymmetryCore.maps_to_self(mode, pos, center): - return "%s liegt auf der Symmetrie-Mitte — nicht gedoppelt." % label - var twin := teil.duplicate(true) # tief: punkte/details dürfen nicht aliasen - var mirror_pos := SymmetryCore.mirror_point(mode, pos, center) - twin["pos"] = [mirror_pos.x, mirror_pos.y, mirror_pos.z] - twin["rot"] = SymmetryCore.mirror_rot_y(mode, float(teil.get("rot", 0.0))) - if not inside_match_bounds(mirror_pos, doc): - return "❌ V9: Gespiegeltes %s läge außerhalb der Match-Grenzen — nur das Original wurde gesetzt." % label - var key := SymmetryCore.next_sym_key(doc) - SymmetryCore.set_sym(teil, key) - SymmetryCore.set_sym(twin, key) - ensure_block(doc).append(twin) - return "" - - -## Zwilling nach Drehen/Anheben/Verschieben/Auf-Boden neu ausrichten: er bekommt -## immer die gespiegelte Form der NEUEN Transform des Originals (Drehung über -## mirror_rot_y, Position über mirror_point, y bleibt Skalar). Teile ohne -## sym-Partner (z. B. bei ausgeschalteter Symmetrie gebaut) bleiben unberührt. -## Läge der gespiegelte Zielpunkt außerhalb der V9-Grenzen, bleibt der Zwilling -## UNVERÄNDERT und die Meldung geht in den Status. Rückgabe: Klartext ("" = ok). -static func update_mirrored(doc: Dictionary, index: int, mode: int) -> String: - if mode == SymmetryCore.Mode.OFF: - return "" - var list := pieces(doc) - if index < 0 or index >= list.size() or not (list[index] is Dictionary): - return "" - var teil := list[index] as Dictionary - var key := SymmetryCore.sym_of(teil) - if key.is_empty(): - return "" - var partner_idx := SymmetryCore.find_partner_index(list, key, index) - if partner_idx < 0 or not (list[partner_idx] is Dictionary): - return "" - var mirror_pos := SymmetryCore.mirror_point(mode, _pos_of(teil), SymmetryCore.map_center(doc)) - if not inside_match_bounds(mirror_pos, doc): - return "❌ V9: Gespiegeltes Bauteil läge außerhalb der Match-Grenzen — Zwilling nicht verschoben." - var twin := list[partner_idx] as Dictionary - twin["pos"] = [mirror_pos.x, mirror_pos.y, mirror_pos.z] - twin["rot"] = SymmetryCore.mirror_rot_y(mode, float(teil.get("rot", 0.0))) - return "" - - -## Bauteil löschen — bei eingeschalteter Symmetrie den sym-Partner mit -## (entfernen → beide weg). Index-Verschiebung sicher: der HÖHERE Index wird -## zuerst entfernt. Modus AUS = exakt das alte Verhalten (nur ein Teil weg). -## Rückgabe: Klartext-Status ("" = nur das eine Teil entfernt). -static func remove_mirrored(doc: Dictionary, index: int, mode: int) -> String: - var list := pieces(doc) - if index < 0 or index >= list.size() or not (list[index] is Dictionary): - return "" - var teil := list[index] as Dictionary - var partner_idx := -1 - if mode != SymmetryCore.Mode.OFF: - partner_idx = SymmetryCore.find_partner_index(list, SymmetryCore.sym_of(teil), index) - if partner_idx < 0: - list.remove_at(index) - return "" - if partner_idx > index: - list.remove_at(partner_idx) - list.remove_at(index) - else: - list.remove_at(index) - list.remove_at(partner_idx) - return "Der gespiegelte Zwilling wurde mit entfernt." - - -static func _pos_of(teil: Dictionary) -> Vector3: - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - return Vector3(float(pos[0]), float(pos[1]), float(pos[2])) diff --git a/src/editor/build_ops.gd.uid b/src/editor/build_ops.gd.uid deleted file mode 100644 index 367d849..0000000 --- a/src/editor/build_ops.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://buoucxvl5d35c diff --git a/src/editor/build_snap.gd b/src/editor/build_snap.gd new file mode 100644 index 0000000..422e631 --- /dev/null +++ b/src/editor/build_snap.gd @@ -0,0 +1,167 @@ +extends RefCounted +class_name BuildSnap +## Raster- und Ebenen-Logik des Bauen-Modus (pure Funktionen, headless testbar). +## +## Warum neu: die alte Logik rastete jedes Teil NUR auf eine Zellecke und nahm +## als Höhe die Klickhöhe — ein Klick auf die Seite einer Wand setzte das neue +## Teil mitten in die Luft, Wände ließen sich nur über Rotationsraten an die +## richtige Kante bringen. Jetzt: +## • XZ: Zellen-Teile rasten in die Zellmitte, Wände auf die NÄCHSTE Zellkante +## (Drehung ergibt sich aus der Kante), Pfeiler auf die nächste Ecke, +## Treppen auf eine 4×2-m-Fläche über zwei Zellen. +## • Y: nie die Klickhöhe, sondern eine EBENE: Gelände (höchster Punkt unter +## der Grundfläche) oder Oberkante eines tragenden Bauteils darunter. Gewählt +## wird die höchste Ebene, die nicht mehr als TOL_M über dem Klickpunkt liegt. +## • Ein gleichartiges Teil auf demselben Platz wird ERSETZT (Tür in eine +## vorhandene Wand einsetzen); Klick auf die Oberkante stapelt stattdessen. +## +## Position eines Teils = Fußpunkt (Unterkante, Mitte der Grundfläche) — wie +## jedes Bibliotheks-Objekt (BauKatalog.mesh ist so normalisiert). + +const TOL_M := 0.35 # so weit darf eine Ebene ÜBER dem Klickpunkt liegen +const SHRINK_M := 0.05 # Grundflächen-Schnitt: minimale Überlappung ignorieren +const SAME_M := 0.08 # Toleranz für "gleiche Stelle" + + +## Ergebnis-Wörterbuch: +## ok false = hier nicht setzbar (grund erklärt warum) +## pos Vector3 Fußpunkt +## rot Y-Drehung in Grad +## ersetze Index des Objekts, das ersetzt wird (-1 = neues Teil) +## ebene gewählte Höhe (= pos.y) +## grund Klartext (bei ok=false) bzw. Hinweis +## +## teile: Array von {"i": Index, "oid": String, "pos": Vector3, "rot": float} +## (nur Bauteile, siehe sammle_teile). +## gelaende: Callable(x: float, z: float) -> float +## dreh: Nutzerdrehung in Grad (wird auf 90° gerastet) +## kamera_dir: Blickrichtung des Klickstrahls (für Zell-Wahl an Wandkanten), optional +static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array, + gelaende: Callable, hit_ist_objekt := false, kamera_dir := Vector3.ZERO) -> Dictionary: + var art := BauKatalog.snap_art(oid) + var gr := BauKatalog.groesse(oid) + 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 + 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) + rot = viertel * 90.0 + "ecke": + mitte = Vector2(roundf(p.x / r) * r, roundf(p.y / r) * r) + "treppe": + rot = viertel * 90.0 + if viertel % 2 == 0: # Lauf entlang X: Mitte auf gerader X-, ungerader Z-Linie + mitte = Vector2(roundf(p.x / r) * r, floorf(p.y / r) * r + r * 0.5) + 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 + else: + mitte = v_mitte + 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 + var half := Vector2(gr.z, gr.x) * 0.5 if quer else Vector2(gr.x, gr.z) * 0.5 + var rect := Rect2(mitte - half, half * 2.0) + + # Ebenen sammeln: Gelände + Oberkanten tragender Teile unter der Grundfläche. + var gel_max := -INF + for sx in [0.0, 0.5, 1.0]: + for sz in [0.0, 0.5, 1.0]: + gel_max = maxf(gel_max, float(gelaende.call(rect.position.x + rect.size.x * sx, + rect.position.y + rect.size.y * sz))) + var ebenen: Array = [gel_max] + var kern := rect.grow(-SHRINK_M) + for t in teile: + var troid := str(t["oid"]) + var tr := teil_rect(t) + if not tr.intersects(kern): + continue + var top: float = (t["pos"] as Vector3).y + BauKatalog.groesse(troid).y + if BauKatalog.ist_traeger(troid): + ebenen.append(top) + var ebene := _waehle_ebene(ebenen, hit.y) + + # Platz belegt? Gleiche Art + gleiche Stelle + gleiche Ebene → ersetzen/ablehnen. + var ersetze := -1 + for t in teile: + var troid := str(t["oid"]) + if BauKatalog.snap_art(troid) != art: + continue + var tp: Vector3 = t["pos"] + if absf(tp.y - ebene) > SAME_M: + continue + if Vector2(tp.x, tp.z).distance_to(mitte) > SAME_M: + continue + if art == "kante" and absf(fposmod(float(t["rot"]), 180.0) - fposmod(rot, 180.0)) > 1.0: + continue + if art == "treppe" and absf(fposmod(float(t["rot"]), 360.0) - rot) > 1.0: + continue + if troid == oid and absf(fposmod(float(t["rot"]) - rot, 360.0)) < 1.0: + return {"ok": false, "pos": Vector3(mitte.x, ebene, mitte.y), "rot": rot, "ersetze": -1, + "ebene": ebene, "grund": "Hier steht schon %s." % BauKatalog.label(oid)} + ersetze = int(t["i"]) + break + return {"ok": true, "pos": Vector3(mitte.x, ebene, mitte.y), "rot": rot, "ersetze": ersetze, + "ebene": ebene, "grund": ""} + + +## 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 + var tiefste := INF + for e in ebenen: + var ev := float(e) + tiefste = minf(tiefste, ev) + if ev <= hit_y + TOL_M and ev > best: + best = ev + return best if best > -INF else tiefste + + +## Grundfläche (XZ) eines gesetzten Bauteils in Weltkoordinaten. +static func teil_rect(t: Dictionary) -> Rect2: + var gr := BauKatalog.groesse(str(t["oid"])) + var rot := float(t["rot"]) + var quer := absf(sin(deg_to_rad(rot))) > 0.5 + var half := Vector2(gr.z, gr.x) * 0.5 if quer else Vector2(gr.x, gr.z) * 0.5 + var pos: Vector3 = t["pos"] + return Rect2(Vector2(pos.x, pos.z) - half, half * 2.0) + + +## Alle Bauteile aus doc["objects"] (Index, oid, pos, rot) — nur `bau_*`. +static func sammle_teile(doc: Dictionary) -> Array: + var out: Array = [] + var objects: Array = doc.get("objects", []) + for i in objects.size(): + var o: Variant = objects[i] + if not (o is Dictionary): + continue + var oid := str((o as Dictionary).get("id", "")) + if not BauKatalog.ist_bau(oid): + continue + var pos: Array = (o as Dictionary).get("pos", [0.0, 0.0, 0.0]) + if pos.size() != 3: + continue + out.append({ + "i": i, "oid": oid, + "pos": Vector3(float(pos[0]), float(pos[1]), float(pos[2])), + "rot": float(SvmapIO.objekt_rotation(o as Dictionary).y), + }) + return out diff --git a/src/editor/build_snap.gd.uid b/src/editor/build_snap.gd.uid new file mode 100644 index 0000000..1d8709e --- /dev/null +++ b/src/editor/build_snap.gd.uid @@ -0,0 +1 @@ +uid://irnbwhk0eijq diff --git a/src/editor/build_tool.gd b/src/editor/build_tool.gd deleted file mode 100644 index 1db4420..0000000 --- a/src/editor/build_tool.gd +++ /dev/null @@ -1,406 +0,0 @@ -extends Node3D -## Bauen-Werkzeug (Karte t_97ea59c5): Modular-Builder-Pieces in der App setzen — -## Fundament/Wand/Dach/Treppe/Tür/Fenster auf einem 2-m-Modulraster. -## -## Bedienung (Chef-Grundsatz Sly: easy, deutsch): -## Bauteil rechts anklicken → Vorschau folgt dem Cursor (rastet aufs Raster) -## Linksklick → Bauteil setzen (auch AUF andere Teile — zum -## Stapeln: Wand oben drauf, Dach auf die Wände) -## Umschalt+Klick → gesetztes Bauteil AUSWÄHLEN (orange Rahmen) -## R → Vorschau drehen (90°) / Auswahl drehen -## Bild↑/Bild↓ → Auswahl anheben/absenken (0,2 m — Wände aufs -## Fundament setzen) -## Entf → Auswahl löschen -## ESC → Auswahl weg, nochmal = Werkzeug beenden -## (Rechte Maustaste = immer Flug, t_2470de89) -## -## UNDO: EIN Rückgängig-Schritt PRO Bauteil (ActionHistory, Block "gebaeude"). - -const MODE_HELP := "Linksklick: Bauteil setzen · Umschalt+Klick: Bauteil auswählen · R: drehen (90°) ·\nBild↑/↓: anheben/absenken · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück" - -var main # Hauptfenster (script-erweitert, Variant) -var camera: Camera3D -var view_builder # Welt-Bauer (script-erweitert, Variant) -var interaction # Objekt-Interaktion (für Auswahl-Entkoppelung) - -var active := false -var kind_id := "fundament" -var ghost_rot := 0.0 # 90°-Schritte -var selected_index := -1 - -var _ghost: MeshInstance3D -var _cursor_valid := false -var _cursor := Vector3.ZERO - -# Terrain-Cache für den Boden-Raycast (lesen) -var _heights := PackedFloat32Array() -var _res := 0 -var _sx := 0.0 -var _sz := 0.0 - - -func setup(p_main, p_camera: Camera3D, p_builder, p_interaction) -> void: - main = p_main - camera = p_camera - view_builder = p_builder - interaction = p_interaction - _ensure_ghost() - - -func _ensure_ghost() -> void: - if _ghost != null and is_instance_valid(_ghost): - return - _ghost = MeshInstance3D.new() - _ghost.visible = false - var mat := StandardMaterial3D.new() - mat.albedo_color = Color(1.0, 0.75, 0.2, 0.45) - mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - mat.emission_enabled = true - mat.emission = Color(1.0, 0.6, 0.1) * 0.3 - _ghost.material_override = mat - add_child(_ghost) - - -# ------------------------------------------------------------ Aktivierung --- - -func activate() -> bool: - active = true - selected_index = -1 - ghost_rot = 0.0 - _cursor_valid = false - _refresh_from_doc() - _rebuild_ghost_mesh() - if view_builder != null: - view_builder.call("set_gebaeude_selected", -1) - return true - - -func deactivate() -> void: - active = false - selected_index = -1 - if _ghost != null and is_instance_valid(_ghost): - _ghost.visible = false - if view_builder != null: - view_builder.call("set_gebaeude_selected", -1) - - -## Nach Undo/Redo/Laden/Neu: Cache frisch, Auswahl nur halten, wenn das Teil -## noch existiert. -func refresh_from_doc() -> void: - _refresh_from_doc() - _rebuild_ghost_mesh() - if selected_index >= 0 and selected_index >= BuildOps.piece_count(main.doc): - selected_index = -1 - if active: - view_builder.call("set_gebaeude_selected", selected_index) - - -func _refresh_from_doc() -> void: - var terrain: Dictionary = main.doc.get("terrain", {}) - _res = int(terrain.get("resolution", 0)) - _sx = float(terrain.get("size_x_m", 0.0)) - _sz = float(terrain.get("size_z_m", 0.0)) - if _res >= 2 and _sx > 0.0 and _sz > 0.0: - _heights = SvmapIO.decode_heights(terrain) - else: - _heights = PackedFloat32Array() - - -# ------------------------------------------------------------- Werkzeug-UI -- - -func set_kind(next_kind: String) -> void: - if not BuildOps.KINDS.has(next_kind): - return - kind_id = next_kind - selected_index = -1 - if view_builder != null: - view_builder.call("set_gebaeude_selected", -1) - _rebuild_ghost_mesh() - if active: - main.call("_set_status", "%s ausgewählt — Linksklick setzt es aufs Raster (auch auf andere Bauteile zum Stapeln). Umschalt+Klick wählt gesetzte Teile aus." % BuildOps.kind_label(kind_id)) - - -func _rebuild_ghost_mesh() -> void: - if _ghost == null or not is_instance_valid(_ghost): - return - var probe := BuildOps.make_piece(kind_id, Vector3.ZERO, 0.0) - _ghost.mesh = BuildOps.piece_mesh(probe) - - -# --------------------------------------------------------------- Eingaben --- - -func _unhandled_input(event: InputEvent) -> void: - if not active: - return - if event is InputEventMouseMotion: - _update_cursor((event as InputEventMouseMotion).position) - return - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: - if mb.shift_pressed: - _select_at(mb.position) - else: - _place_piece() - get_viewport().set_input_as_handled() - # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — - # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug - # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten. - - -func _update_cursor(viewport_pos: Vector2) -> void: - var hit: Variant = _ray_hit(viewport_pos) - if hit == null: - _cursor_valid = false - if _ghost != null and is_instance_valid(_ghost): - _ghost.visible = false - return - _cursor_valid = true - _cursor = hit as Vector3 - if _ghost != null and is_instance_valid(_ghost): - _ghost.position = BuildOps.snapped_pos(kind_id, _cursor) - _ghost.rotation.y = deg_to_rad(ghost_rot) - _ghost.visible = true - - -## Strahl → Trefferpunkt: ZUERST auf die gebackenen Bauteile (Stapeln!), -## sonst auf das Terrain. Entfernung entscheidet. -func _ray_hit(viewport_pos: Vector2) -> Variant: - var origin := camera.project_ray_origin(viewport_pos) - var dir := camera.project_ray_normal(viewport_pos) - var best: Variant = null - var best_t := INF - var baked: Dictionary = view_builder.call("gebaeude_baked") - if not baked.get("faces", PackedVector3Array()).is_empty(): - var pick := BuildOps.ray_pick(baked, origin, dir) - if not pick.is_empty(): - best_t = (origin - (pick["punkt"] as Vector3)).length() - best = pick["punkt"] - var terrain: Dictionary = main.doc.get("terrain", {}) - if _res >= 2 and _heights.size() >= _res * _res: - var ground: Variant = TerrainField.raycast(_heights, _res, _sx, _sz, origin, dir) - if ground != null: - var t := (origin - (ground as Vector3)).length() - if t < best_t: - best = ground - return best - - -# ------------------------------------------------------------ Platzieren ---- - -func _place_piece() -> void: - if not _cursor_valid: - main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true) - return - var before: Dictionary = main.history.snapshot(["gebaeude"]) - var pos: Vector3 = BuildOps.snapped_pos(kind_id, _cursor) - var teil := BuildOps.make_piece(kind_id, pos, ghost_rot) - if teil.is_empty(): - return - BuildOps.ensure_block(main.doc).append(teil) - var label := BuildOps.kind_label(kind_id) - # Symmetrie (M4, t_ffa207b1): Zwilling anlegen — innerhalb desselben - # Undo-Schritts (Snapshot läuft um beide Mutationen herum). - var mirror_msg: String = BuildOps.place_mirrored(main.doc, teil, _sym_mode()) - main.history.commit("%s gesetzt" % label, before, main.history.snapshot(["gebaeude"])) - _after_build_change() - main.call("_set_status", "%s gesetzt (%d Bauteile).%s Weiter klicken zum Bauen — R dreht, Strg+Z nimmt das letzte Teil zurück." % [ - label, BuildOps.piece_count(main.doc), - "" if mirror_msg.is_empty() else " " + mirror_msg]) - - -func _select_at(viewport_pos: Vector2) -> void: - var origin := camera.project_ray_origin(viewport_pos) - var dir := camera.project_ray_normal(viewport_pos) - var baked: Dictionary = view_builder.call("gebaeude_baked") - var pick := BuildOps.ray_pick(baked, origin, dir) - if pick.is_empty(): - selected_index = -1 - view_builder.call("set_gebaeude_selected", -1) - main.call("_set_status", "Kein Bauteil getroffen — Umschalt+Klick direkt AUF ein Bauteil (Wand, Dach …).") - return - selected_index = int(pick["piece"]) - view_builder.call("set_gebaeude_selected", selected_index) - var label := _piece_label(selected_index) - main.call("_set_status", "%s ausgewählt — R drehen · Bild↑/↓ anheben · Entf löschen · Esc: Auswahl weg." % label) - - -func _piece_label(idx: int) -> String: - var list := BuildOps.pieces(main.doc) - if idx < 0 or idx >= list.size(): - return "Bauteil" - return "%s (Teil %d von %d)" % [BuildOps.kind_label(str((list[idx] as Dictionary).get("id", "?"))), idx + 1, list.size()] - - -func rotate_selected() -> void: - if selected_index < 0: - return - var list := BuildOps.pieces(main.doc) - if selected_index >= list.size(): - return - var teil: Dictionary = list[selected_index] - var before: Dictionary = main.history.snapshot(["gebaeude"]) - teil["rot"] = wrapf(float(teil.get("rot", 0.0)) + 90.0, 0.0, 360.0) - # Symmetrie (M4): Zwilling bekommt die gespiegelte neue Drehung. - BuildOps.update_mirrored(main.doc, selected_index, _sym_mode()) - main.history.commit("%s gedreht (90°)" % _piece_label(selected_index), - before, main.history.snapshot(["gebaeude"]), "bauen-rot:%d" % selected_index) - _after_build_change() - view_builder.call("set_gebaeude_selected", selected_index) - - -## Auswahl anheben/absenken (0,2 m — passend zur Fundament-Dicke), merge-gekoppelt. -func lift_selected(delta_m: float) -> void: - if selected_index < 0: - return - var list := BuildOps.pieces(main.doc) - if selected_index >= list.size(): - return - var teil: Dictionary = list[selected_index] - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var before: Dictionary = main.history.snapshot(["gebaeude"]) - teil["pos"] = [float(pos[0]), BuildOps.snap_y(float(pos[1]) + delta_m), float(pos[2])] - # Symmetrie (M4): Zwilling heben/absenken mit (y bleibt gespiegelt identisch). - BuildOps.update_mirrored(main.doc, selected_index, _sym_mode()) - main.history.commit("%s %s" % [_piece_label(selected_index), - "angehoben" if delta_m > 0.0 else "abgesenkt"], - before, main.history.snapshot(["gebaeude"]), "bauen-lift:%d" % selected_index) - _after_build_change() - view_builder.call("set_gebaeude_selected", selected_index) - - -## Pfeiltasten (Karte t_55257792): Bauteil im 2-m-Modulraster verschieben — -## EINE Zelle pro Tastendruck. Das exakte ±2-m-Delta erhält die Raster- -## Ausrichtung ("ecke" = gerade Meter, "mitte" = ungerade Meter bleibt), Y -## bleibt (Höhe ändert die Pfeiltaste nicht). EIN Undo-Schritt pro Tastendruck. -func nudge_selected(delta: Vector3) -> void: - if selected_index < 0: - main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus, dann verschieben die Pfeiltasten es im 2-m-Raster.") - return - var list := BuildOps.pieces(main.doc) - if selected_index >= list.size(): - selected_index = -1 - return - var teil: Dictionary = list[selected_index] - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var before: Dictionary = main.history.snapshot(["gebaeude"]) - var nx := float(pos[0]) + delta.x - var nz := float(pos[2]) + delta.z - teil["pos"] = [nx, float(pos[1]), nz] - # Symmetrie (M4): Zwilling wandert gespiegelt mit (Raster-Parität bleibt). - BuildOps.update_mirrored(main.doc, selected_index, _sym_mode()) - main.history.commit("%s verschoben (Pfeiltaste)" % _piece_label(selected_index), - before, main.history.snapshot(["gebaeude"])) - _after_build_change() - view_builder.call("set_gebaeude_selected", selected_index) - main.call("_set_status", "%s — 2 m pro Tastendruck, Raster bleibt eingehalten." % _piece_label(selected_index)) - - -## Strg+Bild↑ (Karte t_55257792): Bauteil direkt auf das Terrain absetzen -## (Fußpunkt = Terrain-Höhe, auf 5 cm gerastet). EIN Undo-Schritt. -func snap_selected_to_floor() -> void: - if selected_index < 0: - main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus, dann setzt Strg+Bild↑ es auf den Boden.", true) - return - var list := BuildOps.pieces(main.doc) - if selected_index >= list.size(): - selected_index = -1 - return - var teil: Dictionary = list[selected_index] - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var ground := SvmapDocument.terrain_height_at(main.doc, float(pos[0]), float(pos[2])) - var target := BuildOps.snap_y(ground) - if absf(float(pos[1]) - target) < 0.0001: - main.call("_set_status", "%s liegt schon auf dem Boden." % _piece_label(selected_index)) - return - var before: Dictionary = main.history.snapshot(["gebaeude"]) - teil["pos"] = [float(pos[0]), target, float(pos[2])] - # Symmetrie (M4): Zwilling mit absenken (y bleibt gespiegelt identisch). - BuildOps.update_mirrored(main.doc, selected_index, _sym_mode()) - main.history.commit("%s auf den Boden gesetzt" % _piece_label(selected_index), - before, main.history.snapshot(["gebaeude"])) - _after_build_change() - view_builder.call("set_gebaeude_selected", selected_index) - main.call("_set_status", "%s auf den Boden gesetzt — Strg+Z nimmt es zurück." % _piece_label(selected_index)) - - -func delete_selected() -> void: - if selected_index < 0: - main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus (Entf löscht es dann).", true) - return - var list: Array = BuildOps.pieces(main.doc) - if selected_index >= list.size(): - selected_index = -1 - return - var label := _piece_label(selected_index) - var before: Dictionary = main.history.snapshot(["gebaeude"]) - # Symmetrie (M4): bei eingeschalteter Symmetrie den sym-Zwilling mit löschen. - var mirror_msg: String = BuildOps.remove_mirrored(main.doc, selected_index, _sym_mode()) - main.history.commit("%s gelöscht" % label, before, main.history.snapshot(["gebaeude"])) - selected_index = -1 - _after_build_change() - main.call("_set_status", "%s — Strg+Z holt es zurück.%s" % [ - label.replace(" (Teil", " gelöscht (Teil"), - "" if mirror_msg.is_empty() else " " + mirror_msg]) - - -func _after_build_change() -> void: - main.call("_after_structure_change") - if selected_index >= 0 and selected_index < BuildOps.piece_count(main.doc): - view_builder.call("set_gebaeude_selected", selected_index) - - -## R-Taste: Auswahl drehen — sonst die Vorschau (90°-Raster). -func rotate_input() -> void: - if selected_index >= 0: - rotate_selected() - return - ghost_rot = wrapf(ghost_rot + 90.0, 0.0, 360.0) - if _ghost != null and is_instance_valid(_ghost): - _ghost.rotation.y = deg_to_rad(ghost_rot) - main.call("_set_status", "Vorschau gedreht auf %.0f° — Linksklick setzt das Bauteil so." % ghost_rot) - - -## Teil per Baum/Index anwählen (ohne Maus — auch für Demos/Tests). -func select_index(idx: int) -> void: - if idx < 0 or idx >= BuildOps.piece_count(main.doc): - selected_index = -1 - view_builder.call("set_gebaeude_selected", -1) - return - selected_index = idx - view_builder.call("set_gebaeude_selected", idx) - main.call("_set_status", "%s ausgewählt — R drehen · Bild↑/↓ anheben · Entf löschen · Rechtsklick: Auswahl weg." % _piece_label(idx)) - - -# ---------------------------------------------------------- Demo/Headless --- - -## Demo-Hook: Bauteil programmatisch setzen (inkl. History) — exakt wie Klick. -func demo_place(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> Dictionary: - var before: Dictionary = main.history.snapshot(["gebaeude"]) - var teil := BuildOps.make_piece(p_kind, pos, rot_deg) - BuildOps.ensure_block(main.doc).append(teil) - # Symmetrie (M4): Zwilling im selben Undo-Schritt (wie beim Klick). - BuildOps.place_mirrored(main.doc, teil, _sym_mode()) - main.history.commit("%s gesetzt" % BuildOps.kind_label(p_kind), - before, main.history.snapshot(["gebaeude"])) - return teil - - -## Aktiver Symmetrie-Modus aus main (M1-Schalter). Defensive über main.get(): -## Mock-Mains (Tests) haben sym_mode nicht — dann gilt OFF (altes Verhalten), -## statt mit SCRIPT ERROR abzustürzen (Lektion: SCRIPT ERROR in -s-Suiten = rot). -func _sym_mode() -> int: - var v: Variant = main.get("sym_mode") if main != null else null - return SymmetryCore.Mode.OFF if v == null else int(v) - - -## Demo-Hook: Ghost an Position parken (für Screenshots ohne Maus). -func demo_park_ghost(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> void: - set_kind(p_kind) - _cursor = pos - _cursor_valid = true - if _ghost != null and is_instance_valid(_ghost): - _ghost.position = BuildOps.snapped_pos(p_kind, pos) - _ghost.rotation.y = deg_to_rad(rot_deg) - _ghost.visible = true diff --git a/src/editor/build_tool.gd.uid b/src/editor/build_tool.gd.uid deleted file mode 100644 index 5cea67f..0000000 --- a/src/editor/build_tool.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://vukeqokihvko diff --git a/src/editor/interaction.gd b/src/editor/interaction.gd index 26cdf41..4f98c15 100644 --- a/src/editor/interaction.gd +++ b/src/editor/interaction.gd @@ -46,6 +46,13 @@ var _ghost_mi: MeshInstance3D # Kind-Mesh, gehoben um half_height — # exakt wie der Viewport-Bauer (map_view_builder). var _ghost_active := false # Ghost sichtbar weil Cursor über der Map ist +# Bauen-Modus (Bauteile bau_*): der Ghost rastet über BuildSnap (Kante/Zelle/ +# Ebene), jeder Klick setzt ein Teil in das laufende Gebäude (= Gruppe). +var build_group_id := -1 # laufendes Gebäude; -1 = nächstes Teil beginnt ein neues +var _build_last: Dictionary = {} # letztes BuildSnap-Ergebnis (pos/rot/ersetze/ok/grund) +var _build_hit: Dictionary = {} # letzter Treffer {punkt, obj, dir} +var _build_hint := "" # letzte Statuszeile des Ghosts (kein Spam) + # Terrain-Höhen-Cache (Karte t_4092e3a0): der Objekt-Cursor trifft das GELÄNDE # (TerrainField.raycast, wie Terrain-/Bauen-/Spawn-Werkzeug), nicht mehr nur # die flache Basis-Ebene. Dekodiert einmal pro Map-Änderung, nicht pro Mausbewegung. @@ -217,12 +224,19 @@ func start_placement(oid: String) -> void: place_rot_deg = 0.0 place_tilt_x = 0.0 place_tilt_z = 0.0 - _place_series = false # U5: jeder frische Start startet im Einmal-Modus + _place_series = BauKatalog.ist_bau(oid) # U5: Einmal-Modus; Bauteile = Serie (Esc beendet) _ghost_active = false + _build_last = {} + _build_hit = {} + _build_hint = "" refresh_terrain_cache() # Cursor trifft das GELÄNDE — Cache muss frisch sein _ensure_ghost() _ensure_ghost_visible() var label: String = main.object_label(oid) + if BauKatalog.ist_bau(oid): + main.call("_set_status", "%s: Maus über die Karte — die Vorschau rastet auf Zelle/Kante ein. Klick setzt, R dreht, Esc beendet. Klick auf die Wandseite ersetzt das Teil (z. B. Tür), Klick auf die Oberkante stapelt." % label) + main.call("_highlight_library", oid) + return var hinweis := "" if not GlbMeshStore.fehlt(oid) \ else " (Platzhalter — echte GLB fehlt für %s)" % oid main.call("_set_status", "%s%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, ESC bricht ab (Rechte Maustaste = Flug)." % [label, hinweis]) @@ -236,6 +250,8 @@ func cancel_placement() -> void: place_oid = "" _place_series = false _ghost_active = false + _build_last = {} + _build_hit = {} main.call("_highlight_library", "") main.call("_base_tool_status") @@ -303,6 +319,9 @@ func _ensure_ghost_visible() -> void: func _ghost_rotation_anwenden() -> void: if _ghost == null or not is_instance_valid(_ghost): return + if BauKatalog.ist_bau(place_oid): + _bau_ghost_neu() # Drehung ergibt sich aus Kante/Zelle + R + return _ghost.rotation_degrees = Vector3(place_tilt_x, place_rot_deg, place_tilt_z) @@ -323,6 +342,9 @@ func _placement_grid_enabled() -> bool: func _ghost_update(viewport_pos: Vector2) -> void: + if BauKatalog.ist_bau(place_oid): + _bau_ghost_update(viewport_pos) + return var p: Variant = _ground_point(viewport_pos) if p == null: _ghost_active = false @@ -337,6 +359,9 @@ func _ghost_update(viewport_pos: Vector2) -> void: func _place_object(viewport_pos: Vector2, mb_shift_held := false) -> void: + if BauKatalog.ist_bau(place_oid): + _place_bau(viewport_pos) + return var p: Variant = _ground_point(viewport_pos) if p == null: main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true) @@ -415,6 +440,184 @@ func _place_object(viewport_pos: Vector2, mb_shift_held := false) -> void: main.call("_set_status", "%s gesetzt und ausgewählt. Ziehen verschiebt, R oder Mausrad dreht, Entf löscht, Strg+Z nimmt es zurück." % main.object_label(placed_oid)) +# ----------------------------------------------------- Bauen (bau_*) ----- + +## Strahl → Treffer auf Gelände ODER einem gesetzten Objekt (Bauteile müssen +## sich auf Wänden/Böden stapeln lassen). Rückgabe {punkt, obj (-1 = Gelände), dir} +## oder {} bei Leerlauf. +func _build_ray_hit(viewport_pos: Vector2) -> Dictionary: + var origin := camera.project_ray_origin(viewport_pos) + var dir := camera.project_ray_normal(viewport_pos) + var ground: Variant = _ground_point(viewport_pos) + var best_t := INF + var out := {} + if ground != null: + best_t = (ground as Vector3).distance_to(origin) + out = {"punkt": ground, "obj": -1, "dir": dir} + for marker in view_builder.object_markers: + if marker == null or not is_instance_valid(marker): + continue + var center: Vector3 = marker.global_position + Vector3.UP * float(marker.get_meta("half_height", 1.0)) + var to_center := center - origin + var tca := to_center.dot(dir) + if tca <= 0.0: + continue + var radius := float(marker.get_meta("pick_radius", 1.0)) + if to_center.length_squared() - tca * tca > radius * radius: + continue + var mi := SelectionMath.marker_mesh(marker) + if mi == null or mi.mesh == null: + continue + var mt := SelectionMath.ray_mesh_distance(mi.global_transform, + SelectionMath.mesh_faces(mi.mesh), origin, dir) + if mt >= 0.0 and mt < best_t: + best_t = mt + out = {"punkt": origin + dir * mt, "obj": int(marker.get_meta("obj_index", -1)), "dir": dir} + return out + + +func _bau_ghost_update(viewport_pos: Vector2) -> void: + _build_hit = _build_ray_hit(viewport_pos) + if _build_hit.is_empty(): + _ghost_active = false + _ghost.visible = false + _build_last = {} + return + _bau_ghost_neu() + + +## Snap neu berechnen (Mausbewegung ODER R-Taste) und den Ghost setzen. +func _bau_snap() -> Dictionary: + if _build_hit.is_empty(): + return {} + var gelaende := func(x: float, z: float) -> float: + if _res >= 2 and _heights.size() >= _res * _res: + return TerrainField.height_at(_heights, _res, _sx, _sz, x, z) + return float(main.doc.get("terrain", {}).get("min_height_m", 0.0)) + return BuildSnap.berechne(place_oid, _build_hit["punkt"] as Vector3, place_rot_deg, + BuildSnap.sammle_teile(main.doc), gelaende, + int(_build_hit.get("obj", -1)) >= 0, _build_hit.get("dir", Vector3.ZERO) as Vector3) + + +func _bau_ghost_neu() -> void: + _build_last = _bau_snap() + if _build_last.is_empty(): + return + var pos: Vector3 = _build_last["pos"] + var half := _terrain_half() + var innerhalb := absf(pos.x) <= half.x and absf(pos.z) <= half.y + _ghost.position = pos + _ghost.rotation_degrees = Vector3(0.0, float(_build_last["rot"]), 0.0) + _ghost_active = true + _ghost.visible = true + var mat := _ghost_mi.material_override as StandardMaterial3D + var hinweis := "" + if not bool(_build_last["ok"]) or not innerhalb: + mat.albedo_color = Color(1.0, 0.25, 0.2, 0.5) + hinweis = str(_build_last["grund"]) if not bool(_build_last["ok"]) else "Außerhalb der Map." + elif int(_build_last["ersetze"]) >= 0: + mat.albedo_color = Color(1.0, 0.7, 0.2, 0.55) + hinweis = "Klick ersetzt das vorhandene Teil durch %s (Strg+Z holt es zurück)." % main.object_label(place_oid) + else: + mat.albedo_color = Color(0.6, 0.9, 1.0, 0.45) + if hinweis != _build_hint: + _build_hint = hinweis + if not hinweis.is_empty(): + main.call("_set_status", hinweis) + + +## Klick im Bauen-Modus: Teil setzen/ersetzen — ein Undo-Schritt, Teil landet im +## laufenden Gebäude (Gruppe), Serie bleibt aktiv (Esc beendet). +func _place_bau(viewport_pos: Vector2) -> void: + _build_hit = _build_ray_hit(viewport_pos) + if _build_hit.is_empty(): + main.call("_set_status", "Da war nichts unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true) + return + var snap := _bau_snap() + if snap.is_empty(): + return + var pos: Vector3 = snap["pos"] + var half := _terrain_half() + if absf(pos.x) > half.x or absf(pos.z) > half.y: + main.call("_set_status", "Das wäre außerhalb der Map gelandet — klicke innerhalb der grünen Fläche.", true) + return + if not bool(snap["ok"]): + main.call("_set_status", str(snap["grund"]), true) + return + var objects: Array = main.doc["objects"] + var ersetzt := int(snap["ersetze"]) >= 0 + var lib: Dictionary = main.manifest.get("objects", {}) + var poly := float((lib.get(place_oid, {}) as Dictionary).get("poly", 0)) + var sym_mode: int = _sym_mode() + var sym_factor := 1.0 + if sym_mode != SymmetryCore.Mode.OFF and not SymmetryCore.maps_to_self( + sym_mode, pos, SymmetryCore.map_center(main.doc)): + sym_factor = 2.0 + if not ersetzt and objects.size() + int(sym_factor) > SvmapIO.MAX_OBJECTS: + main.call("_set_status", "Limit erreicht: In eine Map passen maximal %d Objekte — das Löschen von Objekten macht wieder Platz." % SvmapIO.MAX_OBJECTS, true) + return + var used := SvmapDocument.poly_usage(main.doc, main.manifest) + var budget := float(SvmapIO.MAX_TOTAL_POLY) + if used + poly * sym_factor > budget: + main.call("_set_status", + "Kein Platz mehr im Poly-Budget (%s von %s belegt) — lass ein Teil weg oder drück Strg+Z." + % [main._fmt_int(int(used)), main._fmt_int(int(budget))], true) + return + var keys := ["objects", "group_names"] + var before: Dictionary = main.history.snapshot(keys) + if not before.has("group_names"): + before["group_names"] = null # Undo entfernt die neu angelegten Gebäude-Namen wieder + var label: String = main.object_label(place_oid) + var gid := _bau_gruppe() + var spiegel := -1 + if sym_mode != SymmetryCore.Mode.OFF and sym_factor > 1.0: + spiegel = _bau_spiegel_gruppe(gid) + BauOps.setze_teil(main.doc, place_oid, snap, gid, sym_mode, spiegel) + main.history.commit("%s bauen" % label if not ersetzt else "%s einsetzen" % label, + before, main.history.snapshot(keys)) + main.call("_after_structure_change") + _build_hit = _build_ray_hit(viewport_pos) + _bau_ghost_neu() + main.call("_set_status", "%s %s (%s, %d Teile). Weiter klicken — R dreht, Esc beendet, Strg+Z nimmt das letzte Teil zurück." % [ + label, "eingesetzt" if ersetzt else "gesetzt", GroupState.display_name(main.doc, gid), + GroupOps.members_of(main.doc, gid).size()]) + main.call("_refresh_bau_info") + + +## Laufendes Gebäude (Gruppe): bestehende Gruppe weiterführen oder neue anlegen +## und benennen. Rückgabe: Gruppen-ID. +func _bau_gruppe() -> int: + if build_group_id >= 0 and not GroupOps.members_of(main.doc, build_group_id).is_empty(): + return build_group_id + build_group_id = BauOps.gruppe_neu(main.doc) + return build_group_id + + +## Gruppe für den Spiegel-Zwilling des laufenden Gebäudes (1 Gebäude ↔ 1 Spiegel). +var _bau_spiegel: Dictionary = {} # laufende ID → Spiegel-ID + +func _bau_spiegel_gruppe(gid: int) -> int: + var sp := int(_bau_spiegel.get(gid, -1)) + if sp >= 0 and not GroupOps.members_of(main.doc, sp).is_empty(): + return sp + sp = maxi(GroupOps.next_group_id(main.doc), gid + 1) + GroupState.set_group_name(main.doc, sp, "%s (Spiegel)" % GroupState.display_name(main.doc, gid)) + _bau_spiegel[gid] = sp + return sp + + +## Nächstes Teil beginnt ein NEUES Gebäude (Button „Neues Gebäude“). +func neues_gebaeude() -> void: + build_group_id = -1 + _bau_spiegel.clear() + + +## Demo-Hook: Ghost an einem Punkt parken (Screenshots ohne Maus). +func demo_park_bau(punkt: Vector3) -> void: + _build_hit = {"punkt": punkt, "obj": -1, "dir": Vector3.ZERO} + _bau_ghost_neu() + + # --------------------------------------------------------------- Auswahl ---- ## Einzel-Auswahl (Klick ins Objekt / Klick im Baum): ersetzt die Auswahl. @@ -630,7 +833,9 @@ func delete_selected() -> void: # Symmetrie (M3, t_34908425): die Partner der Auswahl löschen sich MIT — # ein Paar lebt oder stirbt zusammen, IM SELBEN Undo-Schritt. doomed = ObjectOps.symmetry_expand_delete(main.doc, doomed, _sym_mode()) - var before: Dictionary = main.history.snapshot(["objects"]) + var before: Dictionary = main.history.snapshot(["objects", "group_names"]) + if not before.has("group_names"): + before["group_names"] = null var label: String if doomed.size() == 1: label = "%s löschen" % main.object_label(str(objects[doomed[0]].get("id", ""))) @@ -641,7 +846,8 @@ func delete_selected() -> void: doomed.reverse() for i in doomed: objects.remove_at(i) - main.history.commit(label, before, main.history.snapshot(["objects"])) + GroupState.prune_orphans(main.doc) # Gebäude ohne Teile verlieren ihren Namen + main.history.commit(label, before, main.history.snapshot(["objects", "group_names"])) clear_selection(true) main.call("_after_structure_change") main.call("_set_status", "%s ist erledigt — Strg+Z (Rückgängig) holt %s zurück." % [ @@ -698,8 +904,23 @@ func duplicate_selected() -> void: "die Kopien" if sources.size() > 1 else "eine Kopie", main._fmt_int(int(used)), main._fmt_int(int(budget))], true) return - var before: Dictionary = main.history.snapshot(["objects"]) - var new_indices: Array = ObjectOps.duplicate_objects(main.doc, sources, bool(main.get("grid_snap"))) + var before: Dictionary = main.history.snapshot(["objects", "group_names"]) + if not before.has("group_names"): + before["group_names"] = null # Undo entfernt die Namen der Kopie-Gruppen wieder + # Gebäude (Bauteile): Kopie rückt um die Gebäudebreite weg statt 2 m — sonst + # läge sie halb im Original. + var offset_x := ObjectOps.DUPLICATE_OFFSET_M.x + var bau_min := INF + var bau_max := -INF + for i in sources: + var so: Dictionary = objects[i] + if BauKatalog.ist_bau(str(so.get("id", ""))): + var spx := float((so.get("pos", [0.0, 0.0, 0.0]) as Array)[0]) + bau_min = minf(bau_min, spx) + bau_max = maxf(bau_max, spx) + if bau_min < INF: + offset_x = maxf(offset_x, ceilf((bau_max - bau_min + 4.0) / 2.0) * 2.0) + var new_indices: Array = ObjectOps.duplicate_objects(main.doc, sources, bool(main.get("grid_snap")), offset_x) if new_indices.is_empty(): clear_selection(true) return @@ -717,7 +938,7 @@ func duplicate_selected() -> void: label = "%s duplizieren" % main.object_label(str((objects[sources[0]] as Dictionary).get("id", ""))) else: label = "%d Objekte duplizieren" % new_indices.size() - main.history.commit(label, before, main.history.snapshot(["objects"])) + main.history.commit(label, before, main.history.snapshot(["objects", "group_names"])) main.call("_after_structure_change") # Kopien direkt anwählen — Sly kann sie gleich weiterziehen. Die frischen # Zwillinge wandern mit in die Auswahl (beide Seiten sichtbar markiert). @@ -1195,7 +1416,7 @@ func _unhandled_input(event: InputEvent) -> void: if _camera_flying(): return # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen if placing: - place_rot_deg = fmod(place_rot_deg + ROT_STEP_DEG + 3600.0, 360.0) + place_rot_deg = fmod(place_rot_deg + (90.0 if BauKatalog.ist_bau(place_oid) else ROT_STEP_DEG) + 3600.0, 360.0) _ghost_rotation_anwenden() get_viewport().set_input_as_handled() elif selected_index >= 0 and not _grabbing: @@ -1205,7 +1426,7 @@ func _unhandled_input(event: InputEvent) -> void: if _camera_flying(): return # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen if placing: - place_rot_deg = fmod(place_rot_deg - ROT_STEP_DEG + 3600.0, 360.0) + place_rot_deg = fmod(place_rot_deg - (90.0 if BauKatalog.ist_bau(place_oid) else ROT_STEP_DEG) + 3600.0, 360.0) _ghost_rotation_anwenden() get_viewport().set_input_as_handled() elif selected_index >= 0 and not _grabbing: diff --git a/src/editor/library_browser.gd b/src/editor/library_browser.gd index ff1387b..b96f1b3 100644 --- a/src/editor/library_browser.gd +++ b/src/editor/library_browser.gd @@ -108,8 +108,9 @@ const GROUP_IDS := { ## Diese Zuordnung zeigt Pack-/Pipeline-Objekte in sinnvollen Gruppen statt ## alles in einer Mega-„Sonstiges"-Gruppe abzulegen (das war der eigentliche ## Übersichts-Killer). Reihenfolge = Anzeige-Reihenfolge nach GROUP_ORDER. -const KAT_ORDER := ["mauern", "barrieren", "gebaeude", "requisiten", "fahrzeuge", "natur_uebrig"] +const KAT_ORDER := ["bauteile", "mauern", "barrieren", "gebaeude", "requisiten", "fahrzeuge", "natur_uebrig"] const KAT_GROUPS := { + "bauteile": {"key": "bauteile", "label": "Bauteile (eigene Häuser)"}, "walls": {"key": "mauern", "label": "Mauern"}, "barriers": {"key": "barrieren", "label": "Barrieren"}, "buildings": {"key": "gebaeude", "label": "Gebäude"}, @@ -124,11 +125,15 @@ const THUMB_DISPLAY_PX := 128 static func object_label(oid: String) -> String: + if BauKatalog.ist_bau(oid): + return BauKatalog.label(oid) return NAMES.get(oid, oid.capitalize()) ## Gruppe einer ID (Objekte, die keine festen Gruppe haben → "sonstiges"). static func group_of(oid: String) -> String: + if BauKatalog.ist_bau(oid): + return "bauteile" for key in GROUP_IDS: if (GROUP_IDS[key] as Array).has(oid): return str(key) diff --git a/src/editor/object_ops.gd b/src/editor/object_ops.gd index 3edd966..f9e0e69 100644 --- a/src/editor/object_ops.gd +++ b/src/editor/object_ops.gd @@ -99,7 +99,8 @@ static func snap_coord(value: float, enabled: bool, grid_m := GRID_M) -> float: ## Höhe — der Füßpunkt (pos.y) bleibt exakt. Rückgabe: neuer Index (-1 = ## ungültig). VOR dem Aufruf prüft die UI Budget/Objekt-Limit (hilfs-Meldung ## statt Fehler). -static func duplicate_object(doc: Dictionary, index: int, grid_enabled: bool) -> int: +static func duplicate_object(doc: Dictionary, index: int, grid_enabled: bool, + offset_x := DUPLICATE_OFFSET_M.x) -> int: var objects: Array = doc.get("objects", []) if index < 0 or index >= objects.size(): return -1 @@ -109,7 +110,7 @@ static func duplicate_object(doc: Dictionary, index: int, grid_enabled: bool) -> var copy: Dictionary = (source as Dictionary).duplicate(true) var pos: Array = copy.get("pos", [0.0, 0.0, 0.0]) if (pos as Array).size() == 3: - var nx := float(pos[0]) + DUPLICATE_OFFSET_M.x + var nx := float(pos[0]) + offset_x var nz := float(pos[2]) + DUPLICATE_OFFSET_M.y copy["pos"] = [snap_coord(nx, grid_enabled), float(pos[1]), snap_coord(nz, grid_enabled)] objects.append(copy) @@ -121,15 +122,37 @@ static func duplicate_object(doc: Dictionary, index: int, grid_enabled: bool) -> ## der Quellen bleibt erhalten, alle Indizes sind Originale (nur angehängt). ## Rückgabe: Liste der NEUEN Indizes (in Quell-Reihenfolge). Die UI commitet ## EINEN Undo-Schritt für alle Kopien zusammen. -static func duplicate_objects(doc: Dictionary, indices: Array, grid_enabled: bool) -> Array: +static func duplicate_objects(doc: Dictionary, indices: Array, grid_enabled: bool, + offset_x := DUPLICATE_OFFSET_M.x) -> Array: var new_indices: Array = [] for i in indices: - var ni := duplicate_object(doc, int(i), grid_enabled) + var ni := duplicate_object(doc, int(i), grid_enabled, offset_x) if ni >= 0: new_indices.append(ni) + regroup_copies(doc, new_indices) return new_indices +## Kopien gruppierter Objekte bilden eine NEUE Gruppe je Original-Gruppe — sonst +## würde die Kopie eines Gebäudes mit dem Original zu EINER Gruppe verschmelzen +## (Gruppen-ID wird von duplicate(true) mitkopiert). Name: " (Kopie)". +static func regroup_copies(doc: Dictionary, new_indices: Array) -> void: + var objects: Array = doc.get("objects", []) + var mapping := {} + for ni in new_indices: + var obj: Variant = objects[int(ni)] + var old_gid := GroupOps.group_id(obj) + if old_gid == GroupOps.UNGROUPED: + continue + if not mapping.has(old_gid): + var gid := GroupOps.next_group_id(doc) + mapping[old_gid] = gid + var old_name := GroupState.display_name(doc, old_gid) + objects[int(ni)]["group"] = gid # sofort belegen, damit next_group_id weiterzählt + GroupState.set_group_name(doc, gid, "%s (Kopie)" % old_name) + (obj as Dictionary)["group"] = int(mapping[old_gid]) + + ## Anheben/Absenken in LIFT_STEP_M-Schritten (delta_m positiv = anheben). ## Objekte dürfen vom Terrain abweichen — die Höhe wohnt direkt in object.pos.y ## (das svmap-Format erlaubt freie y-Werte innerhalb der Match-Grenzen, V9 — diff --git a/src/editor/playtest/playtest_mode.gd b/src/editor/playtest/playtest_mode.gd index 741944b..dcf6342 100644 --- a/src/editor/playtest/playtest_mode.gd +++ b/src/editor/playtest/playtest_mode.gd @@ -9,7 +9,7 @@ extends Node ## - Kollider-Bau via KolliderBau (src/svmap/kollider_bau.gd — 1:1-Spiegel ## des Quest-Clients, Karte t_1da96275): Terrain-Trimesh + Objekt-Kollider ## (box/kugel/zylinder/verbund/trimesh inkl. library/kollisions-Quellen) -## + Bauteil-Trimeshe (BuildOps-Pieces: Treppen/Türen/Wände begehbar +## + Bauteil-Trimeshe (bau_*-Objekte: Treppen/Türen/Wände begehbar ## wie echte Geometrie). ## - PlaytestBody (CharacterBody3D, IDENTISCHE Parameter wie der ## Testmap-Spieler inkl. Step-Up 0.45 m) anlegen, Terrain-Raster EINMAL @@ -22,7 +22,6 @@ extends Node ## PlaytestBody; dieser Zustand managed nur Lifecycle + HUD. const BODY_SCRIPT := "res://src/editor/playtest/playtest_body.gd" -const BUILD_OPS_SCRIPT := "res://src/editor/build_ops.gd" const KOLLIDER_SCRIPT := "res://src/svmap/kollider_bau.gd" const AVATAR_SCRIPT := "res://src/editor/playtest/playtest_avatar.gd" @@ -188,30 +187,12 @@ func _spawn_start(doc: Dictionary) -> Vector3: ## - Terrain + Registry-Objekte: KolliderBau.baue_welt_kollider (1:1-Spiegel ## von svmap_world.gd — box/kugel/zylinder/verbund/trimesh inkl. ## library/kollisions-Quelldateien). -## - Bauteile (gebaeude): Trimesh aus dem gebackten Piece-Mesh — Türen -## bleiben Durchgänge, Treppen sind begehbar (wie die sichtbare Geometrie). +## - Bauteile (bau_*) sind normale Registry-Objekte mit Trimesh aus +## library/kollisions — Türen/Fenster bleiben Durchgänge, Treppen begehbar. func _baue_kollider(doc: Dictionary) -> void: var kb := load(KOLLIDER_SCRIPT) _kollider_root = kb.call("baue_welt_kollider", doc) add_child(_kollider_root) - var build_ops := load(BUILD_OPS_SCRIPT) - if build_ops == null: - return - for teil in doc.get("gebaeude", []): - if not (teil is Dictionary): - continue - var mesh: Mesh = build_ops.call("piece_mesh", teil) - if mesh == null: - continue - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var pb := StaticBody3D.new() - pb.name = "Bauteil_Kollider_%d" % _kollider_root.get_child_count() - var cs := CollisionShape3D.new() - cs.shape = mesh.create_trimesh_shape() - pb.add_child(cs) - pb.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2])) - pb.rotation.y = deg_to_rad(float(teil.get("rot", 0.0))) - _kollider_root.add_child(pb) # ------------------------------------------------------------------- HUD ---- diff --git a/src/editor/playtest/test_start_tool.gd b/src/editor/playtest/test_start_tool.gd index 5fe43a6..88ea183 100644 --- a/src/editor/playtest/test_start_tool.gd +++ b/src/editor/playtest/test_start_tool.gd @@ -102,27 +102,6 @@ func _click(viewport_pos: Vector2) -> void: var hit_obj := false if obj_idx is int and int(obj_idx) >= 0: hit_obj = true - # 2) Bauteil-Treffer? (AABB-Strahl über BuildOps.piece_aabb — build_ops.gd - # wird nur gelesen/nutzend gerufen, nicht geändert.) - var origin := camera.project_ray_origin(viewport_pos) - var dir := camera.project_ray_normal(viewport_pos) - var hit_build := false - if not hit_obj: - var t_terrain := INF - var ground: Variant = _ray_hit(viewport_pos) - if ground != null: - t_terrain = origin.distance_to(ground as Vector3) - var best_t := INF - for teil in main.doc.get("gebaeude", []): - if not (teil is Dictionary): - continue - var ab: AABB = BuildOps.piece_aabb(teil) - if ab.size.length_squared() <= 0.0: - continue - var t := TestStart.ray_aabb(origin, dir, ab) - if t >= 0.0 and t < best_t and t < t_terrain + 0.01: - best_t = t - hit_build = true # 3) Punkt setzen: x/z vom Strahl-Treffer, Höhe IMMER vom Terrain (nie # im Objekt schwebend, nie versunken). var hit: Variant = _ray_hit(viewport_pos) @@ -133,8 +112,6 @@ func _click(viewport_pos: Vector2) -> void: var hinweis := "" if hit_obj: hinweis = "auf dem Boden unter dem Objekt" - elif hit_build: - hinweis = "auf dem Boden unter dem Bauteil" main.call("_test_start_apply", p.x, p.z, hinweis) diff --git a/src/editor/selection_math.gd b/src/editor/selection_math.gd index 3a74548..56a3a5c 100644 --- a/src/editor/selection_math.gd +++ b/src/editor/selection_math.gd @@ -81,7 +81,7 @@ static var _faces_cache: Dictionary = {} ## Strahl gegen die ECHTEN Dreiecke eines Meshes (Möller–Trumbore über -## BuildOps._ray_tri). xform = globale Transform der MeshInstance (inkl. +## ray_tri). xform = globale Transform der MeshInstance (inkl. ## Objekt-Drehung/-Kippung, Platzhalter-Lift und Node-Scale). ## Rückgabe: Welt-Abstand vom Strahl-Ursprung zum nächsten Treffer, -1 = keiner. ## WARUM: Die Kugel-Näherung (pick_radius = halbe AABB-Diagonale) eines großen @@ -96,7 +96,7 @@ static func ray_mesh_distance(xform: Transform3D, faces: PackedVector3Array, var ld: Vector3 = inv.basis * dir var best := INF for i in int(faces.size() / 3): - var t := BuildOps._ray_tri(lo, ld, faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2]) + var t := ray_tri(lo, ld, faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2]) if t >= 0.0 and t < best: best = t if best == INF: @@ -121,3 +121,23 @@ static func marker_mesh(marker: Node3D) -> MeshInstance3D: if child is MeshInstance3D: return child as MeshInstance3D return null + + +## Möller–Trumbore: Abstand t entlang des Strahls zum Dreieck (a, b, c), -1 = kein Treffer. +static func ray_tri(origin: Vector3, dir: Vector3, a: Vector3, b: Vector3, c: Vector3) -> float: + var e1 := b - a + var e2 := c - a + var p := dir.cross(e2) + var det := e1.dot(p) + if absf(det) < 0.000001: + return -1.0 + var inv := 1.0 / det + var tv := origin - a + var u := tv.dot(p) * inv + if u < 0.0 or u > 1.0: + return -1.0 + var q := tv.cross(e1) + var v := dir.dot(q) * inv + if v < 0.0 or u + v > 1.0: + return -1.0 + return e2.dot(q) * inv diff --git a/src/editor/shortcut_policy.gd b/src/editor/shortcut_policy.gd index e6efe0d..21e7c8a 100644 --- a/src/editor/shortcut_policy.gd +++ b/src/editor/shortcut_policy.gd @@ -5,14 +5,14 @@ class_name ShortcutPolicy ## und macht nur noch den Text. tests/test_shortcuts.gd prüft alle Kombos. ## ## Priorität (spec „Esc = Auswahl aufheben / Werkzeug abbrechen / Ghost abbrechen"): -## 1. aktives Sonder-Werkzeug verlassen (Terrain/Spawns/Bauen → Objekt-Werkzeug) +## 1. aktives Sonder-Werkzeug verlassen (Terrain/Spawns → Objekt-Werkzeug) ## 2. Ghost-Preview (Platzieren) abbrechen ## 3. Auswahl aufheben ## Rückgabe-Werte: -## "exit_terrain" | "exit_spawns" | "exit_bauen" | "exit_teststart" | "cancel_grab" +## "exit_terrain" | "exit_spawns" | "exit_teststart" | "cancel_grab" ## "cancel_placement" | "clear_selection" | "none" ## grabbing (Karte t_55257792): G-Greifen läuft — Esc rollt die Positionen -## zurück (Blender-Semantik). Vorrang NACH Werkzeug-Exit (im Terrain-/Bauen- +## zurück (Blender-Semantik). Vorrang NACH Werkzeug-Exit (im Terrain- ## Werkzeug gibt es kein Greifen), VOR Platzieren/Greifen-Auswahlheben. static func esc_action(tool_mode: String, placing: bool, has_selection: bool, grabbing := false) -> String: @@ -20,8 +20,6 @@ static func esc_action(tool_mode: String, placing: bool, has_selection: bool, return "exit_terrain" if tool_mode == "spawns": return "exit_spawns" - if tool_mode == "bauen": - return "exit_bauen" if tool_mode == "teststart": # Karte t_96e3c4ee (Wiederanbindung 20.10.): Esc verlässt das # Test-Start-Werkzeug — der gesetzte Punkt BLEIBT bestehen. diff --git a/src/editor/symmetry_core.gd b/src/editor/symmetry_core.gd index 909830d..7c2ddcb 100644 --- a/src/editor/symmetry_core.gd +++ b/src/editor/symmetry_core.gd @@ -168,21 +168,18 @@ static func find_partner_index(list: Array, key: String, exclude_index: int = -1 ## Nächste freie sym-Kennung im Dokument ("sym_1", "sym_2", …): scannt -## Objekte UND Bau-Teile, deterministic (kein Zufall, keine Uhr). +## Objekte (inkl. Bauteile), deterministic (kein Zufall, keine Uhr). static func next_sym_key(doc: Dictionary) -> String: var used: Dictionary = {} - for block_key in ["objects", "gebaeude"]: - var block: Array = doc.get(block_key, []) - if not (block is Array): + var block: Array = doc.get("objects", []) + for item in block: + if not (item is Dictionary): continue - for item in block: - if not (item is Dictionary): - continue - var s := sym_of(item as Dictionary) - if s.begins_with("sym_"): - var tail := s.substr(4) - if tail.is_valid_int(): - used[int(tail)] = true + var s := sym_of(item as Dictionary) + if s.begins_with("sym_"): + var tail := s.substr(4) + if tail.is_valid_int(): + used[int(tail)] = true var n := 1 while used.has(n): n += 1 diff --git a/src/editor/symmetry_ops.gd b/src/editor/symmetry_ops.gd index 117f404..d117e50 100644 --- a/src/editor/symmetry_ops.gd +++ b/src/editor/symmetry_ops.gd @@ -3,7 +3,7 @@ class_name SymmetryOps ## M5 Ein-Klick „Karte symmetrisch machen“ + Symmetrie-Check (Karte t_60014669, ## Spec /opt/data/scopedvr/KARTEN_MIRROR_vc27.md). Baut auf M1–M4 auf: ## Spiegel-Mathe und Partner-Links kommen aus SymmetryCore, das Anwenden auf -## Objekten aus ObjectOps, auf Bau-Teilen aus BuildOps und auf Spawn-Zonen aus +## Objekten aus ObjectOps, auf Bauteilen (normale Objekte) und auf Spawn-Zonen aus ## dem Spawn-Spiegel-Mechanismus (spawn_tool.gd — dort NUR BENUTZT, nie ## geändert). Diese Datei orchestriert den GANZEN Vorgang und den Check: ## @@ -145,7 +145,6 @@ static func analyze(doc: Dictionary, mode: int) -> Dictionary: var an := { "mode": mode, "objects": {"pairs": [], "singles_low": [], "singles_high": [], "diff": 0}, - "gebaeude": {"pairs": [], "singles_low": [], "singles_high": [], "diff": 0}, "spawns": {"pairs": [], "singles_low": [], "singles_high": [], "diff": 0}, "terrain_cells": [], "material_cells": [], # Material-Raster (fein, Karte 24.10.) — eigenes Gitter @@ -160,16 +159,13 @@ static func analyze(doc: Dictionary, mode: int) -> Dictionary: if mode == SymmetryCore.Mode.OFF: return an _analyze_objects(doc, mode, an) - _analyze_gebaeude(doc, mode, an) _analyze_spawns(doc, mode, an) _analyze_terrain(doc, mode, an) - an["conflicts"] = int(an["objects"]["diff"]) + int(an["gebaeude"]["diff"]) \ + an["conflicts"] = int(an["objects"]["diff"]) \ + int(an["spawns"]["diff"]) + (an["terrain_cells"] as Array).size() \ + (an["material_cells"] as Array).size() an["adds"] = (an["objects"]["singles_low"] as Array).size() \ + (an["objects"]["singles_high"] as Array).size() \ - + (an["gebaeude"]["singles_low"] as Array).size() \ - + (an["gebaeude"]["singles_high"] as Array).size() \ + (an["spawns"]["singles_low"] as Array).size() \ + (an["spawns"]["singles_high"] as Array).size() an["total_deviations"] = int(an["_dev_total"]) @@ -301,53 +297,6 @@ static func _analyze_objects(doc: Dictionary, mode: int, an: Dictionary) -> void _push_dev(an, "Objekt #%d hat keinen Zwilling" % int(idx)) -static func _teil_differs(src: Dictionary, twin: Dictionary, mp: Vector3, - mode: int) -> bool: - var tpos := _pos_of(twin.get("pos", [])) - if Vector2(mp.x - tpos.x, mp.z - tpos.z).length() > POS_EPS_M: - return true - if absf(tpos.y - mp.y) > POS_EPS_M: - return true - var trot := fposmod(float(twin.get("rot", 0.0)) - - SymmetryCore.mirror_rot_y(mode, float(src.get("rot", 0.0))), 360.0) - var rotd := minf(trot, 360.0 - trot) - if rotd > ROT_EPS_DEG: - return true - return str(twin.get("id", "")) != str(src.get("id", "")) - - -static func _analyze_gebaeude(doc: Dictionary, mode: int, an: Dictionary) -> void: - var list: Array = doc.get("gebaeude", []) - var items: Array = [] - for i in list.size(): - var teil = list[i] - if not (teil is Dictionary): - continue - var pos: Array = teil.get("pos", []) - if (pos as Array).size() < 3: - continue - items.append({"idx": i, "pos": _pos_of(pos)}) - var paired := _pair_items(mode, doc, items) - var pairs: Array = paired["pairs"] - var diff_n := 0 - for p in pairs: - var low_i: int = p["low"] - var high_i: int = p["high"] - var low_ok: bool = _valid_link(list, low_i) == high_i - var high_ok: bool = _valid_link(list, high_i) == low_i - if not (low_ok and high_ok): - an["links"] = int(an["links"]) + 1 - if _teil_differs(list[low_i], list[high_i], p["mirror"], mode): - diff_n += 1 - _push_dev(an, "Bau-Teil #%d" % low_i) - an["gebaeude"]["pairs"] = pairs - an["gebaeude"]["singles_low"] = paired["singles_low"] - an["gebaeude"]["singles_high"] = paired["singles_high"] - an["gebaeude"]["diff"] = diff_n - for idx in (paired["singles_low"] as Array) + (paired["singles_high"] as Array): - _push_dev(an, "Bau-Teil #%d hat keinen Zwilling" % int(idx)) - - # ------------------------------------------------------------------ Spawns -- ## Nur Teams 0/1 (TDM-Konvention wie M4): der Spiegel-Zwilling eines Spawn- ## Punkts liegt im GEGNER-Team. Kopplung läuft über design_notes.spawn_mirror. @@ -538,12 +487,10 @@ static func apply(doc: Dictionary, mode: int, quelle_hoch: bool) -> Dictionary: var an := analyze(doc, mode) var c := { "objects_added": 0, "objects_fixed": 0, "objects_linked": 0, - "parts_added": 0, "parts_fixed": 0, "parts_linked": 0, "spawns_added": 0, "spawns_fixed": 0, "spawns_linked": 0, "terrain_cells": 0, "skipped": 0, } _apply_objects(doc, mode, an, quelle_hoch, c) - _apply_gebaeude(doc, mode, an, quelle_hoch, c) _apply_terrain(doc, mode, an, quelle_hoch, c) # Spawns ZULETZT: die Zwillinge sitzen auf der Terrain-Höhe — die stimmt # erst nach dem Boden-Ausgleich (M2/M4-Konvention: y = Terrainhöhe). @@ -553,18 +500,18 @@ static func apply(doc: Dictionary, mode: int, quelle_hoch: bool) -> Dictionary: static func _apply_report(c: Dictionary, quelle_hoch: bool, mode: int) -> String: var erg: Array = [] - for entry in [["objects_added", "Objekte ergänzt"], ["parts_added", "Bau-Teile ergänzt"], + for entry in [["objects_added", "Objekte ergänzt"], ["spawns_added", "Spawn-Punkte ergänzt"], ["terrain_cells", "Bodenstellen angeglichen"], - ["objects_fixed", "Objekt-Widersprüche"], ["parts_fixed", "Bau-Teil-Widersprüche"], + ["objects_fixed", "Objekt-Widersprüche"], ["spawns_fixed", "Spawn-Widersprüche"], ["objects_linked", "Objekt-Paare verknüpft"], - ["parts_linked", "Bau-Teil-Paare verknüpft"], ["spawns_linked", "Spawn-Paare verknüpft"]]: + ["spawns_linked", "Spawn-Paare verknüpft"]]: var n := int(c[entry[0]]) if n > 0: erg.append("%d %s" % [n, entry[1]]) if erg.is_empty(): return "Nichts zu tun — die Karte ist schon symmetrisch ✓" var sides := side_labels(mode) - var conflict_sum := int(c["objects_fixed"]) + int(c["parts_fixed"]) + int(c["spawns_fixed"]) + int(c["terrain_cells"]) + var conflict_sum := int(c["objects_fixed"]) + int(c["spawns_fixed"]) + int(c["terrain_cells"]) var text := "Fertig ✓ — " + ", ".join(PackedStringArray(erg)) if conflict_sum > 0: text += ". Bei %d Widersprüchen hat %s gewonnen" % [conflict_sum, str(sides[1] if quelle_hoch else sides[0])] @@ -618,42 +565,6 @@ static func _apply_objects(doc: Dictionary, mode: int, an: Dictionary, c["skipped"] = int(c["skipped"]) + 1 -static func _write_teil_from(twin: Dictionary, donor: Dictionary, mp: Vector3, - mode: int) -> void: - twin["pos"] = [mp.x, mp.y, mp.z] - twin["rot"] = SymmetryCore.mirror_rot_y(mode, float(donor.get("rot", 0.0))) - - -static func _apply_gebaeude(doc: Dictionary, mode: int, an: Dictionary, - quelle_hoch: bool, c: Dictionary) -> void: - var list: Array = doc.get("gebaeude", []) - if (list as Array).is_empty(): - return - for p in (an["gebaeude"]["pairs"] as Array): - var low_i: int = p["low"] - var high_i: int = p["high"] - var donor_i: int = high_i if quelle_hoch else low_i - var twin_i: int = low_i if quelle_hoch else high_i - var donor: Dictionary = list[donor_i] - var twin: Dictionary = list[twin_i] - var key_d := SymmetryCore.sym_of(donor) - var key_t := SymmetryCore.sym_of(twin) - if key_d != key_t or key_d.is_empty(): - # Ohne/widerstreitende Kopplung: frische gemeinsame Kennung. - _fresh_pair_link(doc, list, donor_i, twin_i) - c["parts_linked"] = int(c["parts_linked"]) + 1 - var mp := SymmetryCore.mirror_point(mode, _pos_of(donor.get("pos", [])), _center_of(doc)) - if _teil_differs(donor, twin, mp, mode): - _write_teil_from(twin, donor, mp, mode) - c["parts_fixed"] = int(c["parts_fixed"]) + 1 - for idx in (an["gebaeude"]["singles_low"] as Array) + (an["gebaeude"]["singles_high"] as Array): - var msg := BuildOps.place_mirrored(doc, list[int(idx)], mode) - if not msg.contains("V9") and not msg.contains("nicht gedoppelt"): - c["parts_added"] = int(c["parts_added"]) + 1 - else: - c["skipped"] = int(c["skipped"]) + 1 - - static func _apply_terrain(doc: Dictionary, mode: int, an: Dictionary, quelle_hoch: bool, c: Dictionary) -> void: _apply_materials(doc, mode, an, quelle_hoch, c) diff --git a/src/svmap/map_document.gd b/src/svmap/map_document.gd index edc315d..30a619d 100644 --- a/src/svmap/map_document.gd +++ b/src/svmap/map_document.gd @@ -52,7 +52,6 @@ static func create_new(map_name: String, size_m: float, weather: String) -> Dict "materials_r8_b64": Marshalls.raw_to_base64(zero_materials), }, "objects": [], - "gebaeude": [], # Bauen-Modus (Karte t_97ea59c5) — Bauteile, optional "spawn_zones": _default_spawn_zones(size), "bounds": { "min": [-half, -10.0, -half], @@ -271,13 +270,12 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float, var old_bounds: Dictionary = doc.get("bounds", {}) var old_min: Array = old_bounds.get("min", [-half_x, -10.0, -half_z]) var old_max: Array = old_bounds.get("max", [half_x, 80.0, half_z]) - return { + var out := { "format_version": SvmapIO.FORMAT_VERSION, "info": (doc.get("info", {}) as Dictionary).duplicate(), "terrain": terrain, # R1: Objekte/Bauteile/Spawns unverändert übernehmen (Filter in R2). "objects": (doc.get("objects", []) as Array).duplicate(), - "gebaeude": (doc.get("gebaeude", []) as Array).duplicate(), "spawn_zones": (doc.get("spawn_zones", {}) as Dictionary).duplicate(true), "bounds": { "min": [-half_x, float(old_min[1]), -half_z], @@ -285,6 +283,15 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float, }, "weather": doc.get("weather", ""), } + # Optionale Zusatz-Blöcke (group_names = Gebäude-/Gruppen-Namen, design_notes, + # Himmel …) gehören nicht dem Resize und wandern unverändert mit — nur die + # Prüfsumme wird verworfen. + for key in doc.keys(): + if not out.has(key) and key != "checksum": + var v: Variant = doc[key] + out[key] = (v as Array).duplicate(true) if v is Array \ + else ((v as Dictionary).duplicate(true) if v is Dictionary else v) + return out # ------------------------------------------------- R2: Out-of-Bounds-Liste ---- diff --git a/src/ui/help_text.gd b/src/ui/help_text.gd index c3f5643..b972474 100644 --- a/src/ui/help_text.gd +++ b/src/ui/help_text.gd @@ -59,9 +59,10 @@ static func text() -> String: ])) out += _header("Bauen (F3)") out += _table(_rows([ - ["Linksklick", "Bauteil aufs 2-m-Raster setzen, auch auf andere Teile (stapeln)"], - ["R / Pfeile / Bild↑↓", "drehen / verschieben / anheben"], - ["Umschalt+Klick", "Bauteil auswählen; Entf löscht"], + ["Teil im Tab Bauen", "wählen, dann in die Karte klicken — Wände rasten an die Zellkante, Böden/Dächer in die Zellmitte"], + ["Klick auf Wandoberkante", "stapelt das nächste Geschoss; Klick auf die Seite ersetzt das Teil (Wand → Tür)"], + ["R / Esc", "Teil umdrehen bzw. drehen / Bauen beenden"], + ["Gebäude", "jedes Haus ist eine Gruppe: „Ganzes Gebäude auswählen“, kopieren, löschen im Tab Bauen"], ])) out += _header("Spawn-Zonen (F4) und Test-Start (T)") out += _table(_rows([ diff --git a/src/ui/main.gd b/src/ui/main.gd index c8c0911..470cc46 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -57,8 +57,7 @@ var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von i var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds) var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1) var spawn_tool # src/editor/spawn_tool.gd (Zonen, Karte t_c4162c5d) -var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5) -var tool_mode := "objects" # "objects" | "terrain" | "spawns" | "bauen" (Modular-Builder) +var tool_mode := "objects" # "objects" | "terrain" | "spawns" | "teststart" (Bauen = Objekt-Platzieren mit Rastern) # Symmetrie-Modus (Karte t_f5935023, M1): Modus + Toolbar-Schalter # + Statuszeilen-Anzeige. Standard = Punktspiegelung (Sly 20.10.). M1 legt nur @@ -104,9 +103,6 @@ var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit) var _library_group_rows: Array = [] # weiter in assets_mod (Bequemlichkeit) var spawn_status: Label # weiter in check_mod var terrain_info: Label # weiter in map_mod -var build_edit_btn: Button # weiter in terrain_mod -var build_panel: VBoxContainer # weiter in terrain_mod -var build_kind_buttons: Dictionary = {} # weiter in terrain_mod var terrain_mode_lower: Button # weiter in terrain_mod (Test-Vertrag) var terrain_size_btn_small: Button # weiter in terrain_mod var terrain_size_btn_medium: Button # weiter in terrain_mod @@ -406,9 +402,6 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void: prop_name = map_mod.prop_name spawn_status = check_mod.spawn_status terrain_info = map_mod.terrain_info - build_edit_btn = bauen_mod.build_edit_btn - build_panel = bauen_mod.build_panel - build_kind_buttons = bauen_mod.build_kind_buttons # Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die # alten Namen als Aliase (Tests/Werkzeug-Vertrag). terrain_mod.terrain_palette = malen_mod.palette @@ -617,11 +610,6 @@ func _build_center_viewport() -> SubViewportContainer: world.add_child(spawn_tool) spawn_tool.setup(self, camera, view_builder, interaction) - # Bauen-Werkzeug (Karte t_97ea59c5) — ditto eigener Eingabe-Knoten. - build_tool = Node3D.new() - build_tool.set_script(load("res://src/editor/build_tool.gd")) - world.add_child(build_tool) - build_tool.setup(self, camera, view_builder, interaction) # Test-Start-Werkzeug (T1b, Wiederanbindung 20.10.): Marker in der Karte, # Klick setzt den Startpunkt. Lebt — wie die übrigen Werkzeuge — in main, @@ -1162,7 +1150,7 @@ func _start_playtest() -> void: ## Editor-Input einfrieren/freigeben (Kamera-Flug, Werkzeuge, Gizmo). ## Nur auf die Werkzeug-Nodes — die Sicht (view_builder) bleibt 1:1 stehen. func _playtest_set_frozen(on: bool) -> void: - for t in [camera, interaction, terrain_tool, spawn_tool, build_tool, test_start_tool]: + for t in [camera, interaction, terrain_tool, spawn_tool, test_start_tool]: if t == null: continue t.set_process_unhandled_input(not on) @@ -1235,14 +1223,19 @@ func _on_open_selected(path: String) -> void: # optisch gleich, ab jetzt ~1 m fein malbar. Die Datei ändert sich erst # beim nächsten Speichern (deshalb kein dirty). SvmapIO.upgrade_material_layer(doc.get("terrain", {})) + # Alte Bau-Sektion „gebaeude“ (vor 25.10.) → normale Bauteil-Objekte in einer Gruppe. + var uebernommen := BauOps.migriere_gebaeude(doc) current_path = path - dirty = false + dirty = uebernommen > 0 history.begin(doc) _clear_edit_state() _refresh_all() _test_start_reload() # Karte t_96e3c4ee: Sidecar der geladenen Map lesen var map_name := str(doc.get("info", {}).get("name", "?")) - _set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)]) + if uebernommen > 0: + _set_status("Map „%s“ geladen. %d alte Bauteile wurden in das neue Bausystem übernommen (Gruppe „Gebäude“) — Wandhöhe ist jetzt 3 m, bitte kurz prüfen und speichern." % [map_name, uebernommen]) + else: + _set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)]) func _clear_edit_state() -> void: @@ -1347,7 +1340,6 @@ func _update_tool_label() -> void: var names := { "objects": "Objekte (F2)", "terrain": "Terrain (F1)", - "bauen": "Bauen (F3)", "spawns": "Spawn-Zonen (F4)", } tool_label.text = "Werkzeug: %s" % str(names.get(tool_mode, tool_mode)) @@ -1489,7 +1481,7 @@ func _unhandled_key_input(event: InputEvent) -> void: KEY_F2: _set_tool_mode("objects") KEY_F3: - _set_tool_mode("bauen") + _open_bauen_tab() KEY_F4: _set_tool_mode("spawns") KEY_F5: @@ -1527,9 +1519,9 @@ func _unhandled_key_input(event: InputEvent) -> void: var r_delta := -90.0 if event.shift_pressed else 90.0 if ctrl and not event.alt_pressed: # Strg+R / Strg+Umschalt+R: KIPPEN um X (±15°, Karte t_e166e656 - # — „nicht nur rotieren, sondern auch kippen“). Im Bauen-Modus - # gibt es kein Kippen (Bauteile sind fix ausgerichtet). - if tool_mode == "bauen": + # — „nicht nur rotieren, sondern auch kippen“). Bauteile werden + # nie gekippt (fest ausgerichtet). + if BauKatalog.ist_bau(interaction.place_oid): return var t_delta: float = -interaction.TILT_STEP_DEG if event.shift_pressed else interaction.TILT_STEP_DEG if interaction.placing: @@ -1540,7 +1532,7 @@ func _unhandled_key_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() elif event.alt_pressed and not ctrl: # Alt+R / Alt+Umschalt+R: KIPPEN um Z (±15°, Karte t_e166e656). - if tool_mode == "bauen": + if BauKatalog.ist_bau(interaction.place_oid): return var t_delta2: float = -interaction.TILT_STEP_DEG if event.shift_pressed else interaction.TILT_STEP_DEG if interaction.placing: @@ -1550,14 +1542,9 @@ func _unhandled_key_input(event: InputEvent) -> void: interaction.tilt_selected(false, t_delta2) get_viewport().set_input_as_handled() elif not ctrl and not event.alt_pressed: - # R = Vierteldrehung 90° (Karte t_55257792): Bauen-Modus dreht - # Vorschau/Auswahl (wie bisher), Objekt-Auswahl UND Ghost drehen - # jetzt auch in 90°-Schritten (Umschalt+R = zurück). Feindrehen - # 15° bleibt am Mausrad. - if tool_mode == "bauen": - build_tool.call("rotate_input") - get_viewport().set_input_as_handled() - return + # R = Vierteldrehung 90° (Karte t_55257792): Objekt-Auswahl UND Ghost + # drehen in 90°-Schritten (Umschalt+R = zurück). Feindrehen 15° + # bleibt am Mausrad (Bauteile: 90° pro Rad-Tick). if interaction.placing: interaction.place_rot_deg = wrapf(interaction.place_rot_deg + r_delta, 0.0, 360.0) interaction._ghost_rotation_anwenden() @@ -1601,9 +1588,6 @@ func _unhandled_key_input(event: InputEvent) -> void: "exit_spawns": _set_tool_mode("objects") _set_status("Spawn-Werkzeug beendet — zurück zum Objekt-Werkzeug.") - "exit_bauen": - _set_tool_mode("objects") - _set_status("Bauen-Modus beendet — zurück zum Objekt-Werkzeug.") "exit_teststart": # Der gesetzte Test-Start bleibt bestehen (nur Werkzeug aus). _set_tool_mode("objects") @@ -1624,8 +1608,6 @@ func _unhandled_key_input(event: InputEvent) -> void: return if tool_mode == "spawns": spawn_tool.call("delete_selected") - elif tool_mode == "bauen": - build_tool.call("delete_selected") elif tool_mode == "teststart": # Karte t_96e3c4ee: Entf im Test-Start-Werkzeug = Punkt löschen. _test_start_delete() @@ -1640,26 +1622,17 @@ func _unhandled_key_input(event: InputEvent) -> void: if interaction.placing: return if ctrl: - # Strg+Bild↑ (Karte t_55257792): Auswahl/Bauteil auf den Boden. - if tool_mode == "bauen": - build_tool.call("snap_selected_to_floor") - get_viewport().set_input_as_handled() - elif tool_mode == "objects" and interaction.selected_index >= 0: + # Strg+Bild↑ (Karte t_55257792): Auswahl auf den Boden. + if tool_mode == "objects" and interaction.selected_index >= 0: interaction.snap_selection_to_floor() get_viewport().set_input_as_handled() elif not ctrl: - if tool_mode == "bauen": - build_tool.call("lift_selected", ObjectOps.LIFT_STEP_M) - get_viewport().set_input_as_handled() - elif tool_mode == "objects" and interaction.selected_index >= 0: + if tool_mode == "objects" and interaction.selected_index >= 0: interaction.lift_selected(ObjectOps.LIFT_STEP_M) get_viewport().set_input_as_handled() KEY_PAGEDOWN: if not ctrl and not interaction.placing: - if tool_mode == "bauen": - build_tool.call("lift_selected", -ObjectOps.LIFT_STEP_M) - get_viewport().set_input_as_handled() - elif tool_mode == "objects" and interaction.selected_index >= 0: + if tool_mode == "objects" and interaction.selected_index >= 0: interaction.lift_selected(-ObjectOps.LIFT_STEP_M) get_viewport().set_input_as_handled() KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT: @@ -1681,10 +1654,7 @@ func _unhandled_key_input(event: InputEvent) -> void: side = -1 var delta := MoveMath.nudge_delta( float(camera.call("view_yaw_deg")), fwd, side) - if tool_mode == "bauen": - build_tool.call("nudge_selected", delta) - get_viewport().set_input_as_handled() - elif tool_mode == "objects" and interaction.selected_index >= 0: + if tool_mode == "objects" and interaction.selected_index >= 0: interaction.nudge_selection(delta) get_viewport().set_input_as_handled() KEY_BRACKETLEFT, KEY_BRACKETRIGHT: @@ -1727,6 +1697,48 @@ func _on_library_collapse_all() -> void: ## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf. func _highlight_library(oid: String) -> void: assets_mod.highlight_library(oid) + if bauen_mod != null: + bauen_mod.sync_platzierung() + + +## Bauen-Tab: Gebäude-Info nachziehen (nach Setzen/Löschen/Undo). +func _refresh_bau_info() -> void: + if bauen_mod != null: + bauen_mod.refresh_info() + + +## „Neues Gebäude beginnen“: nächstes Teil startet eine neue Gruppe. +func _bau_neues_gebaeude() -> void: + interaction.call("neues_gebaeude") + _refresh_bau_info() + _set_status("Neues Gebäude: das nächste Bauteil beginnt eine neue Gruppe.") + + +## Alle Teile des Gebäudes wählen, zu dem die Auswahl gehört. +func _bau_gebaeude_waehlen() -> bool: + var sel: Array = interaction.selected_indices + if sel.is_empty(): + _set_status("Nichts markiert — klicke zuerst ein Bauteil des Gebäudes an.", true) + return false + var alle := {} + for i in sel: + for m in BauOps.gebaeude_von(doc, int(i)): + alle[int(m)] = true + var liste: Array = alle.keys() + liste.sort() + interaction.call("select_many", liste, true) + _set_status("Ganzes Gebäude gewählt (%d Teile) — Ziehen verschiebt, R dreht, Strg+D kopiert, Entf löscht." % liste.size()) + return true + + +func _bau_gebaeude_kopieren() -> void: + if _bau_gebaeude_waehlen(): + interaction.call("duplicate_selected") + + +func _bau_gebaeude_loeschen() -> void: + if _bau_gebaeude_waehlen(): + interaction.call("delete_selected") func object_label(oid: String) -> String: @@ -1793,8 +1805,11 @@ func _on_right_tab_changed(idx: int) -> void: terrain_mod.terrain_mode_paint.button_pressed = true _set_tool_mode("terrain") elif tab == bauen_mod: - if tool_mode != "bauen": - _set_tool_mode("bauen") + if tool_mode != "objects": + _set_tool_mode("objects") + # Das Verlassen des Terrain-Werkzeugs schaltet den Tab auf Malen/Terrain + # zurück — der Nutzer wollte aber Bauen. + (ui_right_panel as TabContainer).current_tab = bauen_mod.get_index() func _on_terrain_btn_pressed() -> void: @@ -1812,34 +1827,24 @@ func _set_tool_mode(mode: String) -> void: if mode == tool_mode: _sync_terrain_button() _sync_spawn_button() - _sync_build_button() _sync_test_start_button() _update_tool_label() return if mode == "terrain": _leave_spawn_mode() - _leave_build_mode() _leave_test_start_mode() _enter_terrain_mode() elif mode == "spawns": _leave_terrain_mode() - _leave_build_mode() _leave_test_start_mode() _enter_spawn_mode() - elif mode == "bauen": - _leave_terrain_mode() - _leave_spawn_mode() - _leave_test_start_mode() - _enter_build_mode() elif mode == "teststart": _leave_terrain_mode() _leave_spawn_mode() - _leave_build_mode() _enter_test_start_mode() else: _leave_terrain_mode() _leave_spawn_mode() - _leave_build_mode() _leave_test_start_mode() _enter_objects_mode() _update_tool_label() @@ -1898,50 +1903,27 @@ func _enter_objects_mode() -> void: _base_tool_status() -# ------------------------------------------------------ Bauen-Werkzeug ------ -# (Karte t_97ea59c5 — Modular-Builder-Pieces direkt im App-Skelett; die -# Godot-Editor-UI des Plugins bleibt unangetastet im Addon aktivierbar.) -# U1: die Bauteil-Widgets wohnen im Terrain-Tab (terrain_panel.gd, Werkzeug- -# Zusammenhang); die Werkzeug-Logik bleibt hier im Koordinator. Tests/Tools -# erreichen sie über main.build_edit_btn/main.build_kind_buttons wie vorher. +# ------------------------------------------------------------- Bauen ------ +# Bauteile sind normale Bibliotheks-Objekte (bau_*): das Bauen-Panel startet die +# Platzierung in interaction.gd (Ghost rastet über BuildSnap). Kein eigener +# Werkzeug-Modus mehr — Auswahl, Drehen, Kopieren, Löschen, Undo laufen über die +# Objekt-Werkzeuge. -func _on_build_btn_pressed() -> void: - if build_edit_btn.button_pressed: - _set_tool_mode("bauen") - else: +## F3 / Menü: Bauen-Tab zeigen (wechselt aus Terrain-/Spawn-Modus zurück zu Objekten). +func _open_bauen_tab() -> void: + if tool_mode != "objects": _set_tool_mode("objects") - - -func _on_build_kind_pressed(kind_id: String) -> void: - if tool_mode != "bauen": - _set_tool_mode("bauen") - for other in build_kind_buttons: - (build_kind_buttons[other] as Button).button_pressed = false - (build_kind_buttons[kind_id] as Button).button_pressed = true - build_tool.call("set_kind", kind_id) - - -func _enter_build_mode() -> void: - interaction.call("cancel_placement") - interaction.call("clear_selection", true) - tool_mode = "bauen" - help_label.text = "Linksklick: Bauteil setzen · R: drehen · Entf: löschen · H: Hilfe" - build_edit_btn.button_pressed = true if ui_right_panel is TabContainer and bauen_mod != null: (ui_right_panel as TabContainer).current_tab = bauen_mod.get_index() - build_tool.call("activate") - _set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.") + _set_status("Bauen: Bauteil rechts anklicken, dann in die Karte klicken — Wände rasten an Zellkanten, Böden in die Zellmitte, alles aufs 2-m-Raster.") -func _leave_build_mode() -> void: - if build_tool != null: - build_tool.call("deactivate") - _sync_build_button() - - -func _sync_build_button() -> void: - build_edit_btn.button_pressed = tool_mode == "bauen" +## Bauteil im Bauen-Tab angeklickt → Platzieren mit Snap starten. +func _on_bauteil_gewaehlt(oid: String) -> void: + if tool_mode != "objects": + _set_tool_mode("objects") + interaction.call("start_placement", oid) func _sync_terrain_button() -> void: @@ -2067,6 +2049,7 @@ func _registry_toolbar_button(id: String) -> Control: ## Nach undo/redo/platzieren/löschen: Struktur hat sich geändert → Ansicht neu. func _after_structure_change() -> void: dirty = true + _refresh_bau_info() _refresh_file_label() _refresh_stats() _refresh_history_buttons() @@ -2087,8 +2070,6 @@ func _after_structure_change() -> void: interaction.call("refresh_terrain_cache") # t_4092e3a0: Objekt-Cursor trifft das Gelände if tool_mode == "spawns" and spawn_tool != null: spawn_tool.call("refresh_from_doc") - if tool_mode == "bauen" and build_tool != null: - build_tool.call("refresh_from_doc") # --------------------------------------------------------------- Anzeige --- @@ -2205,7 +2186,7 @@ func _refresh_stats() -> void: var poly_pct := 100.0 * poly / budget stats_objects.text = "%d Objekte · Poly %.0f %%" % [count, poly_pct] stats_objects.tooltip_text = "Objekte: %d / 5000\nPoly-Budget: %s / %s (%.0f %%)\nBauteile: %d" % [ - count, _fmt_int(int(poly)), _fmt_int(budget), poly_pct, BuildOps.piece_count(doc)] + count, _fmt_int(int(poly)), _fmt_int(budget), poly_pct, BauOps.teile_zahl(doc)] # Nur bei knappem Budget auffällig färben (sonst ruhiger Nebentext). stats_objects.add_theme_color_override("font_color", get_theme_color( "warning" if poly_pct >= 90.0 or count >= 4500 else "field", "Editor")) @@ -2641,42 +2622,28 @@ func _start_groups_demo(args: PackedStringArray) -> void: get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind -## `godot ... -- --bauen-demo [ordner]` — Desktop-Smoke (Karte t_97ea59c5): -## baut programmatisch ein kleines Haus (Fundament+Wände+Giebel+Dach+Tür-Wand -## +Treppe), zeigt Auswahl + Ghost, speichert/lädt die Map zum Beweis. Die App -## bleibt offen (Sly sieht das Ergebnis live). +## `godot ... -- --bauen-demo [ordner]` — Desktop-Smoke: baut programmatisch ein +## kleines Haus mit denselben Funktionen wie der Klick (BauSnap + BauOps), zeigt +## Ghost, Gebäude-Auswahl und Weiterbauen nach Speichern/Laden. Die App bleibt +## offen (Sly sieht das Ergebnis live). func _start_bauen_demo(args: PackedStringArray) -> void: var out_dir := "/tmp/scopedvr_bauen" var idx := args.find("--bauen-demo") if idx >= 0 and idx + 1 < args.size() and not args[idx + 1].begins_with("--"): out_dir = args[idx + 1] DirAccess.make_dir_recursive_absolute(out_dir) - - _set_tool_mode("bauen") - var bt = build_tool # untypisiert (script-erweitert) — kein := möglich - # 1) Ghost-Vorschau am Cursor + erstes Fundament. - bt.call("demo_park_ghost", "wand", Vector3(2.0, 0.2, 2.0), 0.0) + _set_tool_mode("objects") + interaction.call("start_placement", "bau_wand") + interaction.call("demo_park_bau", Vector3(2.0, 0.0, 1.0)) _demo_camera(Vector3(3.0, 2.0, 8.0), 14.0, 32.0, 25.0) - _set_status("Demo 1/3: Bauen-Modus — Bauteil rechts anklicken, die gelbe Vorschau rastet aufs 2-m-Raster.") + _set_status("Demo 1/3: Bauen — die Vorschau rastet auf die Zellkante ein.") await _demo_shot(out_dir + "/bauen_ghost.png") - # 2) Kleines Haus komplett gesetzt (jedes Teil = EIN Undo-Schritt): - # Fundament, 3 Wände, Tür-Wand, 2 Giebelwände, Dach, Treppe. - bt.call("demo_place", "fundament", Vector3(3.0, 0.0, 3.0), 0.0) - bt.call("demo_place", "wand", Vector3(2.0, 0.2, 2.0), 0.0) - bt.call("demo_place", "wand", Vector3(2.0, 0.2, 4.0), 0.0) - bt.call("demo_place", "wand", Vector3(2.0, 0.2, 4.0), 90.0) - bt.call("demo_place", "tuer", Vector3(4.0, 0.2, 4.0), 90.0) - bt.call("demo_place", "giebelwand", Vector3(2.0, 2.2, 2.0), 0.0) - bt.call("demo_place", "giebelwand", Vector3(2.0, 2.2, 4.0), 0.0) - bt.call("demo_place", "dach_giebel", Vector3(3.0, 2.2, 3.0), 0.0) - bt.call("demo_place", "treppe", Vector3(8.0, 0.0, 7.0), 0.0) + interaction.call("cancel_placement") + var gid := BauOps.haus_demo(doc, history, Vector3(4.0, 0.0, 4.0)) _after_structure_change() - bt.call("select_index", 7) # Dach ausgewählt (orangener Rahmen) - _demo_camera(Vector3(9.0, 4.5, 10.0), 16.0, 35.0, 40.0) - _set_status("Demo 2/3: Kleines Haus gebaut — Fundament, Wände, Tür-Öffnung, Giebelwände, Dach, Treppe. Dach ist ausgewählt (orange).") + _demo_camera(Vector3(4.0, 1.5, 4.0), 13.0, 30.0, 35.0) + _set_status("Demo 2/3: Kleines Haus gebaut (Gruppe %d) — Fundament, Wände, Tür, Fenster, Giebel, Dach, Treppe." % (gid + 1)) await _demo_shot(out_dir + "/bauen_haus.png") - # 3) Speichern → Laden → weiterbauen (Abnahme-Flow): Map roundtrippen, - # dann eine zusätzliche Wand — sie muss nach dem Laden sauber passen. _save_to(out_dir + "/bauen_demo.svmap") var reload := SvmapIO.load_from_file(out_dir + "/bauen_demo.svmap") if bool(reload["ok"]): @@ -2684,16 +2651,10 @@ func _start_bauen_demo(args: PackedStringArray) -> void: history.begin(doc) _clear_edit_state() _refresh_all() - var before: Dictionary = history.snapshot(["gebaeude"]) - BuildOps.ensure_block(doc).append(BuildOps.make_piece("fenster", Vector3(2.0, 0.2, 4.0), 90.0)) - history.commit("Wand mit Fenster gesetzt", before, history.snapshot(["gebaeude"])) - _after_structure_change() - bt.call("demo_park_ghost", "fenster", Vector3(6.0, 0.2, 4.0), 90.0) - _demo_camera(Vector3(9.0, 4.5, 10.0), 16.0, 35.0, 40.0) - _set_status("Demo 3/3: Gespeichert, geladen und WEITERGEBAUT (Fenster-Wand dazwischen) — Strg+Z nimmt nur das neue Teil zurück.") + _demo_camera(Vector3(4.0, 1.5, 4.0), 13.0, 30.0, 35.0) + _set_status("Demo 3/3: Gespeichert und wieder geladen — das Haus ist als Gruppe erhalten.") await _demo_shot(out_dir + "/bauen_weiter.png") - print("BAUEN_DEMO_OK: 3 Screenshots in %s (%d Bauteile, Map OK)" % [ - out_dir, BuildOps.piece_count(doc)]) + print("BAUEN_DEMO_OK: 3 Screenshots in %s (%d Bauteile, Map OK)" % [out_dir, BauOps.teile_zahl(doc)]) if args.has("--bauen-demo-quit"): get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind @@ -2791,6 +2752,7 @@ func _on_resign_confirmed() -> void: return doc = result["data"] SvmapIO.upgrade_material_layer(doc.get("terrain", {})) # feines Material-Raster + BauOps.migriere_gebaeude(doc) # alte Bau-Sektion → Bauteil-Objekte current_path = _resign_pending_path dirty = true history.begin(doc) diff --git a/src/ui/panels/bauen_panel.gd b/src/ui/panels/bauen_panel.gd index cf92be1..0fb415a 100644 --- a/src/ui/panels/bauen_panel.gd +++ b/src/ui/panels/bauen_panel.gd @@ -1,12 +1,21 @@ extends "res://src/ui/panels/editor_panel.gd" -## Bauen-Tab (25.10., UI-Aufräumen): Bauteil-Auswahl für das Bauen-Werkzeug -## (Fundamente, Wände, Dächer …). Vorher im Terrain-Tab. Die Werkzeug-Logik -## bleibt in main.gd/build_tool.gd; main reicht die Felder unter den alten -## Namen (build_edit_btn, build_panel, build_kind_buttons) weiter. +## 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 +## 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. -var build_edit_btn: Button -var build_panel: VBoxContainer -var build_kind_buttons: Dictionary = {} # kind_id → Button +## 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 static func spec() -> Dictionary: @@ -18,45 +27,107 @@ func panel_title() -> String: func build() -> void: - build_edit_btn = Button.new() - build_edit_btn.text = "🏠 Bauen (Häuser)" - build_edit_btn.custom_minimum_size = UIScale.v2(0, 48) - build_edit_btn.toggle_mode = true - build_edit_btn.tooltip_text = "Fundamente, Wände, Dächer, Treppen und Türen im 2-Meter-Raster bauen (F3)" - build_edit_btn.pressed.connect(_on_build_btn_pressed) - add_child(build_edit_btn) - - build_panel = VBoxContainer.new() - build_panel.add_theme_constant_override("separation", UIScale.px(8)) - add_child(build_panel) - - build_panel.add_child(_field_label("Bauteil:")) - var build_grid := GridContainer.new() - build_grid.columns = 2 - build_grid.add_theme_constant_override("h_separation", UIScale.px(8)) - build_grid.add_theme_constant_override("v_separation", UIScale.px(8)) - build_panel.add_child(build_grid) - var build_group := ButtonGroup.new() - for kind_id in BuildOps.KINDS.keys(): - var kind_btn := Button.new() - kind_btn.text = str(BuildOps.KINDS[kind_id]["label"]) - kind_btn.toggle_mode = true - kind_btn.button_group = build_group - kind_btn.custom_minimum_size = UIScale.v2(148, 44) - kind_btn.clip_text = true - kind_btn.tooltip_text = "%s — %s" % [str(BuildOps.KINDS[kind_id]["label"]), str(BuildOps.KINDS[kind_id].get("info", ""))] - kind_btn.pressed.connect(_on_build_kind_pressed.bind(str(kind_id))) - build_kind_buttons[str(kind_id)] = kind_btn - build_grid.add_child(kind_btn) - (build_kind_buttons["fundament"] as Button).set_pressed_no_signal(true) + _group = ButtonGroup.new() + _group.allow_unpress = true + 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(_separator()) + add_child(_header_label("Gebäude")) + info_label = _muted_label("") + add_child(info_label) + add_child(_aktion("🏠 Neues Gebäude beginnen", "Das nächste Teil gehört zu einem neuen Gebäude (neue Gruppe).", + func() -> void: main.call("_bau_neues_gebaeude"))) + add_child(_aktion("🔲 Ganzes Gebäude auswählen", "Wählt alle Teile des Gebäudes, zu dem das markierte Teil gehört — zum Verschieben, Drehen, Kopieren.", + func() -> void: main.call("_bau_gebaeude_waehlen"))) + add_child(_aktion("📄 Gebäude kopieren", "Kopiert das ganze Gebäude als neues Gebäude (Strg+Z nimmt es zurück).", + func() -> void: main.call("_bau_gebaeude_kopieren"))) + 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() -func _on_build_btn_pressed() -> void: - if build_edit_btn.button_pressed: - main.call("_set_tool_mode", "bauen") - else: - main.call("_set_tool_mode", "objects") +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 _on_build_kind_pressed(kind_id: String) -> void: - main.call("_on_build_kind_pressed", kind_id) +func _knopf_raster(oids: Array, bau: bool) -> GridContainer: + var grid := GridContainer.new() + grid.columns = 2 + grid.add_theme_constant_override("h_separation", UIScale.px(8)) + grid.add_theme_constant_override("v_separation", UIScale.px(8)) + var lib: Dictionary = main.manifest.get("objects", {}) + for oid in oids: + var btn := Button.new() + btn.toggle_mode = true + btn.button_group = _group + btn.custom_minimum_size = UIScale.v2(148, 44) + btn.clip_text = true + var poly := int((lib.get(oid, {}) as Dictionary).get("poly", 0)) + if bau: + btn.text = BauKatalog.label(oid) + btn.tooltip_text = "%s — %s\n%d Dreiecke" % [BauKatalog.label(oid), + str((BauKatalog.KINDS[oid] as Dictionary).get("info", "")), poly] + else: + var name_de := str((lib.get(oid, {}) as Dictionary).get("name_de", oid)) + btn.text = name_de + btn.tooltip_text = "%s — %d Dreiecke" % [name_de, poly] + btn.pressed.connect(_on_teil_gedrueckt.bind(str(oid), btn)) + bau_buttons[str(oid)] = btn + grid.add_child(btn) + return grid + + +func _aktion(text: String, tip: String, cb: Callable) -> Button: + var b := Button.new() + b.text = text + b.tooltip_text = tip + b.custom_minimum_size = UIScale.v2(0, 44) + b.pressed.connect(cb) + return b + + +func _on_teil_gedrueckt(oid: String, btn: Button) -> void: + if not btn.button_pressed: + main.interaction.call("cancel_placement") + return + main.call("_on_bauteil_gewaehlt", oid) + + +## Teil-Markierung an den laufenden Platzier-Zustand koppeln (Esc/Abbruch löst sie). +func sync_platzierung() -> void: + var aktiv := str(main.interaction.get("place_oid")) if bool(main.interaction.get("placing")) else "" + for oid in bau_buttons: + (bau_buttons[oid] as Button).set_pressed_no_signal(oid == aktiv) + + +## Info-Zeile: aktuelles Gebäude (Name, Teile, Dreiecke) bzw. Gesamtzahl. +func refresh_info() -> void: + if info_label == null or main == null or main.doc.is_empty(): + return + var gebaeude := BauOps.gebaeude_gruppen(main.doc) + var text := "%d Gebäude, %d Bauteile in der Map." % [gebaeude.size(), BauOps.teile_zahl(main.doc)] + var gid := int(main.interaction.get("build_group_id")) + for g in gebaeude: + if int(g["gid"]) == gid: + text = "Aktuell: %s — %d Teile, %d Dreiecke.\n%s" % [g["name"], (g["indizes"] as Array).size(), int(g["dreiecke"]), text] + break + info_label.text = text diff --git a/src/ui/panels/scene_tree_panel.gd b/src/ui/panels/scene_tree_panel.gd index a8b778b..80d5400 100644 --- a/src/ui/panels/scene_tree_panel.gd +++ b/src/ui/panels/scene_tree_panel.gd @@ -71,9 +71,6 @@ func _on_tree_selected() -> void: var item := content_tree.get_selected() if item == null: return - if item.has_meta("gebaeude_index"): - _on_tree_selected_gebaeude(int(item.get_meta("gebaeude_index"))) - return # Gruppe geklickt (Karte t_e3c0310a U2): alle Mitglieder auswählen. if item.has_meta("group_id"): var g_indices: Array = [] @@ -111,13 +108,6 @@ func _on_tree_selected() -> void: main.call("_set_status", "%d Objekte im Baum markiert — Entf löscht ALLE, Strg+D dupliziert ALLE (jeweils EIN Undo-Schritt)." % valid.size()) -## Baum: Bauteil-Klick → Bauen-Modus + Teil anwählen (Karte t_97ea59c5). -func _on_tree_selected_gebaeude(gi: int) -> void: - if str(main.get("tool_mode")) != "bauen": - main.call("_set_tool_mode", "bauen") - main.build_tool.call("select_index", gi) - - func mark_tree_selection(idx: int) -> void: mark_tree_selection_multi([idx]) @@ -165,11 +155,10 @@ func refresh_tree() -> void: var root := content_tree.create_item() var objects: Array = main.doc.get("objects", []) var zones: Dictionary = main.doc.get("spawn_zones", {}) - var geb_list: Array = BuildOps.pieces(main.doc) var group_ids: Array = GroupOps.active_group_ids(main.doc) # Leerer Zustand (Karte t_e3c0310a U2): Klartext-Hinweis statt leere Gruppen. - if objects.is_empty() and zones.is_empty() and geb_list.is_empty() and group_ids.is_empty(): + if objects.is_empty() and zones.is_empty() and group_ids.is_empty(): var empty_item := content_tree.create_item(root) empty_item.set_text(0, "Noch keine Objekte — links in der Objekt-Bibliothek wählen") empty_item.set_selectable(0, false) @@ -218,16 +207,6 @@ func refresh_tree() -> void: point_item.set_text(0, "%s (%.0f, %.0f)" % [ str(p.get("id", "?")), float(p.get("pos", [0, 0, 0])[0]), float(p.get("pos", [0, 0, 0])[2])]) - # Gebäude (Karte t_97ea59c5): Bauteile im Baum — Klick wählt das Teil an. - if not geb_list.is_empty(): - var geb_item := content_tree.create_item(root) - geb_item.set_text(0, "Gebäude (%d Bauteile)" % geb_list.size()) - for i in geb_list.size(): - var teil: Dictionary = geb_list[i] - var g_child := content_tree.create_item(geb_item) - g_child.set_text(0, "%s #%d" % [BuildOps.kind_label(str(teil.get("id", "?"))), i + 1]) - g_child.set_meta("gebaeude_index", i) - # Gruppen (Karte t_e3c0310a U2): aktive Gruppen als eigene Ebene im Baum. # Klick auf eine Gruppe wählt ALLE Mitglieder in der 3D-Szene (Zwei-Wege). if not group_ids.is_empty(): diff --git a/src/ui/panels/symmetry_panel.gd b/src/ui/panels/symmetry_panel.gd index 018f034..773fd7b 100644 --- a/src/ui/panels/symmetry_panel.gd +++ b/src/ui/panels/symmetry_panel.gd @@ -146,13 +146,10 @@ func _on_apply_pressed() -> void: func _conflict_text(an: Dictionary) -> String: var parts: Array = [] var obj_n := int(an["objects"]["diff"]) - var teil_n := int(an["gebaeude"]["diff"]) var spawn_n := int(an["spawns"]["diff"]) var boden_n := (an["terrain_cells"] as Array).size() + (an.get("material_cells", []) as Array).size() if obj_n > 0: parts.append("%d Objekt-Paare unterscheiden sich" % obj_n) - if teil_n > 0: - parts.append("%d Bau-Teil-Paare unterscheiden sich" % teil_n) if spawn_n > 0: parts.append("%d Spawn-Punkt-Paare unterscheiden sich" % spawn_n) if boden_n > 0: diff --git a/src/ui/viewport_toolbar.gd b/src/ui/viewport_toolbar.gd index 54d4ac4..1f85a2f 100644 --- a/src/ui/viewport_toolbar.gd +++ b/src/ui/viewport_toolbar.gd @@ -65,7 +65,7 @@ func _process(_delta: float) -> void: return var inter = main.interaction var mode: String = str(main.get("tool_mode")) - var want: bool = (mode == "objects" or mode == "bauen") \ + var want: bool = mode == "objects" \ and not inter.selected_indices.is_empty() \ and not bool(inter.get("placing")) \ and main.get("playtest") == null diff --git a/src/viewport/glb_mesh_store.gd b/src/viewport/glb_mesh_store.gd index 5bf6145..e4f3eda 100644 --- a/src/viewport/glb_mesh_store.gd +++ b/src/viewport/glb_mesh_store.gd @@ -178,6 +178,10 @@ static func _eintrag(oid: String) -> Dictionary: static func mesh_fuer(oid: String) -> Mesh: if oid.is_empty(): return null + # Bauteile (bau_*) entstehen aus dem Katalog — identisch zu den GLBs, die + # der Client lädt (tools/gen_bauteile.gd), aber ohne Modell-Ordner nötig. + if BauKatalog.ist_bau(oid): + return BauKatalog.mesh(oid) if _cache.has(oid): return _cache[oid] if _cache[oid] is Mesh else null for pfad in _kandidaten(oid): @@ -199,6 +203,8 @@ static func mesh_fuer(oid: String) -> Mesh: static func fehlt(oid: String) -> bool: if oid.is_empty(): return true + if BauKatalog.ist_bau(oid): + return false if _cache.has(oid): return not (_cache[oid] is Mesh) for pfad in _kandidaten(oid): diff --git a/src/viewport/map_view_builder.gd b/src/viewport/map_view_builder.gd index d8d6835..cee8f99 100644 --- a/src/viewport/map_view_builder.gd +++ b/src/viewport/map_view_builder.gd @@ -92,11 +92,8 @@ var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis) var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius) var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant) -var gebaeude_markers: Array = [] # MeshInstance3D je Bauteil (Karte t_97ea59c5) var grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014) -var _gebaeude_baked: Dictionary = {} # faces/offsets für Ray-Pick (Bauen-Werkzeug) -var _gebaeude_sel: MeshInstance3D # Kontur-Mesh um das gewählte Bauteil var _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung) var _base_mat: StandardMaterial3D @@ -118,10 +115,7 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void: child.queue_free() object_markers = [] spawn_markers = [] - gebaeude_markers = [] grid_node = null - _gebaeude_baked = {} - _gebaeude_sel = null _doc_ref = data _base_mat = StandardMaterial3D.new() _base_mat.albedo_color = OBJECT_COLOR @@ -156,7 +150,6 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void: grid_node.visible = terrain.is_empty() _build_objects(data.get("objects", []), manifest) _rebuild_spawn_markers(data) - _build_gebaeude(data) ## Terrain als echtes Höhenfeld-Mesh statt flacher Ebene (Karte t_9aba04f1): @@ -628,70 +621,6 @@ func set_spawn_selected(team: int, pid: String) -> void: label.modulate = _markierung_farbe("selection_outline") if is_sel else col -# ------------------------------------------------------------ Bauteile ------ -# (Karte t_97ea59c5 — Modular-Builder im App-Skelett; Meshes kommen aus der -# CC0-Plugin-Runtime-Geometrie, Materialien = eigene flache Fallback-Farben.) - -## Bauteile aus dem doc backen und als MeshInstances aufbauen — plus die -## Welt-Flächen cachen, damit das Bauen-Werkzeug präzise auf Wände/Dächer -## klicken (stapeln) und Teile auswählen kann. -func _build_gebaeude(data: Dictionary) -> void: - _gebaeude_baked = BuildOps.bake_faces(data) - var mats := {} - var list: Array = BuildOps.pieces(data) - for i in list.size(): - var teil: Dictionary = list[i] - var mesh := BuildOps.piece_mesh(teil) - if mesh == null: - continue - var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) - var mi := MeshInstance3D.new() - mi.mesh = mesh - mi.material_override = BuildOps.piece_material(str(teil.get("id", "")), mats) - mi.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2])) - mi.rotation.y = deg_to_rad(float(teil.get("rot", 0.0))) - add_child(mi) - gebaeude_markers.append(mi) - - -## Gebackene Welt-Flächen aller Bauteile (für das Bauen-Werkzeug: Klicken/ -## Stapeln/Auswählen). Leeres faces = noch nichts gebaut. -func gebaeude_baked() -> Dictionary: - if _gebaeude_baked.is_empty(): - return {"faces": PackedVector3Array(), "offsets": []} - return _gebaeude_baked - - -## Auswahl-Markierung (U6, t_639135dd): dezente Außenhüllen-Kontur um das -## Bauteil-Mesh — gleiche Technik wie bei den Objekten (selection_outline- -## Shader, Theme-Farbe). Keine halbtransparente Vollbox mehr, die das Teil -## verdeckt. -func set_gebaeude_selected(idx: int) -> void: - if _gebaeude_sel != null and is_instance_valid(_gebaeude_sel): - _gebaeude_sel.queue_free() - _gebaeude_sel = null - var list: Array = BuildOps.pieces(_doc_ref) - if idx < 0 or idx >= list.size(): - return - var aabb := BuildOps.piece_aabb(list[idx]) - if aabb.size.length() < 0.001: - return - var mesh := BuildOps.piece_mesh(list[idx]) - if mesh == null: - return - # Kontur in GLEICHER Transform wie das echte Bauteil-Mesh (aabb ist - # welt-transformiert — nur als Größen-Guard; das Mesh selbst sitzt am - # Teil-Anker mit rot). - var kontur := MeshInstance3D.new() - kontur.mesh = mesh - var pos: Array = list[idx].get("pos", [0.0, 0.0, 0.0]) - kontur.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2])) - kontur.rotation.y = deg_to_rad(float(list[idx].get("rot", 0.0))) - kontur.material_override = _outline_mat_sel - add_child(kontur) - _gebaeude_sel = kontur - - # ------------------------------------------------------ U6-Markierung ------- # (Karte t_639135dd — dezente Auswahl: Kontur statt Vollorange. Eine Quelle # für Farben/Breiten ist das Theme; der Shader ist die Außenhüllen-Technik.) diff --git a/src/viewport/orbit_camera.gd b/src/viewport/orbit_camera.gd index b412390..ba92c81 100644 --- a/src/viewport/orbit_camera.gd +++ b/src/viewport/orbit_camera.gd @@ -377,19 +377,17 @@ func fit_to_document(doc: Dictionary, map_mn: Vector3, map_mx: Vector3) -> void: int(START_DISTANCE_M)) -## Bounding-Box des Dokument-INHALTS: Objekte, Bauteile („gebaeude") und +## Bounding-Box des Dokument-INHALTS: Objekte (inkl. Bauteile) und ## Spawnpunkte — Terrain und Map-Bounds zählen NICHT (Karte K1b). ## Rückgabe: {mn: Vector3, mx: Vector3, count: int}; count 0 = leeres Dokument. static func content_bounds(doc: Dictionary) -> Dictionary: var mn := Vector3(INF, INF, INF) var mx := Vector3(-INF, -INF, -INF) var count := 0 - for pts in [_content_points(doc.get("objects", []) as Array), - _content_points(doc.get("gebaeude", []) as Array)]: - for p: Vector3 in pts as Array: - mn = mn.min(p) - mx = mx.max(p) - count += 1 + for p: Vector3 in _content_points(doc.get("objects", []) as Array): + mn = mn.min(p) + mx = mx.max(p) + count += 1 var zones: Dictionary = doc.get("spawn_zones", {}) as Dictionary for team_key in zones: var zone: Dictionary = zones[team_key] if zones[team_key] is Dictionary else {} diff --git a/tests/test_bauen.gd b/tests/test_bauen.gd index a0eca56..ee95e69 100644 --- a/tests/test_bauen.gd +++ b/tests/test_bauen.gd @@ -1,8 +1,9 @@ extends SceneTree -## Editor-Tests (headless): Karte t_97ea59c5 — Bauen-Modus (Modular-Builder). +## Editor-Tests (headless): Bauen-Modus (Umbau 25.10. — Bauteile sind normale +## Bibliotheks-Objekte `bau_*`, Gebäude = Gruppe). ## Ausführen: godot --headless --path . --script res://tests/test_bauen.gd -## Exit 0 = alles grün. Läuft OHNE UI (BuildOps/SvmapIO/ActionHistory) — -## GUI-Feinheiten (Ghost, Panel) verifiziert der Desktop-Smoke (--bauen-demo). +## Exit 0 = alles grün. Läuft OHNE UI (BauKatalog/BuildSnap/BauOps/SvmapIO/ +## ActionHistory) — Ghost/Panel prüft der Desktop-Smoke (--bauen-demo). var failures := 0 var checks := 0 @@ -21,7 +22,7 @@ func _init() -> void: func _cleanup() -> void: - for path in ["user://test_bauen.svmap", "user://test_bauen_legacy.svmap"]: + for path in ["user://test_bauen.svmap"]: if FileAccess.file_exists(path): DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) @@ -37,189 +38,371 @@ func _check(cond: bool, label: String) -> void: func _run_tests() -> void: _test_catalog() - _test_make_piece_isolation() - _test_piece_meshes() - _test_snap() - _test_bake_and_ray_pick() - _test_undo_redo() - _test_save_load_roundtrip() - _test_legacy_map_without_gebaeude() + _test_registry() + _test_meshes() + _test_openings() + _test_stairs() + _test_snap_cells_and_edges() + _test_snap_levels() + _test_replace_and_stack() + _test_ops_group_undo() + _test_duplicate_building() + _test_save_load() + _test_migration() # ------------------------------------------------------------------ Tests --- func _test_catalog() -> void: print("Katalog") - _check(BuildOps.KINDS.size() == 10, "10 Bauteil-Typen im Katalog") - for kind_id in BuildOps.KINDS.keys(): - var kind: Dictionary = BuildOps.KINDS[kind_id] - _check(str(kind.get("label", "")).length() > 3, "Label für %s" % str(kind_id)) - _check((kind.get("punkte", []) as Array).size() >= 2, "Punkte für %s" % str(kind_id)) - _check((kind.get("raster", "") == "ecke") or (kind.get("raster", "") == "mitte"), - "Raster-Typ für %s" % str(kind_id)) - # Abnahme-Typen vorhanden: Fundament/Wand/Dach/Treppe/Tür - for need in ["fundament", "wand", "dach_giebel", "treppe", "tuer"]: - _check(BuildOps.KINDS.has(need), "Katalog enthält %s" % need) + _check(BauKatalog.KINDS.size() == 12, "12 Bauteile im Katalog") + var in_gruppen := {} + for g in BauKatalog.GRUPPEN: + for oid in (g["oids"] as Array): + in_gruppen[oid] = true + _check(BauKatalog.KINDS.has(oid), "Gruppe verweist auf bekanntes Teil %s" % oid) + _check(in_gruppen.size() == BauKatalog.KINDS.size(), "jedes Teil steht in genau einer Gruppe (%d)" % in_gruppen.size()) + for oid in BauKatalog.oids(): + _check(str(oid).begins_with("bau_"), "%s: ID mit bau_-Präfix" % oid) + _check(BauKatalog.label(oid).length() > 3, "%s: deutscher Name" % oid) + _check(["zelle", "kante", "ecke", "treppe"].has(BauKatalog.snap_art(oid)), "%s: Snap-Art" % oid) -func _test_make_piece_isolation() -> void: - print("make_piece") - var teil := BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 90.0) - _check(not teil.is_empty(), "Bauteil erzeugt") - _check(str(teil["id"]) == "wand", "id übernommen") - # pos kommt aus einem Vector3 (float32!) — Vergleiche mit Toleranz. - _check(absf(float(teil["pos"][0]) - 2.0) < 0.001 and absf(float(teil["pos"][1]) - 0.2) < 0.001 \ - and absf(float(teil["pos"][2]) - 4.0) < 0.001, "Position übernommen") - _check(float(teil["rot"]) == 90.0, "Drehung übernommen") - # Punkte sind KOPIEN (kein Aliasing mit dem Katalog) - var before_x: float = (BuildOps.KINDS["wand"]["punkte"] as Array)[1][0] - (teil["punkte"] as Array)[1][0] = 99.0 - _check(float((BuildOps.KINDS["wand"]["punkte"] as Array)[1][0]) == before_x, - "Katalog-Punkte bleiben unangetastet (keine Alias-Falle)") - var tuer := BuildOps.make_piece("tuer", Vector3.ZERO, 0.0) - _check((tuer["details"] as Array).size() == 1, "Tür-Wand hat 1 Öffnung") - _check(int(((tuer["details"] as Array)[0] as Dictionary)["typ"]) == BuildOps.DETAIL_DOOR, - "Öffnung ist eine Tür") - _check(BuildOps.make_piece("gibts_nicht", Vector3.ZERO, 0.0).is_empty(), - "Unbekannte ID → leeres Teil (kein Crash)") +func _test_registry() -> void: + print("Registry / Dateien") + var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string("res://library/objects.json")) + var lib: Dictionary = (parsed as Dictionary).get("objects", {}) + for oid in BauKatalog.oids(): + _check(lib.has(oid), "%s steht in objects.json" % oid) + var e: Dictionary = lib.get(oid, {}) + var soll := BauKatalog.registry_eintrag(oid) + var gleich := int(e.get("poly", -1)) == int(soll["poly"]) and str(e.get("glb", "")) == str(soll["glb"]) \ + and str(e.get("name_de", "")) == str(soll["name_de"]) \ + and str((e.get("collision", {}) as Dictionary).get("quelle", "")) == str((soll["collision"] as Dictionary)["quelle"]) + var sz: Array = (e.get("collision", {}) as Dictionary).get("size", []) + var sz_soll: Array = (soll["collision"] as Dictionary)["size"] + for k in 3: + gleich = gleich and sz.size() == 3 and absf(float(sz[k]) - float(sz_soll[k])) < 0.002 + _check(gleich, "%s: Registry = Katalog (sonst tools/gen_bauteile.gd neu laufen lassen)" % oid) + _check(str(e.get("kategorie", "")) == "bauteile", "%s: Kategorie bauteile" % oid) + var col: Dictionary = e.get("collision", {}) + _check(str(col.get("shape", "")) == "trimesh", "%s: Trimesh-Kollision" % oid) + var path := "res://library/%s" % str(col.get("quelle", "")) + _check(FileAccess.file_exists(path), "%s: Kollisionsdatei vorhanden" % oid) + var kj: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) + var verts: Array = (kj as Dictionary).get("vertices", []) if kj is Dictionary else [] + _check(verts.size() == int(e.get("poly", -1)) * 3, "%s: Kollisions-Dreiecke = poly (%d)" % [oid, int(e.get("poly", -1))]) + _check(int(e.get("poly", 0)) <= 260, "%s: Quest-Budget (≤ 260 Dreiecke)" % oid) -func _test_piece_meshes() -> void: - print("Meshes") - for kind_id in BuildOps.KINDS.keys(): - var mesh := BuildOps.piece_mesh(BuildOps.make_piece(str(kind_id), Vector3.ZERO, 0.0)) - _check(mesh != null and mesh.get_surface_count() > 0, "Mesh für %s" % str(kind_id)) - var wand := BuildOps.piece_mesh(BuildOps.make_piece("wand", Vector3.ZERO, 0.0)) - var tuer := BuildOps.piece_mesh(BuildOps.make_piece("tuer", Vector3.ZERO, 0.0)) - var wand_tris := int(wand.get_faces().size() / 3) - var tuer_tris := int(tuer.get_faces().size() / 3) - _check(wand_tris > 0 and tuer_tris > 0, "Wand/Tür-Meshes haben Dreiecke") - _check(wand_tris != tuer_tris, "Tür-Öffnung verändert das Wand-Mesh (%d vs %d Tris)" % [wand_tris, tuer_tris]) - # Fundament: 2×2×0,2 m — AABB prüfen (lokale Geometrie, Ursprung Fußpunkt) - var f := BuildOps.piece_mesh(BuildOps.make_piece("fundament", Vector3.ZERO, 0.0)) - var size := f.get_aabb().size - _check(absf(size.x - 2.0) < 0.01 and absf(size.z - 2.0) < 0.01 and absf(size.y - 0.2) < 0.01, - "Fundament-AABB 2×0,2×2 m (ist %.2f×%.2f×%.2f)" % [size.x, size.y, size.z]) +func _test_meshes() -> void: + print("Meshes / Fußpunkt-Normalisierung") + for oid in BauKatalog.oids(): + var m := BauKatalog.mesh(oid) + _check(m != null and m.get_faces().size() >= 36, "%s: Mesh vorhanden" % oid) + var aabb := m.get_aabb() + _check(absf(aabb.position.y) < 0.001, "%s: Unterkante y = 0" % oid) + _check(absf(aabb.position.x + aabb.size.x * 0.5) < 0.001 and absf(aabb.position.z + aabb.size.z * 0.5) < 0.001, + "%s: XZ zentriert" % oid) + var arr := m.surface_get_arrays(0) + var verts: PackedVector3Array = arr[Mesh.ARRAY_VERTEX] + var nrms: PackedVector3Array = arr[Mesh.ARRAY_NORMAL] + var falsch := 0 + for t in range(0, verts.size() - 2, 3): + if (verts[t + 2] - verts[t]).cross(verts[t + 1] - verts[t]).dot(nrms[t]) < 0.0: + falsch += 1 + _check(falsch == 0, "%s: Wicklung passt zur Normale (kein Culling-Fehler)" % oid) + var mat := m.surface_get_material(0) as StandardMaterial3D + _check(mat != null and mat.albedo_color.a > 0.99 and mat.albedo_color != Color.WHITE, "%s: Grundfarbe im Material" % oid) + var w := BauKatalog.groesse("bau_wand") + _check(absf(w.x - 2.0) < 0.01 and absf(w.y - 3.0) < 0.01 and absf(w.z - 0.2) < 0.01, "Wand 2×3×0,2 m") + var f := BauKatalog.groesse("bau_fundament") + _check(absf(f.x - 2.0) < 0.01 and absf(f.y - 0.2) < 0.01 and absf(f.z - 2.0) < 0.01, "Fundament 2×0,2×2 m") + var b := BauKatalog.groesse("bau_bruestung") + _check(absf(b.y - 1.1) < 0.01, "Brüstung 1,1 m hoch") -func _test_snap() -> void: - print("Raster-Snap") - # "ecke" (Wand) → gerade Meter; "mitte" (Fundament) → ungerade Meter. - var w := BuildOps.snapped_pos("wand", Vector3(2.7, 0.11, -3.4)) - _check(w.x == 2.0 and w.z == -4.0, "Wand rastet auf Zellen-Ecken (2, −4)") - _check(absf(w.y - 0.10) < 0.001, "Y rastet auf 5 cm") - var f := BuildOps.snapped_pos("fundament", Vector3(2.7, 0.0, 3.2)) - _check(f.x == 3.0 and f.z == 3.0, "Fundament rastet auf Zellen-Mitte (3, 3)") - # Verbund: Fundament Mitte (3,3) hat Ecken bei 2 und 4 → Wand auf (2, y, 4) - # grenzt bündig. (Konvention, dokumentiert in build_ops.gd.) +## Strahl durch das Kollisions-Mesh: true = trifft (Wand), false = frei (Öffnung). +func _trifft(oid: String, origin: Vector3, dir: Vector3) -> bool: + var faces := BauKatalog.mesh(oid).get_faces() + for i in int(faces.size() / 3): + if SelectionMath.ray_tri(origin, dir, faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2]) >= 0.0: + return true + return false -func _test_bake_and_ray_pick() -> void: - print("Baken + Ray-Pick") - var doc := SvmapDocument.create_new("Bautest", 256.0, "mittag") - # Kleines Haus: Fundament → 3 Wände + Tür-Wand → 2 Giebelwände → Dach + Treppe - var pieces: Array = BuildOps.ensure_block(doc) - pieces.append(BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0)) - pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 2.0), 0.0)) # Süd - pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 0.0)) # Nord - pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 90.0)) # West - pieces.append(BuildOps.make_piece("tuer", Vector3(4.0, 0.2, 4.0), 90.0)) # Ost + Tür - pieces.append(BuildOps.make_piece("giebelwand", Vector3(2.0, 2.2, 2.0), 0.0)) - pieces.append(BuildOps.make_piece("giebelwand", Vector3(2.0, 2.2, 4.0), 0.0)) - pieces.append(BuildOps.make_piece("dach_giebel", Vector3(3.0, 2.2, 3.0), 0.0)) - pieces.append(BuildOps.make_piece("treppe", Vector3(10.0, 0.0, 9.0), 0.0)) - var baked := BuildOps.bake_faces(doc) - var tris := int((baked["faces"] as PackedVector3Array).size() / 3) - _check(tris > 100, "Haus gebacken: %d Dreiecke" % tris) - _check((baked["offsets"] as Array).size() == 9, "Offsets je Teil (9)") - _check(int(baked["offsets"][0]) == 0, "erstes Teil startet bei Dreieck 0") - _check(int(baked["offsets"][8]) < tris, "letztes Teil liegt im Bereich") - # Ray von oben → trifft Dach (Teil-Index 7) - var pick := BuildOps.ray_pick(baked, Vector3(3.0, 50.0, 3.0), Vector3(0.0, -1.0, 0.0)) - _check(not pick.is_empty(), "Ray von oben trifft das Haus") - _check(int(pick["piece"]) >= 0 and int(pick["piece"]) < 9, "Treffer gehört zu einem Teil (Index %d)" % int(pick["piece"])) - # Ray von der Seite auf Wandhöhe, daneben der Türöffnung → Teil 4 (Tür-Wand) - var side := BuildOps.ray_pick(baked, Vector3(50.0, 1.0, 3.6), Vector3(-1.0, 0.0, 0.0)) - _check(not side.is_empty() and int(side["piece"]) == 4, - "Seiten-Ray trifft die Tür-Wand (Index %s)" % str(side.get("piece", "?"))) - # … und DURCH die Türöffnung hindurch auf die West-Wand (Teil 3) - var door := BuildOps.ray_pick(baked, Vector3(50.0, 1.0, 3.0), Vector3(-1.0, 0.0, 0.0)) - _check(not door.is_empty() and int(door["piece"]) == 3, - "Ray durch die Türöffnung trifft die West-Wand (Index %s)" % str(door.get("piece", "?"))) - # Leer → kein Treffer - _check(BuildOps.ray_pick({"faces": PackedVector3Array(), "offsets": []}, - Vector3.ZERO, Vector3.DOWN).is_empty(), "Leeres Baken → kein Treffer") - # AABB eines Teils liegt an der richtigen Stelle (Dach über 2,2 m) - var aabb := BuildOps.piece_aabb(pieces[7]) - _check(aabb.position.y > 2.0, "Dach-AABB liegt über den Wänden (y=%.2f)" % aabb.position.y) +func _test_openings() -> void: + print("Türen / Fenster / Scharte sind echte Löcher") + var quer := Vector3(0.0, 0.0, 1.0) + # Tür: Mitte (x=0) frei von Boden bis 2,2 m, daneben (x=0.8) massiv, über der Tür (y=2.6) massiv. + _check(not _trifft("bau_wand_tuer", Vector3(0.0, 0.5, -1.0), quer), "Tür: Durchgang knapp über dem Boden frei") + _check(not _trifft("bau_wand_tuer", Vector3(0.0, 2.0, -1.0), quer), "Tür: Durchgang auf 2,0 m frei") + _check(_trifft("bau_wand_tuer", Vector3(0.0, 2.6, -1.0), quer), "Tür: über der Tür ist Wand") + _check(_trifft("bau_wand_tuer", Vector3(0.8, 1.0, -1.0), quer), "Tür: neben dem Rahmen ist Wand") + # Fenster: 1,1–2,3 m offen. + _check(not _trifft("bau_wand_fenster", Vector3(0.0, 1.7, -1.0), quer), "Fenster: Öffnung auf 1,7 m frei") + _check(_trifft("bau_wand_fenster", Vector3(0.0, 0.5, -1.0), quer), "Fenster: unter der Brüstung ist Wand") + _check(_trifft("bau_wand_fenster", Vector3(0.0, 2.6, -1.0), quer), "Fenster: darüber ist Wand") + # Schießscharte: Schlitz auf 1,3–1,7 m. + _check(not _trifft("bau_wand_scharte", Vector3(0.0, 1.5, -1.0), quer), "Scharte: Schlitz auf 1,5 m frei") + _check(_trifft("bau_wand_scharte", Vector3(0.0, 1.0, -1.0), quer), "Scharte: darunter Wand") + _check(_trifft("bau_wand", Vector3(0.0, 1.5, -1.0), quer), "Volle Wand ist dicht") -func _test_undo_redo() -> void: - print("Undo/Redo") - var doc := SvmapDocument.create_new("Undo", 256.0, "mittag") +func _test_stairs() -> void: + print("Treppe begehbar") + var faces := BauKatalog.mesh("bau_treppe").get_faces() + var g := BauKatalog.groesse("bau_treppe") + _check(absf(g.x - 4.0) < 0.01 and absf(g.y - 3.0) < 0.01 and absf(g.z - 2.0) < 0.01, "Treppe 4×3×2 m") + # Oberste Höhe unter x entlang der Mittellinie (z=0): steigt monoton, Stufen ≤ 22 cm. + var letzte := 0.0 + var max_sprung := 0.0 + var steigt := true + for k in range(1, 40): + var x := -2.0 + 4.0 * float(k) / 40.0 + var top := -1.0 + for i in int(faces.size() / 3): + var t := SelectionMath.ray_tri(Vector3(x, 10.0, 0.0), Vector3.DOWN, + faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2]) + if t >= 0.0: + top = maxf(top, 10.0 - t) + if top < letzte - 0.001: + steigt = false + max_sprung = maxf(max_sprung, top - letzte) + letzte = top + _check(steigt, "Lauffläche steigt monoton") + _check(max_sprung <= 0.22, "Stufenhöhe ≤ 22 cm (größter Sprung %.3f)" % max_sprung) + _check(letzte > 2.7, "Oben ≥ 2,7 m erreicht (%.2f)" % letzte) + + +func _flach(h: float) -> Callable: + return func(_x: float, _z: float) -> float: return h + + +func _test_snap_cells_and_edges() -> void: + print("Snap: Zelle / Kante / Ecke / Treppe") + var kein: Array = [] + var s := BuildSnap.berechne("bau_fundament", Vector3(0.3, 0.0, 0.4), 0.0, kein, _flach(0.0)) + _check(s["pos"] == Vector3(1.0, 0.0, 1.0), "Fundament: Zellmitte (1, 1)") + 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") + 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") + 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") + 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)) + _check(s["pos"] == Vector3(2.0, 0.0, 2.0), "Pfeiler: nächste Zellecke") + s = BuildSnap.berechne("bau_treppe", Vector3(3.3, 0.0, 1.2), 0.0, kein, _flach(0.0)) + _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) + 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)) + return + _check(true, "60 Zufallsklicks: Wandmitte liegt immer auf einer Kantenmitte") + # 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)") + + +func _teil(i: int, oid: String, pos: Vector3, rot: float) -> Dictionary: + return {"i": i, "oid": oid, "pos": pos, "rot": rot} + + +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), + ] + # 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") + # 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"])) + + +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), + ] + 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") + 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)) + _check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Wand von der anderen Seite (R) ersetzt = dreht um") + s = BuildSnap.berechne("bau_wand_tuer", Vector3(3.0, 0.0, 0.1), 0.0, teile, _flach(0.0)) + _check(bool(s["ok"]) and int(s["ersetze"]) == -1, "Nachbarkante bleibt frei (kein Ersetzen)") + + +func _neue_doc() -> Dictionary: + return SvmapDocument.create_new("Bautest", 256.0, "mittag") + + +func _test_ops_group_undo() -> void: + print("BauOps: Gruppe, Undo, Ersetzen") + var doc := _neue_doc() var hist := ActionHistory.new() hist.begin(doc) - # Teil 1: erster Snapshot MUSS den (leeren) Block enthalten, damit Strg+Z - # das erste Bauteil komplett entfernen kann. - var before: Dictionary = hist.snapshot(["gebaeude"]) - _check(before.has("gebaeude"), "Snapshot enthält den leeren gebaeude-Block") - BuildOps.ensure_block(doc).append(BuildOps.make_piece("fundament", Vector3.ZERO, 0.0)) - hist.commit("Fundament (2×2 m) gesetzt", before, hist.snapshot(["gebaeude"])) - # Teil 2 + 3 - for i in 2: - before = hist.snapshot(["gebaeude"]) - BuildOps.ensure_block(doc).append(BuildOps.make_piece("wand", Vector3(float(i) * 2.0, 0.2, 0.0), 0.0)) - hist.commit("Wand (2 m) gesetzt", before, hist.snapshot(["gebaeude"])) - _check(BuildOps.piece_count(doc) == 3, "3 Bauteile gesetzt") + var snap := BuildSnap.berechne("bau_fundament", Vector3(1, 0, 1), 0.0, [], _flach(0.0)) + var before := hist.snapshot(["objects", "group_names"]) + if not before.has("group_names"): + before["group_names"] = null + var gid := BauOps.gruppe_neu(doc) + _check(GroupState.group_name(doc, gid) == "Gebäude 1", "Gebäude-Gruppe heißt „Gebäude 1“") + var r := BauOps.setze_teil(doc, "bau_fundament", snap, gid, SymmetryCore.Mode.OFF) + hist.commit("Fundament bauen", before, hist.snapshot(["objects", "group_names"])) + var objs: Array = doc["objects"] + var n0 := objs.size() + _check(n0 >= 1 and int(r["index"]) == n0 - 1, "Teil als Objekt angehängt") + _check(GroupOps.group_id(objs[n0 - 1]) == gid, "Teil gehört zum Gebäude (Gruppe %d)" % gid) + _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 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)) + _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) + hist.commit("Tür einsetzen", before3, hist.snapshot(["objects", "group_names"])) + _check(bool(tr["ersetzt"]) and (doc["objects"] as Array).size() == n0 + 1, "Ersetzen fügt kein zusätzliches Objekt hinzu") + _check(str((doc["objects"] as Array)[widx]["id"]) == "bau_wand_tuer", "Objekt ist jetzt die Tür-Wand") + _check(GroupOps.group_id((doc["objects"] as Array)[widx]) == gid, "Gruppe bleibt beim Ersetzen") hist.undo() - _check(BuildOps.piece_count(doc) == 2, "Undo → 2 Bauteile") + _check(str((doc["objects"] as Array)[widx]["id"]) == "bau_wand", "Undo: wieder normale Wand") hist.undo() hist.undo() - _check(BuildOps.piece_count(doc) == 0, "3× Undo → alles weg (inkl. erstes Teil)") + _check((doc["objects"] as Array).is_empty(), "Undo ×3: alles weg") + _check(not doc.has("group_names") or (doc["group_names"] as Dictionary).is_empty(), "Undo entfernt auch den Gebäude-Namen") hist.redo() hist.redo() - _check(BuildOps.piece_count(doc) == 2, "2× Redo → 2 Bauteile wieder da") - # Löschen + Undo (wie build_tool.delete_selected) - before = hist.snapshot(["gebaeude"]) - (BuildOps.pieces(doc) as Array).remove_at(0) - hist.commit("Bauteil gelöscht", before, hist.snapshot(["gebaeude"])) - _check(BuildOps.piece_count(doc) == 1, "Löschen → 1 Teil") - hist.undo() - _check(BuildOps.piece_count(doc) == 2, "Undo holt gelöschtes Teil zurück") + hist.redo() + _check(BauOps.teile_zahl(doc) == 2, "Redo ×3 stellt Fundament + Tür-Wand wieder her") + # Gebäude-Info + var geb := BauOps.gebaeude_gruppen(doc) + _check(geb.size() == 1 and (geb[0]["indizes"] as Array).size() == 2, "gebaeude_gruppen: 1 Gebäude mit 2 Teilen") + _check(BauOps.gebaeude_von(doc, 0).size() == 2, "gebaeude_von liefert alle Mitglieder") + # 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.gebaeude_von(d2, 0).size() == BauOps.teile_zahl(d2), "alle Teile des Hauses in einer Gruppe (%d)" % gid2) + var doppelt := {} + var hat_doppelte := false + for t in BuildSnap.sammle_teile(d2): + var key := "%s|%s|%s" % [str((t["pos"] as Vector3).snapped(Vector3(0.01, 0.01, 0.01))), str(roundf(float(t["rot"]))), BauKatalog.snap_art(str(t["oid"]))] + if doppelt.has(key): + hat_doppelte = true + doppelt[key] = true + _check(not hat_doppelte, "Haus hat keine doppelt gesetzten Teile") -func _test_save_load_roundtrip() -> void: - print("Speichern/Laden") - var doc := SvmapDocument.create_new("Bauen-Save", 256.0, "mittag") - var pieces: Array = BuildOps.ensure_block(doc) - pieces.append(BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0)) - pieces.append(BuildOps.make_piece("tuer", Vector3(2.0, 0.2, 2.0), 90.0)) - pieces.append(BuildOps.make_piece("dach_pyramide", Vector3(3.0, 2.2, 3.0), 0.0)) - var save := SvmapIO.save_to_file("user://test_bauen.svmap", doc) - _check(bool(save["ok"]), "Map mit Bauteilen speichert (ohne Fehler)") - var load := SvmapIO.load_from_file("user://test_bauen.svmap") - _check(bool(load["ok"]), "Map mit Bauteilen lädt (V1–V15 + Prüfsumme grün)") - var loaded: Dictionary = load["data"] - _check(BuildOps.piece_count(loaded) == 3, "3 Bauteile überleben .svmap") - var src: Dictionary = pieces[1] - var dst: Dictionary = (BuildOps.pieces(loaded) as Array)[1] - _check(str(dst["id"]) == "tuer" and float(dst["rot"]) == 90.0, "Bauteil-Felder identisch") - _check((dst["punkte"] as Array) == (src["punkte"] as Array), "Punkte byte-gleich über den Roundtrip") - _check(((dst["details"] as Array)[0] as Dictionary)["w"] == 1.0, "Tür-Öffnung überlebt den Roundtrip") - # Gebackenes Mesh aus GELADENEN Teilen = wie vor dem Speichern (deterministisch) - var before_faces: PackedVector3Array = BuildOps.bake_faces(doc)["faces"] - var after_faces: PackedVector3Array = BuildOps.bake_faces(loaded)["faces"] - _check(before_faces == after_faces, "Baken ist deterministisch (vor/nach Speichern identisch)") +func _test_duplicate_building() -> void: + print("Gebäude kopieren / löschen") + var doc := _neue_doc() + var gid := BauOps.haus_demo(doc, null, Vector3(4.0, 0.0, 4.0)) + var anzahl := (doc["objects"] as Array).size() + var alle := BauOps.gebaeude_von(doc, 0) + var neue := ObjectOps.duplicate_objects(doc, alle, false, 8.0) + _check(neue.size() == anzahl, "alle %d Teile kopiert" % anzahl) + var gid_kopie := GroupOps.group_id((doc["objects"] as Array)[int(neue[0])]) + _check(gid_kopie != gid and gid_kopie != GroupOps.UNGROUPED, "Kopie bekommt eine EIGENE Gruppe (%d ≠ %d)" % [gid_kopie, gid]) + _check(GroupOps.members_of(doc, gid).size() == anzahl and GroupOps.members_of(doc, gid_kopie).size() == anzahl, + "Original und Kopie haben je %d Teile" % anzahl) + _check(GroupState.display_name(doc, gid_kopie).contains("Kopie"), "Kopie heißt „… (Kopie)“") + _check(BauOps.gebaeude_gruppen(doc).size() == 2, "zwei Gebäude in der Map") + var o0: Dictionary = (doc["objects"] as Array)[0] + var k0: Dictionary = (doc["objects"] as Array)[int(neue[0])] + _check(absf(float(k0["pos"][0]) - float(o0["pos"][0]) - 8.0) < 0.001, "Kopie um 8 m versetzt") + # Löschen: Gebäude ohne Teile verliert den Namen + var objects: Array = doc["objects"] + var del := GroupOps.members_of(doc, gid_kopie) + del.sort() + del.reverse() + for i in del: + objects.remove_at(i) + GroupState.prune_orphans(doc) + _check(BauOps.gebaeude_gruppen(doc).size() == 1, "nach Löschen: 1 Gebäude") + _check(not GroupState.names_dict(doc).has(str(gid_kopie)), "Gruppenname der gelöschten Kopie ist weg") -func _test_legacy_map_without_gebaeude() -> void: - print("Legacy-Maps") - var doc := SvmapDocument.create_new("Legacy", 256.0, "mittag") - doc.erase("gebaeude") # alte Map ohne die Sektion - var save := SvmapIO.save_to_file("user://test_bauen_legacy.svmap", doc) - _check(bool(save["ok"]), "Map OHNE gebaeude speichert") - var load := SvmapIO.load_from_file("user://test_bauen_legacy.svmap") - _check(bool(load["ok"]), "Map OHNE gebaeude lädt") - _check(BuildOps.piece_count(load["data"]) == 0, "Legacy-Map: 0 Bauteile, kein Crash") - if FileAccess.file_exists("user://test_bauen_legacy.svmap"): - DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_bauen_legacy.svmap")) +func _test_save_load() -> void: + print("Speichern / Laden") + var doc := _neue_doc() + BauOps.haus_demo(doc, null, Vector3(4.0, 0.0, 4.0)) + var n := (doc["objects"] as Array).size() + var res := SvmapIO.save_to_file("user://test_bauen.svmap", doc) + _check(bool(res["ok"]), "Map mit Haus speichern (Validierung V1–V17 grün)") + var back := SvmapIO.load_from_file("user://test_bauen.svmap") + _check(bool(back["ok"]), "…und wieder laden") + if bool(back["ok"]): + var d: Dictionary = back["data"] + _check((d["objects"] as Array).size() == n, "alle %d Teile nach dem Laden da" % n) + _check(BauOps.gebaeude_gruppen(d).size() == 1, "Gebäude-Gruppe erhalten") + _check(GroupState.display_name(d, 0).begins_with("Gebäude"), "Gebäude-Name erhalten") + _check(not d.has("gebaeude"), "keine alte gebaeude-Sektion mehr") + # Poly-Rechnung: Bauteile zählen im Budget wie jedes Objekt + var manifest: Variant = JSON.parse_string(FileAccess.get_file_as_string("res://library/objects.json")) + var poly := SvmapDocument.poly_usage(doc, manifest as Dictionary) + _check(absf(poly - float(BauOps.dreiecke_gesamt(doc))) < 0.5, "Poly-Budget = Summe der Bauteil-Dreiecke (%d)" % int(poly)) + + +func _test_migration() -> void: + print("Migration alte gebaeude-Sektion") + var doc := _neue_doc() + doc["gebaeude"] = [ + {"id": "fundament", "pos": [3.0, 0.0, 3.0], "rot": 0.0, "punkte": []}, + {"id": "wand", "pos": [2.0, 0.2, 2.0], "rot": 0.0, "punkte": []}, + {"id": "tuer", "pos": [4.0, 0.2, 4.0], "rot": 90.0, "punkte": []}, + {"id": "unbekannt", "pos": [0.0, 0.0, 0.0], "rot": 0.0}, + ] + var n := BauOps.migriere_gebaeude(doc) + _check(n == 3, "3 bekannte Teile übernommen (unbekannte ignoriert)") + _check(not doc.has("gebaeude"), "alte Sektion entfernt") + var objs: Array = doc["objects"] + var wand: Dictionary = {} + for o in objs: + if str(o["id"]) == "bau_wand": + wand = o + _check(not wand.is_empty() and absf(float(wand["pos"][0]) - 3.0) < 0.001 and absf(float(wand["pos"][2]) - 2.0) < 0.001, + "Wand-Ursprung wandert in die Wandmitte (2,2 → 3,2)") + var tuer: Dictionary = {} + for o in objs: + if str(o["id"]) == "bau_wand_tuer": + tuer = o + _check(not tuer.is_empty() and absf(float(tuer["pos"][0]) - 4.0) < 0.001 and absf(float(tuer["pos"][2]) - 3.0) < 0.001, + "gedrehte Tür-Wand (90°): Mitte (4, 3)") + _check(BauOps.gebaeude_gruppen(doc).size() == 1, "alle Teile in einem Gebäude") + _check(BauOps.migriere_gebaeude(doc) == 0, "zweiter Lauf tut nichts") + var leer := _neue_doc() + leer["gebaeude"] = [] + _check(BauOps.migriere_gebaeude(leer) == 0 and not leer.has("gebaeude"), "leere Sektion wird still entfernt") diff --git a/tests/test_build_spawn_mirror.gd b/tests/test_build_spawn_mirror.gd index f953b32..d8f326e 100644 --- a/tests/test_build_spawn_mirror.gd +++ b/tests/test_build_spawn_mirror.gd @@ -1,9 +1,9 @@ extends SceneTree -## Editor-Tests (headless): Karte t_ffa207b1 — M4 Bau-Teile + Spawnpunkte +## Editor-Tests (headless): Karte t_ffa207b1 — M4 Bauteile (jetzt normale Objekte) + Spawnpunkte ## spiegeln (Spec /opt/data/scopedvr/KARTEN_MIRROR_vc27.md, Mathe aus M1 ## symmetry_core.gd). Ausführen: ## godot --headless --path . --script res://tests/test_build_spawn_mirror.gd -## Exit 0 = alles grün. Läuft OHNE UI (BuildOps/SpawnOps-Statics + +## Exit 0 = alles grün. Läuft OHNE UI (BauOps/SpawnOps-Statics + ## ActionHistory) — GUI-Feinheiten (Statuszeile, Ghost) verifiziert der ## Desktop-Smoke, nicht hier. ## @@ -50,10 +50,7 @@ func _check(cond: bool, label: String) -> void: func _run_tests() -> void: _test_build_place_point() _test_build_place_axes() - _test_build_snap_parity() - _test_build_remove() - _test_build_update_follows() - _test_build_v9_bounds() + _test_build_replace_follows() _test_build_off_gegenprobe() _test_build_one_undo_step() _test_spawn_add_mirror() @@ -68,13 +65,6 @@ func _new_doc() -> Dictionary: return SvmapDocument.create_new("Sym-M4-Test", 256.0, "mittag") -func _piece_at(doc: Dictionary, index: int) -> Dictionary: - var list := BuildOps.pieces(doc) - if index < 0 or index >= list.size() or not (list[index] is Dictionary): - return {} - return list[index] as Dictionary - - func _pos3(p: Dictionary) -> Vector3: var pos: Array = p.get("pos", [0.0, 0.0, 0.0]) return Vector3(float(pos[0]), float(pos[1]), float(pos[2])) @@ -84,217 +74,89 @@ func _near(a: float, b: float) -> bool: return absf(a - b) < 0.001 -# ------------------------------------------------------- Bau-Teile: setzen --- +# ---------------------------------------- Bauteile (normale Objekte) -------- + +func _flach() -> Callable: + return func(_x: float, _z: float) -> float: return 0.0 + + +## Wand per echter Snap-Logik setzen (Hit knapp neben der Kantenmitte). +func _setze(doc: Dictionary, oid: String, hit: Vector3, mode: int, gid: int, spiegel := -1) -> Dictionary: + var snap := BuildSnap.berechne(oid, hit, 0.0, BuildSnap.sammle_teile(doc), _flach()) + return BauOps.setze_teil(doc, oid, snap, gid, mode, spiegel) + func _test_build_place_point() -> void: - print("Testgruppe: Bau-Teil setzen → Zwilling (Punktspiegelung)") + print("Testgruppe: Bauteil setzen → Zwilling (Punktspiegelung)") var doc := _new_doc() - var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.2, 20.0), 90.0) - BuildOps.ensure_block(doc).append(teil) - var msg: String = BuildOps.place_mirrored(doc, teil, POINT) - _check(msg == "", "Punktspiegelung: still gespiegelt (keine Meldung)") - _check(BuildOps.piece_count(doc) == 2, "2 Bauteile nach dem Setzen") - var twin := _piece_at(doc, 1) - _check(str(twin.get("id", "")) == "wand", "Zwilling hat gleiche kind_id") - _check(_near(_pos3(twin).x, -10.0) and _near(_pos3(twin).y, 0.2) and _near(_pos3(twin).z, -20.0), - "Zwilling-Position gespiegelt (−10, 0.2, −20)") - _check(_near(float(twin.get("rot", 0.0)), 270.0), "Zwilling-Drehung 90° + 180° = 270°") - var key := SymmetryCore.sym_of(teil) - _check(not key.is_empty() and SymmetryCore.sym_of(twin) == key, - "beide Teile tragen dieselbe sym-Kennung") - # V16-Realität: GENAU 2 Einträge pro Kennung. - var n := 0 - for t in BuildOps.pieces(doc): - if SymmetryCore.sym_of(t as Dictionary) == key: - n += 1 - _check(n == 2, "sym-Kennung verbindet genau 2 Teile") - # Zwilling ist eine tiefe Kopie (keine Alias-Falle bei punkte). - var first_x: float = (teil["punkte"] as Array)[0][0] - (teil["punkte"] as Array)[0][0] = 123.0 - _check(_near(float((twin["punkte"] as Array)[0][0]), first_x), - "Zwilling-Punkte sind Kopien (kein Aliasing)") + var gid := BauOps.gruppe_neu(doc) + var r := _setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), POINT, gid, gid + 1) + var objs: Array = doc["objects"] + _check(objs.size() == 2 and int(r["twin"]) == 1, "Original + Zwilling im Objekt-Block") + var orig: Dictionary = objs[0] + var twin: Dictionary = objs[1] + _check(str(twin["id"]) == "bau_wand", "Zwilling hat gleiche ID") + var op := _pos3(orig) + var tp := _pos3(twin) + _check(_near(tp.x, -op.x) and _near(tp.z, -op.z) and _near(tp.y, op.y), "Zwilling-Position punktgespiegelt") + _check(_near(fposmod(float(twin["rot_y_deg"]) - float(orig["rot_y_deg"]), 360.0), 180.0), "Zwilling um 180° gedreht") + var key := SymmetryCore.sym_of(orig) + _check(not key.is_empty() and SymmetryCore.sym_of(twin) == key, "gleiche sym-Kennung") + _check(GroupOps.group_id(orig) == gid and GroupOps.group_id(twin) == gid + 1, "Zwilling bekommt das Spiegel-Gebäude") func _test_build_place_axes() -> void: - print("Testgruppe: Bau-Teil setzen → Zwilling (Achse X / Achse Z)") + print("Testgruppe: Bauteil setzen → Zwilling (Achsen X / Z)") + for mode in [AXIS_X, AXIS_Z]: + var doc := _new_doc() + var gid := BauOps.gruppe_neu(doc) + _setze(doc, "bau_fundament", Vector3(11.0, 0.0, 21.0), mode, gid, gid + 1) + var objs: Array = doc["objects"] + _check(objs.size() == 2, "Modus %d: Zwilling da" % mode) + var op := _pos3(objs[0]) + var tp := _pos3(objs[1]) + if mode == AXIS_X: + _check(_near(tp.x, -op.x) and _near(tp.z, op.z), "Achse X: x gespiegelt, z gleich") + else: + _check(_near(tp.z, -op.z) and _near(tp.x, op.x), "Achse Z: z gespiegelt, x gleich") + + +func _test_build_replace_follows() -> void: + print("Testgruppe: Tür einsetzen → Zwilling bekommt auch die Tür") var doc := _new_doc() - var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) - BuildOps.ensure_block(doc).append(teil) - BuildOps.place_mirrored(doc, teil, AXIS_X) - var twin := _piece_at(doc, 1) - _check(_near(_pos3(twin).x, -10.0) and _near(_pos3(twin).z, 20.0), - "Achse X: x gespiegelt, z bleibt") - _check(_near(float(twin.get("rot", 0.0)), 270.0), "Achse X: Drehung −90° = 270°") - var doc2 := _new_doc() - var teil2 := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) - BuildOps.ensure_block(doc2).append(teil2) - BuildOps.place_mirrored(doc2, teil2, AXIS_Z) - var twin2 := _piece_at(doc2, 1) - _check(_near(_pos3(twin2).x, 10.0) and _near(_pos3(twin2).z, -20.0), - "Achse Z: z gespiegelt, x bleibt") - _check(_near(float(twin2.get("rot", 0.0)), 270.0), "Achse Z: Drehung −90° = 270°") - # Auf der Mitte: nicht gedoppelt (Spec vc27). - var doc3 := _new_doc() - var mitte := BuildOps.make_piece("wand", Vector3(0.0, 0.0, 0.0), 0.0) - BuildOps.ensure_block(doc3).append(mitte) - var msg: String = BuildOps.place_mirrored(doc3, mitte, POINT) - _check(msg.contains("nicht gedoppelt"), "Bauteil auf der Mitte: Meldung statt Zwilling") - _check(BuildOps.piece_count(doc3) == 1, "Auf der Mitte: nur 1 Bauteil") - _check(SymmetryCore.sym_of(mitte).is_empty(), "Auf der Mitte: kein sym-Link") + var gid := BauOps.gruppe_neu(doc) + _setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), POINT, gid, gid + 1) + var r := _setze(doc, "bau_wand_tuer", Vector3(11.0, 1.5, 20.1), POINT, gid, gid + 1) + var objs: Array = doc["objects"] + _check(bool(r["ersetzt"]) and objs.size() == 2, "ersetzt statt anhängen") + _check(str(objs[0]["id"]) == "bau_wand_tuer" and str(objs[1]["id"]) == "bau_wand_tuer", "Original UND Zwilling haben die Tür") -## Gleiche Snap-Regeln: Spiegelung erhält die Raster-Parität -## (Raster „ecke" = gerade Meter, „mitte" = ungerade Meter) bei zentrierter Karte. -func _test_build_snap_parity() -> void: - print("Testgruppe: Snap-Parität bleibt erhalten") - var doc := _new_doc() - var ecke := BuildOps.make_piece("wand", Vector3(2.0, 0.0, 4.0), 0.0) - BuildOps.ensure_block(doc).append(ecke) - BuildOps.place_mirrored(doc, ecke, POINT) - _check(_near(_pos3(_piece_at(doc, 1)).x, -2.0) and _near(_pos3(_piece_at(doc, 1)).z, -4.0), - "ecke-Teil: gerade Meter bleiben gerade (−2, −4)") - var doc2 := _new_doc() - var mitte := BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0) - BuildOps.ensure_block(doc2).append(mitte) - BuildOps.place_mirrored(doc2, mitte, POINT) - _check(_near(_pos3(_piece_at(doc2, 1)).x, -3.0) and _near(_pos3(_piece_at(doc2, 1)).z, -3.0), - "mitte-Teil: ungerade Meter bleiben ungerade (−3, −3)") - - -# ----------------------------------------------------- Bau-Teile: entfernen --- - -func _test_build_remove() -> void: - print("Testgruppe: Bau-Teil entfernen → beide weg") - var doc := _new_doc() - var teil := BuildOps.make_piece("boden", Vector3(11.0, 0.0, 21.0), 0.0) - BuildOps.ensure_block(doc).append(teil) - BuildOps.place_mirrored(doc, teil, POINT) - _check(BuildOps.piece_count(doc) == 2, "Vorbereitung: 2 Bauteile") - var msg: String = BuildOps.remove_mirrored(doc, 0, POINT) - _check(BuildOps.piece_count(doc) == 0, "Original löschen → Zwilling auch weg") - _check(msg.contains("Zwilling"), "Status meldet den mitgelöschten Zwilling") - # Umgekehrt: Zwilling löschen (Index 1) → Original mit weg. - var doc2 := _new_doc() - var teil2 := BuildOps.make_piece("boden", Vector3(11.0, 0.0, 21.0), 0.0) - BuildOps.ensure_block(doc2).append(teil2) - BuildOps.place_mirrored(doc2, teil2, POINT) - BuildOps.remove_mirrored(doc2, 1, POINT) - _check(BuildOps.piece_count(doc2) == 0, "Zwilling löschen → Original auch weg") - # Paar + Fremdteil: Löschung des Fremdteils lässt das Paar unangetastet. - var doc3 := _new_doc() - var a := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 0.0), 0.0) - BuildOps.ensure_block(doc3).append(a) - BuildOps.place_mirrored(doc3, a, POINT) - var fremd := BuildOps.make_piece("boden", Vector3(30.0, 0.0, 0.0), 0.0) - BuildOps.ensure_block(doc3).append(fremd) - BuildOps.remove_mirrored(doc3, 2, POINT) - _check(BuildOps.piece_count(doc3) == 2, "Fremdteil weg, Paar bleibt (2 Teile)") - _check(not _piece_at(doc3, 0).is_empty() and not _piece_at(doc3, 1).is_empty(), - "Paar-Indizes unverändert verschoben") - - -# ------------------------------------- Bau-Teile: verschieben/drehen/anheben --- - -func _test_build_update_follows() -> void: - print("Testgruppe: Verschieben/Drehen/Anheben — Zwilling folgt") - var doc := _new_doc() - var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) - BuildOps.ensure_block(doc).append(teil) - BuildOps.place_mirrored(doc, teil, POINT) - # Verschieben (Pfeiltaste-Muster): Original auf neue Position, dann sync. - teil["pos"] = [14.0, 0.0, 20.0] - BuildOps.update_mirrored(doc, 0, POINT) - var twin := _piece_at(doc, 1) - _check(_near(_pos3(twin).x, -14.0) and _near(_pos3(twin).z, -20.0), - "Zwilling folgt der verschobenen Position gespiegelt") - # Drehen: +90° am Original → Zwilling = gespiegelte NEUE Drehung. - teil["rot"] = 180.0 - BuildOps.update_mirrored(doc, 0, POINT) - _check(_near(float((_piece_at(doc, 1) as Dictionary).get("rot", 0.0)), 0.0), - "Zwilling-Drehung: 180° + 180° = 0° (modulo 360)") - # Anheben: y wandert 1:1 mit (Höhe ist ein Skalar). - teil["pos"] = [14.0, 2.4, 20.0] - BuildOps.update_mirrored(doc, 0, POINT) - _check(_near(_pos3(_piece_at(doc, 1)).y, 2.4), "Zwilling-y folgt 1:1 (Skalar)") - # Teil OHNE sym-Partner (außerhalb des Modus gebaut) bleibt unberührt. - var doc2 := _new_doc() - var solo := BuildOps.make_piece("wand", Vector3(6.0, 0.0, 0.0), 0.0) - BuildOps.ensure_block(doc2).append(solo) - _check(BuildOps.update_mirrored(doc2, 0, POINT) == "", "Teil ohne sym: kein Zwilling, keine Meldung") - _check(BuildOps.piece_count(doc2) == 1, "Teil ohne sym: nichts wurde gedoppelt") - - -# ------------------------------------------------------- Bau-Teile: V9 ------- - -## V9-Realität (deshalb so getestet): bei zentrierten bounds bildet die -## Spiegelung gültiger Elemente bildet IMMER auf gültige Stellen ab — der V9-Fall -## entsteht, wenn das ORIGINAL bereits außerhalb liegt (Editor erzwingt die -## Grenzen beim Nudge/Baken nicht) oder Grenzen asymmetrisch sind. Dann -## verweigert der Zwilling die Speicherung mit Klartext-Meldung. -func _test_build_v9_bounds() -> void: - print("Testgruppe: Bau-Teil — V9-Grenzen scharf") - var doc := _new_doc() - # Original außerhalb (z = 200 > 128 + 1 Toleranz): auch der Zwilling läge - # außerhalb → Meldung statt still zu speichern. - var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 200.0), 0.0) - BuildOps.ensure_block(doc).append(teil) - var msg: String = BuildOps.place_mirrored(doc, teil, POINT) - _check(msg.contains("V9"), "V9-Meldung statt still zu speichern") - _check(msg.contains("Match-Grenzen"), "Meldung nennt die Match-Grenzen") - _check(BuildOps.piece_count(doc) == 1, "V9-Fall: NUR das Original gespeichert") - _check(SymmetryCore.sym_of(teil).is_empty(), "V9-Fall: kein halber sym-Link zurückbleibt") - # Grenz-Nähe INNERHALB (Toleranz ±1 m): spiegeln bleibt erlaubt. - var doc2 := _new_doc() - var nah := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 127.5), 0.0) - BuildOps.ensure_block(doc2).append(nah) - _check(BuildOps.place_mirrored(doc2, nah, POINT) == "", - "nahe der Grenze (|z| ≤ 128 + 1): gespiegelt ok") - _check(BuildOps.piece_count(doc2) == 2, "Grenz-Nähe: Zwilling vorhanden") - # update_mirrored: Original aus den Grenzen verschoben → Zwilling bleibt unverändert. - var doc3 := _new_doc() - var t3 := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 30.0), 0.0) - BuildOps.ensure_block(doc3).append(t3) - BuildOps.place_mirrored(doc3, t3, POINT) - t3["pos"] = [10.0, 85.0, 30.0] # y = 85 > 80 + 1 → gespiegelt ebenfalls draußen - var msg3: String = BuildOps.update_mirrored(doc3, 0, POINT) - _check(msg3.contains("V9"), "update: V9-Meldung statt Zwilling verschieben") - _check(_near(_pos3(_piece_at(doc3, 1)).z, -30.0) and _near(_pos3(_piece_at(doc3, 1)).y, 0.0), - "update-V9: Zwilling blieb UNVERÄNDERT (−30 / y 0)") - - -# ------------------------------------------------- Bau-Teile: Modus AUS ----- - func _test_build_off_gegenprobe() -> void: - print("Testgruppe: Modus AUS = Verhalten wie bisher (Bau-Teile)") + print("Testgruppe: Symmetrie AUS = ein Teil, kein Zwilling") var doc := _new_doc() - var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) - BuildOps.ensure_block(doc).append(teil) - _check(BuildOps.place_mirrored(doc, teil, OFF) == "", "OFF: keine Meldung, kein Zwilling") - _check(BuildOps.piece_count(doc) == 1, "OFF: nur 1 Bauteil") - _check(SymmetryCore.sym_of(teil).is_empty(), "OFF: kein sym-Feld gesetzt") - teil["pos"] = [12.0, 0.0, 20.0] - BuildOps.update_mirrored(doc, 0, OFF) - _check(BuildOps.piece_count(doc) == 1, "OFF: Verschieben dupliziert nicht") - var msg: String = BuildOps.remove_mirrored(doc, 0, OFF) - _check(BuildOps.piece_count(doc) == 0 and msg == "", "OFF: Löschen entfernt genau 1 Teil") + var gid := BauOps.gruppe_neu(doc) + var r := _setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), OFF, gid) + _check((doc["objects"] as Array).size() == 1 and int(r["twin"]) == -1, "kein Zwilling im Modus AUS") + _check(SymmetryCore.sym_of((doc["objects"] as Array)[0]).is_empty(), "keine sym-Kennung") -## EIN Undo-Schritt: Original + Zwilling springen zusammen zurück. func _test_build_one_undo_step() -> void: - print("Testgruppe: Bau-Teile — EIN Undo-Schritt für beide Seiten") + print("Testgruppe: EIN Undo-Schritt nimmt Original + Zwilling zurück") var doc := _new_doc() var hist := ActionHistory.new() hist.begin(doc) - var before: Dictionary = hist.snapshot(["gebaeude"]) - var teil := BuildOps.make_piece("wand", Vector3(10.0, 0.0, 20.0), 90.0) - BuildOps.ensure_block(doc).append(teil) - BuildOps.place_mirrored(doc, teil, POINT) - hist.commit("Wand gesetzt", before, hist.snapshot(["gebaeude"])) - _check(BuildOps.piece_count(doc) == 2, "commit: beide Teile drin") + var before := hist.snapshot(["objects", "group_names"]) + if not before.has("group_names"): + before["group_names"] = null + var gid := BauOps.gruppe_neu(doc) + _setze(doc, "bau_wand", Vector3(11.0, 0.0, 20.1), POINT, gid, gid + 1) + hist.commit("Wand bauen", before, hist.snapshot(["objects", "group_names"])) + _check((doc["objects"] as Array).size() == 2, "nach dem Setzen: 2 Teile") hist.undo() - _check(BuildOps.piece_count(doc) == 0, "EIN undo: Original UND Zwilling weg") + _check((doc["objects"] as Array).is_empty(), "EIN Undo: beide weg") hist.redo() - _check(BuildOps.piece_count(doc) == 2, "EIN redo: beide wieder da") + _check((doc["objects"] as Array).size() == 2, "Redo: beide wieder da") # -------------------------------------------------------- Spawns: setzen ----- diff --git a/tests/test_gui_smoke.gd b/tests/test_gui_smoke.gd index 80df5cc..ca42952 100644 --- a/tests/test_gui_smoke.gd +++ b/tests/test_gui_smoke.gd @@ -224,8 +224,7 @@ func _test_shortcuts_karte3() -> void: main._unhandled_key_input(_press_key(KEY_F2, false)) _check(str(main.get("tool_mode")) == "objects", "F2: zurück zum Objekt-Werkzeug") main._unhandled_key_input(_press_key(KEY_F3, false)) - _check(str(main.get("tool_mode")) == "bauen", "F3: Bauen-Werkzeug aktiv") - _check(str(main.tool_label.text).contains("Bauen"), "Statuszeile: Werkzeug-Label zeigt Bauen") + _check(str(main.get("tool_mode")) == "objects" and (main.ui_right_panel as TabContainer).current_tab == main.bauen_mod.get_index(), "F3: Bauen-Tab geöffnet") main._unhandled_key_input(_press_key(KEY_F4, false)) _check(str(main.get("tool_mode")) == "spawns", "F4: Spawn-Werkzeug aktiv") main._unhandled_key_input(_press_key(KEY_F2, false)) diff --git a/tests/test_playtest.gd b/tests/test_playtest.gd index 297c2cf..5634e8b 100644 --- a/tests/test_playtest.gd +++ b/tests/test_playtest.gd @@ -452,12 +452,12 @@ func _test_bauteile_treppe_tuer() -> void: # Erst raus aus dem laufenden Testlauf, dann Map mit Bauteilen neu starten. pt.call("handle_escape") await process_frame - var build_ops := load("res://src/editor/build_ops.gd") var doc: Dictionary = SvmapDocument.create_new("Bauteil-Karte", 100.0, "klar") var t0: float = SvmapDocument.terrain_height_at(doc, 0.0, 0.0) - var treppe: Dictionary = build_ops.call("make_piece", "treppe", Vector3(4.0, t0, 0.0), 0.0) - var tuer: Dictionary = build_ops.call("make_piece", "tuer", Vector3(0.0, t0, 14.0), 0.0) - doc["gebaeude"] = [treppe, tuer] + # Treppe (4 m lang, 3 m hoch, Lauf +x) und Tür-Wand als NORMALE Objekte (bau_*). + var treppe := {"id": "bau_treppe", "pos": [6.0, t0, 0.0], "scale": 1.0, "rot_y_deg": 0.0} + var tuer := {"id": "bau_wand_tuer", "pos": [1.0, t0, 14.0], "scale": 1.0, "rot_y_deg": 0.0} + doc["objects"] = [treppe, tuer] main.set("doc", doc) main.set("current_path", "") main.set("dirty", false) @@ -475,11 +475,11 @@ func _test_bauteile_treppe_tuer() -> void: var kb: Node3D = pt2.get("_kollider_root") var bauteile := 0 for child in kb.get_children(): - if str(child.name).begins_with("Bauteil_Kollider"): + if str(child.name).contains("_bau_"): bauteile += 1 _check(bauteile == 2, "2 Bauteil-Kollider gebaut (Treppe + Tür, ist %d)" % bauteile) - # a) Treppe (2 m lang, 1 m hoch, 10 Stufen à 0.1 m): hochlaufen. + # a) Treppe (4 m lang, 3 m hoch, 15 Stufen à 0.2 m): hochlaufen. # Erst VOR die Treppe stellen (Spawn liegt weit weg — Karte Mitte). body.set("position", Vector3(0.0, t0 + PlaytestBody.HALF_H + 0.05, 0.0)) body.set("velocity", Vector3.ZERO) @@ -495,10 +495,10 @@ func _test_bauteile_treppe_tuer() -> void: body.set("auto_move_dir", Vector3.ZERO) for i in 10: await physics_frame - _check(feet_max >= 0.9, - "Treppe (1.0 m, Stufen à 0.1) erklommen (max feet %.2f)" % feet_max) + _check(feet_max >= t0 + 2.5, + "Treppe (3.0 m, Stufen à 0.2) erklommen (max feet %.2f, Start %.2f)" % [feet_max, t0]) - # b) Tür (Wand 2 m entlang x, Öffnung 1 × 2 m): durchgehen (Lauf +z). + # b) Tür (Wand 2 m entlang x, Öffnung 1,2 × 2,2 m): durchgehen (Lauf +z). var t0_2: float = body.call("terrain_height", 1.0, 16.0) body.set("position", Vector3(1.0, t0_2 + PlaytestBody.HALF_H + 0.05, 16.0)) body.set("velocity", Vector3.ZERO) diff --git a/tests/test_rmb_vorrang.gd b/tests/test_rmb_vorrang.gd index d07c2d4..13e422f 100644 --- a/tests/test_rmb_vorrang.gd +++ b/tests/test_rmb_vorrang.gd @@ -253,53 +253,36 @@ func _test_placement_rmb_keeps_hand() -> void: _check(not bool(interaction.get("placing")), "Esc wirft das Objekt ab (Platzieren aus)") -## Abnahme 4: Bauen-Werkzeug — RMB fliegt, wechselt NICHT das Werkzeug und -## behält die Bauteil-Auswahl. +## Abnahme 4: Bauen — RMB fliegt, die Bauteil-Serie (Teil in der Hand) bleibt. func _test_build_rmb_flies() -> void: - print("Testgruppe: Bauen-Werkzeug + RMB = Flug (Werkzeug/Auswahl bleiben)") - main.call("_set_tool_mode", "bauen") - _check(str(main.get("tool_mode")) == "bauen", "Bauen-Werkzeug aktiv") - var build: Node = main.get("build_tool") + print("Testgruppe: Bauen + RMB = Flug (Teil bleibt in der Hand)") + main.call("_set_tool_mode", "objects") + var interaction: Node = main.get("interaction") var pos := _vp_center() _motion(pos) - - # Ein Bauteil setzen, dann per Umschalt+Klick auswählen — Klickpunkt ist - # die UNPROJIZIERTE Bauteil-Mitte (der gerastete Fußpunkt liegt sonst exakt - # auf der Kante des Center-Strahls). + interaction.call("start_placement", "bau_fundament") + _check(bool(interaction.get("placing")) and str(interaction.get("place_oid")) == "bau_fundament", "Bauteil in der Hand") _motion(pos) _mouse(MOUSE_BUTTON_LEFT, true, pos) _mouse(MOUSE_BUTTON_LEFT, false, pos) await process_frame + _check(_build_piece_count() > 0, "Bauteil per Klick gesetzt (normales Objekt)") + _check(bool(interaction.get("placing")), "Serie: Teil bleibt nach dem Setzen in der Hand") var doc: Dictionary = main.doc - var gebaeude: Array = doc.get("gebaeude", []) - _check(gebaeude.size() > 0, "Bauteil gesetzt") - if gebaeude.is_empty(): - _finish() - return - var ppos: Array = (gebaeude[0] as Dictionary).get("pos", [0.0, 0.0, 0.0]) - var piece_center := Vector3(float(ppos[0]), float(ppos[1]) + 0.3, float(ppos[2])) - # Regression (t_555e33da): Selektion muss im DEFAULT-Framing funktionieren — - # kein Heranzoomen mehr nötig (früher: ray_pick-Cap 500 m < Kamera-Distanz). - var on_piece := camera.unproject_position(piece_center) - _motion(on_piece) - _mouse(MOUSE_BUTTON_LEFT, true, on_piece, false, true) - _mouse(MOUSE_BUTTON_LEFT, false, on_piece, false, true) - await process_frame - var sel := int(build.get("selected_index")) - _check(sel >= 0, "Bauteil per Umschalt+Klick ausgewählt") + _check(GroupOps.group_id((doc["objects"] as Array).back()) != GroupOps.UNGROUPED, "Teil gehört zu einem Gebäude (Gruppe)") - _mouse(MOUSE_BUTTON_RIGHT, true, on_piece) + _mouse(MOUSE_BUTTON_RIGHT, true, pos) await process_frame - _check(camera.call("fly_active"), "RMB → Flug aktiv (im Bauen-Werkzeug)") - _check(str(main.get("tool_mode")) == "bauen", "RMB wechselt NICHT mehr das Werkzeug") - _check(int(build.get("selected_index")) == sel, "RMB wirft die Bauteil-Auswahl NICHT weg") + _check(camera.call("fly_active"), "RMB → Flug aktiv") + _check(bool(interaction.get("placing")), "RMB wirft das Teil NICHT ab") _mouse(MOUSE_BUTTON_RIGHT, false, pos) await process_frame _check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus") - - main.call("_set_tool_mode", "objects") + main._unhandled_key_input(_press_key(KEY_ESCAPE)) + await process_frame + _check(not bool(interaction.get("placing")), "Esc beendet das Bauen") func _build_piece_count() -> int: var doc: Dictionary = main.doc - return int((doc.get("gebaeude", []) as Array).size()) + return BauOps.teile_zahl(doc) diff --git a/tests/test_selection_look.gd b/tests/test_selection_look.gd index dcaecd7..e51c33b 100644 --- a/tests/test_selection_look.gd +++ b/tests/test_selection_look.gd @@ -39,7 +39,6 @@ func _run_all() -> void: await _test_hover_abstufung() await _test_zwilling() await _test_spawn_ring() - await _test_gebaeude_kontur() if failures == 0: print("ALL OK: %d checks" % checks) quit(0) @@ -262,26 +261,3 @@ func _test_spawn_ring() -> void: await process_frame _check(not marker.has_meta("spawn_ring"), "Abwahl: Ring-Meta restlos weg") builder.queue_free() - - -# ------------------------------------------------------------------ T6 ------ -func _test_gebaeude_kontur() -> void: - print("== T6 Bau-Teile: Kontur statt Vollbox ==") - var doc := _flat_doc() - var teil: Dictionary = BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 0.0) - _check(not teil.is_empty(), "Proben-Bauteil angelegt") - (doc["gebaeude"] as Array).append(teil) - var builder := _build(_manifest(), doc) - builder.call("set_gebaeude_selected", 0) - await process_frame - var sel: Variant = builder.get("_gebaeude_sel") - _check(sel is MeshInstance3D and is_instance_valid(sel), "Bauteil-Auswahl: Kontur-Mesh existiert") - if sel is MeshInstance3D: - _check((sel as MeshInstance3D).mesh != null, "Kontur trägt das Bauteil-Mesh") - _check((sel as MeshInstance3D).material_override == builder.get("_outline_mat_sel"), - "Kontur trägt das Auswahl-Material (Theme)") - builder.call("set_gebaeude_selected", -1) - await process_frame - var sel2: Variant = builder.get("_gebaeude_sel") - _check(sel2 == null, "Bauteil-Abwahl: Kontur restlos weg") - builder.queue_free() diff --git a/tests/test_shortcuts.gd b/tests/test_shortcuts.gd index 4d8cc55..b483c96 100644 --- a/tests/test_shortcuts.gd +++ b/tests/test_shortcuts.gd @@ -32,7 +32,6 @@ func _init() -> void: _test_lift_multi() _test_snap_to_floor() _test_grab() - _test_build_nudge_floor() _test_brush_paint_mode() if failures == 0: print("ALL OK: %d checks" % checks) @@ -101,14 +100,10 @@ class ProbeMain: class ProbeBuilder extends Node3D: var object_markers: Array = [] var last_selection: Array = [] - var last_gebaeude_selection := -99 func set_selected_multi(indices: Array) -> void: last_selection = indices.duplicate() - func set_gebaeude_selected(idx: int) -> void: - last_gebaeude_selection = idx - # ------------------------------------------------------------ InputMap ------- @@ -387,9 +382,8 @@ func _test_esc_policy() -> void: print("Testgruppe: Esc-Zustandswechsel (ShortcutPolicy)") _check(ShortcutPolicy.esc_action("terrain", false, false) == "exit_terrain", "Esc im Terrain-Werkzeug → Werkzeug verlassen") _check(ShortcutPolicy.esc_action("spawns", false, false) == "exit_spawns", "Esc im Spawn-Werkzeug → Werkzeug verlassen") - _check(ShortcutPolicy.esc_action("bauen", false, false) == "exit_bauen", "Esc im Bauen-Modus → Werkzeug verlassen") _check(ShortcutPolicy.esc_action("teststart", false, false) == "exit_teststart", "Esc im Test-Start-Werkzeug → Werkzeug verlassen (Punkt bleibt)") - _check(ShortcutPolicy.esc_action("bauen", true, true) == "exit_bauen", "Werkzeug hat VORRANG vor Platzieren") + _check(ShortcutPolicy.esc_action("terrain", true, true) == "exit_terrain", "Werkzeug hat VORRANG vor Platzieren") _check(ShortcutPolicy.esc_action("objects", true, true) == "cancel_placement", "Esc beim Platzieren → Ghost-Preview abbrechen") _check(ShortcutPolicy.esc_action("objects", false, true) == "clear_selection", "Esc mit Auswahl → Auswahl aufheben") _check(ShortcutPolicy.esc_action("objects", false, false) == "none", "Esc ohne nichts → kein Zustandswechsel") @@ -698,25 +692,6 @@ func _test_rotate_90() -> void: fake.history.undo() _check(absf(float(objects[0]["rot_y_deg"]) - 30.0) < 0.0001, "Strg+Z: zweite Ebene — Einzel-Drehung auch zurück (30°)") - # Bauen-Modus: Bauteil rotiert um seinen Fußpunkt (Position unverändert). - var build_doc := {"gebaeude": [ - {"id": "wand", "pos": [4.0, 0.0, 6.0], "rot": 0.0, "punkte": [[0, 0, 0], [2, 0, 0]]}, - ]} - var bfake := ProbeMain.new() - bfake.doc = build_doc - bfake.history.begin(bfake.doc) - var bbuilder := ProbeBuilder.new() - root.add_child(bbuilder) - var btool := Node3D.new() - btool.set_script(load("res://src/editor/build_tool.gd")) - root.add_child(btool) - btool.setup(bfake, Camera3D.new(), bbuilder, interaction) - btool.select_index(0) - btool.rotate_input() - var list: Array = BuildOps.pieces(build_doc) - _check(absf(float(list[0]["rot"]) - 90.0) < 0.0001, "Bauen: R dreht Bauteil 90°") - _check(float(list[0]["pos"][0]) == 4.0 and float(list[0]["pos"][2]) == 6.0, - "Bauen: Fußpunkt bleibt (Drehung um den Fußpunkt)") # ---------------------------------------------------- Pfeiltasten 2-m-Raster -- @@ -873,40 +848,6 @@ func _test_grab() -> void: _check(fake.statuses.size() > 0, "G ohne Auswahl: hilfreiche Meldung statt Stille") -# ------------------------------------------- Bauen: Pfeile + Strg+Bild↑ ------- - -func _test_build_nudge_floor() -> void: - print("Testgruppe: Bauen-Modus — Pfeiltasten (2-m-Raster) + Strg+Bild↑") - var doc := {"gebaeude": [ - {"id": "wand", "pos": [4.0, 0.0, 6.0], "rot": 0.0, "punkte": [[0, 0, 0], [2, 0, 0]]}, - ]} - var bfake := ProbeMain.new() - bfake.doc = doc - bfake.history.begin(bfake.doc) - var bbuilder := ProbeBuilder.new() - root.add_child(bbuilder) - var btool := Node3D.new() - btool.set_script(load("res://src/editor/build_tool.gd")) - root.add_child(btool) - btool.setup(bfake, Camera3D.new(), bbuilder, null) - btool.select_index(0) - btool.nudge_selected(Vector3(0, 0, -2)) - var list: Array = BuildOps.pieces(doc) - _check(float(list[0]["pos"][2]) == 4.0 and float(list[0]["pos"][0]) == 4.0, - "Bauen-Pfeil: exakt 2 m, Raster-Ausrichtung bleibt") - _check(bfake.history.can_undo(), "Bauen-Pfeil: EIN History-Eintrag") - bfake.history.undo() - _check(float(list[0]["pos"][2]) == 6.0, "Bauen-Pfeil: Strg+Z zurück") - # Strg+Bild↑: Bauteil auf Terrain-Höhe (flaches Terrain → 0 m, 5-cm-Raster). - list[0]["pos"] = [4.0, 2.5, 6.0] - btool.snap_selected_to_floor() - _check(absf(float(list[0]["pos"][1])) < 0.0001, "Bauen Strg+Bild↑: Bauteil auf dem Boden (0 m)") - # Erneut: liegt schon am Boden → Meldung, kein Commit. - var can_undo: bool = bfake.history.can_undo() - btool.snap_selected_to_floor() - _check(bfake.history.can_undo() == can_undo, "Bauen Strg+Bild↑ am Boden: kein leerer Commit") - - # ------------------------------------------ Terrain-Ziffer 4 = Malen (Fix) ---- func _test_brush_paint_mode() -> void: diff --git a/tests/test_symmetry_core.gd b/tests/test_symmetry_core.gd index b84f981..f7bbabc 100644 --- a/tests/test_symmetry_core.gd +++ b/tests/test_symmetry_core.gd @@ -185,7 +185,7 @@ func _test_partner_link() -> void: var obj := {"id": "baum", "pos": [1.0, 0.0, 2.0], "rot_y_deg": 0.0} _check(SymmetryCore.sym_of(obj) == "", "Objekt ohne sym → leere Kennung") _check(SymmetryCore.sym_of({"id": "x", "sym": 42}) == "", "Defektes sym (Zahl) → leer, kein Crash") - var key := SymmetryCore.next_sym_key({"objects": [], "gebaeude": []}) + var key := SymmetryCore.next_sym_key({"objects": []}) _check(key == "sym_1", "Erste freie Kennung = sym_1") SymmetryCore.set_sym(obj, key) _check(SymmetryCore.sym_of(obj) == key, "set_sym setzt die Kennung") @@ -196,17 +196,16 @@ func _test_partner_link() -> void: {"id": "a", "pos": [0.0, 0.0, 0.0], "sym": "sym_1"}, {"id": "b", "pos": [0.0, 0.0, 0.0]}, {"id": "c", "pos": [0.0, 0.0, 0.0], "sym": "sym_2"}, - ], "gebaeude": [ - {"id": "wand", "pos": [0.0, 0.0, 0.0], "sym": "sym_3"}, + {"id": "bau_wand", "pos": [0.0, 0.0, 0.0], "sym": "sym_3"}, ]} _check(SymmetryCore.next_sym_key(doc) == "sym_4", - "next_sym_key überspringt belegte Kennungen über BEIDE Blöcke") + "next_sym_key überspringt belegte Kennungen (Bauteile sind normale Objekte)") _check(SymmetryCore.find_partner_index(doc["objects"], "sym_2") == 2, "find_partner_index findet den Partner") _check(SymmetryCore.find_partner_index(doc["objects"], "sym_2", 2) == -1, "find_partner_index schließt den Index selbst aus") - _check(SymmetryCore.find_partner_index(doc["gebaeude"], "sym_3") == 0, - "find_partner_index funktioniert auch im gebaeude-Block") + _check(SymmetryCore.find_partner_index(doc["objects"], "sym_3") == 3, + "find_partner_index findet auch Bauteile") _check(SymmetryCore.find_partner_index(doc["objects"], "") == -1, "Leere Kennung → kein Partner") diff --git a/tests/test_terrain.gd b/tests/test_terrain.gd index 99a487f..8d6d36d 100644 --- a/tests/test_terrain.gd +++ b/tests/test_terrain.gd @@ -1027,8 +1027,7 @@ func _test_resize_grow() -> void: _check((grown["objects"] as Array).size() == 1 and str((grown["objects"] as Array)[0]["id"]) == "baum_laub_1", "Objekte unverändert übernommen") - _check((grown["gebaeude"] as Array).is_empty() and not grown.has("checksum"), - "Bauteile übernommen, alte Prüfsumme verworfen") + _check(not grown.has("checksum"), "alte Prüfsumme verworfen") _check(absf(float(grown["bounds"]["max"][0]) - 256.0) < 0.0001 and absf(float(grown["bounds"]["min"][2]) + 256.0) < 0.0001 and absf(float(grown["bounds"]["max"][1]) - 80.0) < 0.0001, @@ -1093,9 +1092,9 @@ func _test_resize_shrink() -> void: # Bauteile: Wand mit Fußabdruck ÜBER der Kante (Punkte 127→129 m) = drin, # Fundament komplett draußen, gedrehte Wand über der -Z-Kante = drin. doc["gebaeude"] = [ - BuildOps.make_piece("wand", Vector3(127.0, 0.0, 10.0), 0.0), - BuildOps.make_piece("fundament", Vector3(150.0, 0.0, -50.0), 0.0), - BuildOps.make_piece("wand", Vector3(0.0, 0.0, -127.0), 90.0), + {"id": "wand", "pos": [127.0, 0.0, 10.0], "rot": 0.0}, + {"id": "fundament", "pos": [150.0, 0.0, -50.0], "rot": 0.0}, + {"id": "wand", "pos": [0.0, 0.0, -127.0], "rot": 90.0}, ] # Spawn-Punkte drin/draußen gemischt (Default-Zonen von 512 m lägen alle # bei ±204,8 m draußen — hier bewusst gemischt, damit nach dem Filter diff --git a/tests/test_ui_aufraeumen.gd b/tests/test_ui_aufraeumen.gd index c60d243..99f1df5 100644 --- a/tests/test_ui_aufraeumen.gd +++ b/tests/test_ui_aufraeumen.gd @@ -32,7 +32,7 @@ func _run() -> void: _check(main.malen_mod != null and main.bauen_mod != null, "Module malen/bauen gebaut") _check(main.terrain_mod.terrain_mode_paint.get_parent() == main.terrain_mod.terrain_panel and not main.terrain_mod.terrain_mode_paint.visible, "Mal-Modus nicht mehr als Knopf im Terrain-Tab") - _check(main.build_edit_btn.get_parent() == main.bauen_mod, "Bauen-Knopf wohnt im Bauen-Tab") + _check(main.bauen_mod.bau_buttons.size() >= BauKatalog.KINDS.size(), "Bauen-Tab hat einen Knopf je Bauteil (%d)" % main.bauen_mod.bau_buttons.size()) _check(main.malen_mod.mat_buttons.size() == SvmapIO.MATERIAL_IDS.size(), "Malen: eine Kachel je Material") print("== Tab-Klick aktiviert das Werkzeug direkt ==") @@ -47,7 +47,7 @@ func _run() -> void: main.malen_mod.radius_slider.value = 20.0 _check(int(main.terrain_mod.terrain_radius_slider.value) == 20, "Malen-Radius steuert den Pinsel") tabs.current_tab = main.bauen_mod.get_index() - _check(str(main.tool_mode) == "bauen" and main.build_panel.visible, "Tab Bauen: Bauen sofort an, Bauteile offen") + _check(str(main.tool_mode) == "objects" and tabs.current_tab == main.bauen_mod.get_index() and not main.bauen_mod.bau_buttons.is_empty(), "Tab Bauen: Objekt-Werkzeug aktiv, Bauteile da") main.call("_set_tool_mode", "objects") tabs.current_tab = 0 diff --git a/tools/gen_bauteile.gd b/tools/gen_bauteile.gd new file mode 100644 index 0000000..906d0da --- /dev/null +++ b/tools/gen_bauteile.gd @@ -0,0 +1,54 @@ +extends SceneTree +## Erzeugt die Bauteil-Dateien des Bauen-Modus aus src/editor/bau_katalog.gd: +## GLB (Client) · Trimesh-JSON (Server/Client/Editor) · Registry-Fragment (Server) +## +## Aufruf aus dem Editor-Repo (Pfade = Repo-Wurzeln der Geschwister-Klone): +## godot --headless --path . --script res://tools/gen_bauteile.gd -- \ +## --client ../scopedvr-client --server ../scopedvr-match-server +## Danach im Match-Server: python3 tools/merge_object_library.py, und die +## erzeugte library/objects.json in Client + Editor kopieren (tools/sync_objects.sh). +## Idempotent: dieselbe Katalog-Version erzeugt byte-gleiche Dateien. + +func _init() -> void: + var args := OS.get_cmdline_user_args() + var client := _arg(args, "--client") + var server := _arg(args, "--server") + if client.is_empty() or server.is_empty(): + print("FEHLER: --client und --server angeben.") + quit(2) + return + var glb_dir := client.path_join("assets/objekte/military_desert/%s" % BauKatalog.KATEGORIE) + DirAccess.make_dir_recursive_absolute(glb_dir) + var kol_dirs := [ + server.path_join("library/kollisions"), + client.path_join("library/kollisions"), + ProjectSettings.globalize_path("res://library/kollisions"), + ] + for d in kol_dirs: + DirAccess.make_dir_recursive_absolute(d) + var fragment := {"objects": {}} + for oid in BauKatalog.oids(): + var err := BauKatalog.glb_schreiben(oid, glb_dir.path_join(oid + ".glb")) + if err != OK: + print("FEHLER GLB %s: %d" % [oid, err]) + quit(1) + return + var json := JSON.stringify(BauKatalog.kollisions_json(oid)) + for d in kol_dirs: + var f := FileAccess.open((d as String).path_join(oid + ".json"), FileAccess.WRITE) + f.store_string(json) + f.close() + fragment["objects"][oid] = BauKatalog.registry_eintrag(oid) + print("%s: %d Dreiecke" % [oid, BauKatalog.dreiecke(oid)]) + var frag_dir := server.path_join("library/objects.d") + DirAccess.make_dir_recursive_absolute(frag_dir) + var ff := FileAccess.open(frag_dir.path_join("bauteile.json"), FileAccess.WRITE) + ff.store_string(JSON.stringify(fragment, " ", false) + "\n") + ff.close() + print("Fertig: %d Bauteile." % BauKatalog.oids().size()) + quit(0) + + +func _arg(args: PackedStringArray, key: String) -> String: + var i := args.find(key) + return args[i + 1] if i >= 0 and i + 1 < args.size() else "" diff --git a/tools/gen_bauteile.gd.uid b/tools/gen_bauteile.gd.uid new file mode 100644 index 0000000..2a18125 --- /dev/null +++ b/tools/gen_bauteile.gd.uid @@ -0,0 +1 @@ +uid://divoncorxchmo diff --git a/tools/run_tests.sh b/tools/run_tests.sh index b228912..bda5600 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." GODOT=${GODOT:-/opt/data/home/bin/godot4} rc=0 -for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein test_ui_aufraeumen test_ui_layout test_ui_scale; do +for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_build_spawn_mirror test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein test_ui_aufraeumen test_ui_layout test_ui_scale; do "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 code=$? line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)