Bauen-Modus neu: Bauteile als normale Objekte (bau_*), Snap an Kante/Zelle/Ebene, Gebäude = Gruppe
- BauKatalog: 12 Teile (Fundament, Boden, Wände mit Tür/Fenster/Scharte, Brüstung, Pfeiler, Giebel, Dächer, Treppe) auf Basis der CC0-Geometrie des Modular Builder 0.2.0, Wandhöhe 3 m - BuildSnap: Wände auf Zellkanten, Böden in Zellmitte, Höhe aus Ebenen statt Klickhöhe, Ersetzen (Tür in Wand) - Gebäude kopieren/auswählen/löschen im Bauen-Tab, Kopien bekommen eigene Gruppe - Kollision serverkonsistent über Trimesh-Dateien (Türen/Fenster echte Löcher), tools/gen_bauteile.gd erzeugt GLB, Kollision, Registry-Fragment - Alte gebaeude-Sektion wird beim Laden migriert, altes Bauen-Werkzeug entfernt, Resize behält Gruppennamen - Tests: test_bauen (258 Checks), Playtest läuft Treppe/Tür Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
@ -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.
|
||||
|
|
|
|||
62
docs/bauen.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
1
library/kollisions/bau_boden.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_bruestung.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_dach_giebel.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_dach_pyramide.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_fundament.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_giebelwand.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_pfeiler.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_treppe.json
Normal file
1
library/kollisions/bau_wand.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_wand_fenster.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_wand_scharte.json
Normal file
|
|
@ -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]]}
|
||||
1
library/kollisions/bau_wand_tuer.json
Normal file
|
|
@ -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]]}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
BIN
library/thumbs/bau_boden.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
library/thumbs/bau_bruestung.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
library/thumbs/bau_dach_giebel.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
library/thumbs/bau_dach_pyramide.png
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
library/thumbs/bau_fundament.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
library/thumbs/bau_giebelwand.png
Normal file
|
After Width: | Height: | Size: 833 B |
BIN
library/thumbs/bau_pfeiler.png
Normal file
|
After Width: | Height: | Size: 599 B |
BIN
library/thumbs/bau_treppe.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
library/thumbs/bau_wand.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
library/thumbs/bau_wand_fenster.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
library/thumbs/bau_wand_scharte.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
library/thumbs/bau_wand_tuer.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
307
src/editor/bau_katalog.gd
Normal file
|
|
@ -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/<oid>.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
|
||||
1
src/editor/bau_katalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://defuinyvy5j8i
|
||||
204
src/editor/bau_ops.gd
Normal file
|
|
@ -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
|
||||
1
src/editor/bau_ops.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bh4sybddgu3mt
|
||||
|
|
@ -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]))
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://buoucxvl5d35c
|
||||
167
src/editor/build_snap.gd
Normal file
|
|
@ -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
|
||||
1
src/editor/build_snap.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://irnbwhk0eijq
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://vukeqokihvko
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: "<alt> (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 —
|
||||
|
|
|
|||
|
|
@ -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 ----
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -168,13 +168,10 @@ 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):
|
||||
continue
|
||||
var block: Array = doc.get("objects", [])
|
||||
for item in block:
|
||||
if not (item is Dictionary):
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ----
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
238
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,13 +1223,18 @@ 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", "?"))
|
||||
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)])
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
func _pack_gruppen() -> Array:
|
||||
var lib: Dictionary = main.manifest.get("objects", {}) if main != null else {}
|
||||
var out: Array = []
|
||||
for pg in PACK_MODULE:
|
||||
var oids: Array = []
|
||||
for oid in (pg["oids"] as Array):
|
||||
if lib.has(oid):
|
||||
oids.append(oid)
|
||||
if not oids.is_empty():
|
||||
out.append({"label": pg["label"], "oids": oids})
|
||||
return out
|
||||
|
||||
|
||||
func _knopf_raster(oids: Array, bau: bool) -> GridContainer:
|
||||
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:
|
||||
main.call("_set_tool_mode", "objects")
|
||||
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 _on_build_kind_pressed(kind_id: String) -> void:
|
||||
main.call("_on_build_kind_pressed", kind_id)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -377,16 +377,14 @@ 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:
|
||||
for p: Vector3 in _content_points(doc.get("objects", []) as Array):
|
||||
mn = mn.min(p)
|
||||
mx = mx.max(p)
|
||||
count += 1
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 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_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")
|
||||
|
||||
|
||||
## 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")
|
||||
func _test_build_replace_follows() -> void:
|
||||
print("Testgruppe: Tür einsetzen → Zwilling bekommt auch die Tür")
|
||||
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)")
|
||||
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")
|
||||
|
||||
|
||||
# ----------------------------------------------------- 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 -----
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
54
tools/gen_bauteile.gd
Normal file
|
|
@ -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 <Pfad> und --server <Pfad> 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 ""
|
||||
1
tools/gen_bauteile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://divoncorxchmo
|
||||
|
|
@ -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)
|
||||
|
|
|
|||