Compare commits
2 commits
5e32c771fc
...
902181500a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
902181500a | ||
|
|
bcbd416960 |
20 changed files with 476 additions and 30 deletions
|
|
@ -28,6 +28,7 @@ Ein Bauteil ist ein **ganz normales Bibliotheks-Objekt** (`bau_*`, Kategorie
|
||||||
| Dach & Giebel (klein 2 m) | Giebelwand, Satteldach, Pyramidendach |
|
| Dach & Giebel (klein 2 m) | Giebelwand, Satteldach, Pyramidendach |
|
||||||
| Dach & Giebel (mittel 4 m) | Giebelwand 4 m, Satteldach 4×4, Pyramidendach 4×4 (Spitze 2 m) |
|
| Dach & Giebel (mittel 4 m) | Giebelwand 4 m, Satteldach 4×4, Pyramidendach 4×4 (Spitze 2 m) |
|
||||||
| Dach & Giebel (groß 6 m) | Giebelwand 6 m, Satteldach 6×6, Pyramidendach 6×6 (Spitze 3 m) |
|
| Dach & Giebel (groß 6 m) | Giebelwand 6 m, Satteldach 6×6, Pyramidendach 6×6 (Spitze 3 m) |
|
||||||
|
| Dach aus Einzelteilen (45°) | Dachschräge 2×2 m (2 m hoch), Dachecke außen (Grat), Dachecke innen (Kehle), Dachplateau, Dachstirn — massiv, rundum geschlossen; tiefe Kante/Ecke zeigt Richtung Vorschau, R dreht |
|
||||||
| Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) |
|
| Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) |
|
||||||
|
|
||||||
Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab,
|
Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab,
|
||||||
|
|
@ -47,6 +48,11 @@ sondern wie alle Objekte in der Objekt-Bibliothek unten.
|
||||||
Pfeiler zählen von der Unterkante einer Decke/eines Fundaments**, die Decke selbst
|
Pfeiler zählen von der Unterkante einer Decke/eines Fundaments**, die Decke selbst
|
||||||
liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder
|
liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder
|
||||||
ohne Decke darunter stehen die oberen Wände gleich hoch.
|
ohne Decke darunter stehen die oberen Wände gleich hoch.
|
||||||
|
* **Geschoss fest:** Bild↑/Bild↓ (oder Knöpfe im Tab) wählen ein festes Geschoss
|
||||||
|
(Basis = tiefstes Teil in der Nähe, je 3 m). Der Mausstrahl trifft dann die
|
||||||
|
Geschoss-Ebene statt Wand/Boden darunter — Decken und Böden lassen sich mitten im
|
||||||
|
großen Raum setzen. Ganz unten = Automatik.
|
||||||
|
* **Kein Z-Fighting:** Wand-Außenseite steht 1 cm vor der Plattenkante.
|
||||||
* **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene
|
* **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene
|
||||||
Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt.
|
Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt.
|
||||||
|
|
||||||
|
|
|
||||||
1
library/kollisions/bau_dach_ecke_aussen.json
Normal file
1
library/kollisions/bau_dach_ecke_aussen.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":6,"oid":"bau_dach_ecke_aussen","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_ecke_aussen.glb","vertices":[[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,2.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.0,1.0]]}
|
||||||
1
library/kollisions/bau_dach_ecke_innen.json
Normal file
1
library/kollisions/bau_dach_ecke_innen.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":10,"oid":"bau_dach_ecke_innen","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_ecke_innen.glb","vertices":[[-1.0,0.0,-1.0],[1.0,2.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,-1.0],[-1.0,0.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[1.0,2.0,-1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[-1.0,2.0,1.0],[1.0,2.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
library/kollisions/bau_dach_giebel_ende.json
Normal file
1
library/kollisions/bau_dach_giebel_ende.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"aabb_max":[1.0,2.0,0.1],"aabb_min":[-1.0,0.0,-0.1],"dreiecke":8,"oid":"bau_dach_giebel_ende","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_giebel_ende.glb","vertices":[[-1.0,0.0,-0.1],[1.0,0.0,-0.1],[-1.0,2.0,-0.1],[-1.0,0.0,0.1],[-1.0,2.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,2.0,-0.1],[-1.0,2.0,0.1],[-1.0,0.0,-0.1],[-1.0,2.0,0.1],[-1.0,0.0,0.1],[1.0,0.0,-0.1],[1.0,0.0,0.1],[-1.0,2.0,0.1],[1.0,0.0,-0.1],[-1.0,2.0,0.1],[-1.0,2.0,-0.1]]}
|
||||||
1
library/kollisions/bau_dach_plateau.json
Normal file
1
library/kollisions/bau_dach_plateau.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":12,"oid":"bau_dach_plateau","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_plateau.glb","vertices":[[-1.0,2.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,-1.0],[-1.0,2.0,-1.0],[1.0,0.0,-1.0],[-1.0,2.0,-1.0],[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[1.0,2.0,-1.0],[1.0,0.0,1.0],[1.0,2.0,-1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[-1.0,2.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,2.0,1.0],[1.0,2.0,-1.0],[-1.0,2.0,1.0],[-1.0,2.0,-1.0],[1.0,2.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
library/kollisions/bau_dach_schraege.json
Normal file
1
library/kollisions/bau_dach_schraege.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"aabb_max":[1.0,2.0,1.0],"aabb_min":[-1.0,0.0,-1.0],"dreiecke":8,"oid":"bau_dach_schraege","quelle_glb":"res://assets/objekte/military_desert/bauteile/bau_dach_schraege.glb","vertices":[[-1.0,0.0,-1.0],[1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[1.0,2.0,1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,2.0,1.0],[1.0,0.0,1.0],[-1.0,0.0,1.0],[-1.0,2.0,1.0],[1.0,2.0,1.0],[-1.0,0.0,-1.0],[-1.0,2.0,1.0],[-1.0,0.0,1.0],[1.0,0.0,-1.0],[1.0,0.0,1.0],[1.0,2.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]]}
|
||||||
|
|
@ -3491,6 +3491,36 @@
|
||||||
"kategorie": "bauteile",
|
"kategorie": "bauteile",
|
||||||
"name_de": "Brüstung"
|
"name_de": "Brüstung"
|
||||||
},
|
},
|
||||||
|
"bau_dach_ecke_aussen": {
|
||||||
|
"poly": 6,
|
||||||
|
"collision": {
|
||||||
|
"quelle": "kollisions/bau_dach_ecke_aussen.json",
|
||||||
|
"shape": "trimesh",
|
||||||
|
"size": [
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
2.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_ecke_aussen.glb",
|
||||||
|
"kategorie": "bauteile",
|
||||||
|
"name_de": "Dachecke außen"
|
||||||
|
},
|
||||||
|
"bau_dach_ecke_innen": {
|
||||||
|
"poly": 10,
|
||||||
|
"collision": {
|
||||||
|
"quelle": "kollisions/bau_dach_ecke_innen.json",
|
||||||
|
"shape": "trimesh",
|
||||||
|
"size": [
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
2.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_ecke_innen.glb",
|
||||||
|
"kategorie": "bauteile",
|
||||||
|
"name_de": "Dachecke innen"
|
||||||
|
},
|
||||||
"bau_dach_giebel": {
|
"bau_dach_giebel": {
|
||||||
"poly": 48,
|
"poly": 48,
|
||||||
"collision": {
|
"collision": {
|
||||||
|
|
@ -3536,6 +3566,36 @@
|
||||||
"kategorie": "bauteile",
|
"kategorie": "bauteile",
|
||||||
"name_de": "Satteldach 6×6 m"
|
"name_de": "Satteldach 6×6 m"
|
||||||
},
|
},
|
||||||
|
"bau_dach_giebel_ende": {
|
||||||
|
"poly": 8,
|
||||||
|
"collision": {
|
||||||
|
"quelle": "kollisions/bau_dach_giebel_ende.json",
|
||||||
|
"shape": "trimesh",
|
||||||
|
"size": [
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
0.2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_giebel_ende.glb",
|
||||||
|
"kategorie": "bauteile",
|
||||||
|
"name_de": "Dachstirn 45°"
|
||||||
|
},
|
||||||
|
"bau_dach_plateau": {
|
||||||
|
"poly": 12,
|
||||||
|
"collision": {
|
||||||
|
"quelle": "kollisions/bau_dach_plateau.json",
|
||||||
|
"shape": "trimesh",
|
||||||
|
"size": [
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
2.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_plateau.glb",
|
||||||
|
"kategorie": "bauteile",
|
||||||
|
"name_de": "Dachplateau"
|
||||||
|
},
|
||||||
"bau_dach_pyramide": {
|
"bau_dach_pyramide": {
|
||||||
"poly": 64,
|
"poly": 64,
|
||||||
"collision": {
|
"collision": {
|
||||||
|
|
@ -3581,6 +3641,21 @@
|
||||||
"kategorie": "bauteile",
|
"kategorie": "bauteile",
|
||||||
"name_de": "Pyramidendach 6×6 m"
|
"name_de": "Pyramidendach 6×6 m"
|
||||||
},
|
},
|
||||||
|
"bau_dach_schraege": {
|
||||||
|
"poly": 8,
|
||||||
|
"collision": {
|
||||||
|
"quelle": "kollisions/bau_dach_schraege.json",
|
||||||
|
"shape": "trimesh",
|
||||||
|
"size": [
|
||||||
|
2.0,
|
||||||
|
2.0,
|
||||||
|
2.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"glb": "res://assets/objekte/military_desert/bauteile/bau_dach_schraege.glb",
|
||||||
|
"kategorie": "bauteile",
|
||||||
|
"name_de": "Dachschräge 45°"
|
||||||
|
},
|
||||||
"bau_fundament": {
|
"bau_fundament": {
|
||||||
"poly": 24,
|
"poly": 24,
|
||||||
"collision": {
|
"collision": {
|
||||||
|
|
|
||||||
BIN
library/thumbs/bau_dach_ecke_aussen.png
Normal file
BIN
library/thumbs/bau_dach_ecke_aussen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
library/thumbs/bau_dach_ecke_innen.png
Normal file
BIN
library/thumbs/bau_dach_ecke_innen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
library/thumbs/bau_dach_giebel_ende.png
Normal file
BIN
library/thumbs/bau_dach_giebel_ende.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 867 B |
BIN
library/thumbs/bau_dach_plateau.png
Normal file
BIN
library/thumbs/bau_dach_plateau.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
library/thumbs/bau_dach_schraege.png
Normal file
BIN
library/thumbs/bau_dach_schraege.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -52,6 +52,8 @@ const GRUPPEN := [
|
||||||
{"key": "dach", "label": "Dach & Giebel (klein 2 m)", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]},
|
{"key": "dach", "label": "Dach & Giebel (klein 2 m)", "oids": ["bau_giebelwand", "bau_dach_giebel", "bau_dach_pyramide"]},
|
||||||
{"key": "dach_mittel", "label": "Dach & Giebel (mittel 4 m)", "oids": ["bau_giebelwand_4", "bau_dach_giebel_4", "bau_dach_pyramide_4"]},
|
{"key": "dach_mittel", "label": "Dach & Giebel (mittel 4 m)", "oids": ["bau_giebelwand_4", "bau_dach_giebel_4", "bau_dach_pyramide_4"]},
|
||||||
{"key": "dach_gross", "label": "Dach & Giebel (groß 6 m)", "oids": ["bau_giebelwand_6", "bau_dach_giebel_6", "bau_dach_pyramide_6"]},
|
{"key": "dach_gross", "label": "Dach & Giebel (groß 6 m)", "oids": ["bau_giebelwand_6", "bau_dach_giebel_6", "bau_dach_pyramide_6"]},
|
||||||
|
{"key": "dach_teile", "label": "Dach aus Einzelteilen (45°)", "oids": ["bau_dach_schraege", "bau_dach_ecke_aussen",
|
||||||
|
"bau_dach_ecke_innen", "bau_dach_plateau", "bau_dach_giebel_ende"]},
|
||||||
{"key": "treppe", "label": "Treppe", "oids": ["bau_treppe"]},
|
{"key": "treppe", "label": "Treppe", "oids": ["bau_treppe"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -139,6 +141,29 @@ const KINDS := {
|
||||||
"label": "Pyramidendach 6×6 m", "info": "6×6 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
|
"label": "Pyramidendach 6×6 m", "info": "6×6 m, Spitze in der Mitte", "snap": "zelle", "traeger": false,
|
||||||
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18),
|
"typ": T_DACH_PYRAMIDE, "punkte": QUADRAT_6, "rise": 3.0, "farbe": Color(0.40, 0.25, 0.18),
|
||||||
},
|
},
|
||||||
|
# Dach aus Einzelteilen: alles 2×2-Zellen mit 45° (2 m Lauf = 2 m Höhe), massiv
|
||||||
|
# (rundum geschlossen, keine Lücke von innen). "profil" → _dach_mesh(). Tiefe
|
||||||
|
# Kante = -Z lokal (R dreht), Ecken: tiefer Punkt bei (-X,-Z).
|
||||||
|
"bau_dach_schraege": {
|
||||||
|
"label": "Dachschräge 45°", "info": "2×2 m, 2 m hoch — eine Seite des Dachs; die tiefe Kante zeigt in Richtung der Vorschau (R dreht)",
|
||||||
|
"snap": "zelle", "traeger": false, "profil": "schraege", "farbe": Color(0.40, 0.25, 0.18),
|
||||||
|
},
|
||||||
|
"bau_dach_ecke_aussen": {
|
||||||
|
"label": "Dachecke außen", "info": "Walm-Ecke (Grat): zwei Schrägen treffen sich an einer Hausecke, die tiefe Ecke zeigt nach außen",
|
||||||
|
"snap": "zelle", "traeger": false, "profil": "ecke_aussen", "farbe": Color(0.40, 0.25, 0.18),
|
||||||
|
},
|
||||||
|
"bau_dach_ecke_innen": {
|
||||||
|
"label": "Dachecke innen", "info": "Kehle: zwei Schrägen laufen in einer einspringenden Ecke (L-/T-Dach) zusammen",
|
||||||
|
"snap": "zelle", "traeger": false, "profil": "ecke_innen", "farbe": Color(0.40, 0.25, 0.18),
|
||||||
|
},
|
||||||
|
"bau_dach_plateau": {
|
||||||
|
"label": "Dachplateau", "info": "2×2 m Block, 2 m hoch, oben flach — Mittelstück zwischen zwei Schrägen (breite Häuser), Schornstein-Sockel",
|
||||||
|
"snap": "zelle", "traeger": true, "profil": "plateau", "farbe": Color(0.40, 0.25, 0.18),
|
||||||
|
},
|
||||||
|
"bau_dach_giebel_ende": {
|
||||||
|
"label": "Dachstirn 45°", "info": "Senkrechte Dreieck-Stirn (2 m breit, 2 m hoch) — Giebel-Abschluss unter einer Schräge, wenn das Dach an der Seite offen bleibt",
|
||||||
|
"snap": "kante", "traeger": false, "profil": "stirn", "farbe": Color(0.62, 0.53, 0.38),
|
||||||
|
},
|
||||||
"bau_treppe": {
|
"bau_treppe": {
|
||||||
"label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber",
|
"label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber",
|
||||||
"snap": "treppe", "traeger": false,
|
"snap": "treppe", "traeger": false,
|
||||||
|
|
@ -190,7 +215,13 @@ static func mesh(oid: String) -> ArrayMesh:
|
||||||
var kind: Dictionary = KINDS.get(oid, {})
|
var kind: Dictionary = KINDS.get(oid, {})
|
||||||
if kind.is_empty():
|
if kind.is_empty():
|
||||||
return null
|
return null
|
||||||
var roh: ArrayMesh = _box_mesh(kind["box"]) if kind.has("box") else _addon_mesh(kind)
|
var roh: ArrayMesh
|
||||||
|
if kind.has("box"):
|
||||||
|
roh = _box_mesh(kind["box"])
|
||||||
|
elif kind.has("profil"):
|
||||||
|
roh = _dach_mesh(str(kind["profil"]))
|
||||||
|
else:
|
||||||
|
roh = _addon_mesh(kind)
|
||||||
var fertig := _normalisieren(roh, kind["farbe"] as Color)
|
var fertig := _normalisieren(roh, kind["farbe"] as Color)
|
||||||
_mesh_cache[oid] = fertig
|
_mesh_cache[oid] = fertig
|
||||||
return fertig
|
return fertig
|
||||||
|
|
@ -204,6 +235,79 @@ static func _box_mesh(size: Vector3) -> ArrayMesh:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Massive Dach-Teile aus Dreiecken (Zelle 2×2 m, lokal x,z ∈ [-1,1], 45°).
|
||||||
|
## Oberfläche h(x,z): Schräge = z+1, Ecke außen = min(x,z)+1, Ecke innen = max(x,z)+1,
|
||||||
|
## Plateau = 2. Dach-Stirn: dünnes Dreieck-Prisma.
|
||||||
|
static func _dach_mesh(profil: String) -> ArrayMesh:
|
||||||
|
var tris: Array = [] # je Dreieck [a, b, c] als Vector3
|
||||||
|
var V := func(x: float, y: float, z: float) -> Vector3: return Vector3(x, y, z)
|
||||||
|
var quad := func(a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void:
|
||||||
|
tris.append([a, b, c])
|
||||||
|
tris.append([a, c, d])
|
||||||
|
var tri := func(a: Vector3, b: Vector3, c: Vector3) -> void:
|
||||||
|
tris.append([a, b, c])
|
||||||
|
var mitte := Vector3(0.0, 0.7, 0.0)
|
||||||
|
match profil:
|
||||||
|
"schraege":
|
||||||
|
quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 2, 1), V.call(-1, 2, 1)) # Hang
|
||||||
|
quad.call(V.call(-1, 0, 1), V.call(1, 0, 1), V.call(1, 2, 1), V.call(-1, 2, 1)) # Rückwand
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(-1, 0, 1), V.call(-1, 2, 1)) # Stirn links
|
||||||
|
tri.call(V.call(1, 0, -1), V.call(1, 0, 1), V.call(1, 2, 1)) # Stirn rechts
|
||||||
|
quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 0, 1), V.call(-1, 0, 1)) # Boden
|
||||||
|
"ecke_aussen":
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 2, 1))
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(1, 2, 1), V.call(-1, 0, 1))
|
||||||
|
tri.call(V.call(1, 0, -1), V.call(1, 0, 1), V.call(1, 2, 1))
|
||||||
|
tri.call(V.call(-1, 0, 1), V.call(1, 0, 1), V.call(1, 2, 1))
|
||||||
|
quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 0, 1), V.call(-1, 0, 1))
|
||||||
|
"ecke_innen":
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(1, 2, -1), V.call(1, 2, 1))
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(1, 2, 1), V.call(-1, 2, 1))
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 2, -1))
|
||||||
|
tri.call(V.call(-1, 0, -1), V.call(-1, 2, 1), V.call(-1, 0, 1))
|
||||||
|
quad.call(V.call(1, 0, -1), V.call(1, 0, 1), V.call(1, 2, 1), V.call(1, 2, -1))
|
||||||
|
quad.call(V.call(-1, 0, 1), V.call(1, 0, 1), V.call(1, 2, 1), V.call(-1, 2, 1))
|
||||||
|
quad.call(V.call(-1, 0, -1), V.call(1, 0, -1), V.call(1, 0, 1), V.call(-1, 0, 1))
|
||||||
|
"plateau":
|
||||||
|
var bm := BoxMesh.new()
|
||||||
|
bm.size = Vector3(2.0, 2.0, 2.0)
|
||||||
|
var arr := bm.get_mesh_arrays()
|
||||||
|
var out := ArrayMesh.new()
|
||||||
|
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr)
|
||||||
|
return out
|
||||||
|
"stirn":
|
||||||
|
# Dünnes Dreieck-Prisma (Kante entlang X, 2 m breit, 2 m hoch, 0,2 m dick)
|
||||||
|
var d := 0.1
|
||||||
|
tri.call(V.call(-1, 0, -d), V.call(1, 0, -d), V.call(-1, 2, -d))
|
||||||
|
tri.call(V.call(-1, 0, d), V.call(-1, 2, d), V.call(1, 0, d))
|
||||||
|
quad.call(V.call(-1, 0, -d), V.call(-1, 0, d), V.call(1, 0, d), V.call(1, 0, -d))
|
||||||
|
quad.call(V.call(-1, 0, -d), V.call(-1, 2, -d), V.call(-1, 2, d), V.call(-1, 0, d))
|
||||||
|
quad.call(V.call(1, 0, -d), V.call(1, 0, d), V.call(-1, 2, d), V.call(-1, 2, -d))
|
||||||
|
mitte = Vector3(-0.33, 0.67, 0.0)
|
||||||
|
var pos := PackedVector3Array()
|
||||||
|
var nrm := PackedVector3Array()
|
||||||
|
for t in tris:
|
||||||
|
var a: Vector3 = t[0]
|
||||||
|
var b: Vector3 = t[1]
|
||||||
|
var c: Vector3 = t[2]
|
||||||
|
var n := (b - a).cross(c - a).normalized()
|
||||||
|
# nach außen drehen: Fläche muss vom Körper-Mittelpunkt wegzeigen
|
||||||
|
if n.dot((a + b + c) / 3.0 - mitte) < 0.0:
|
||||||
|
var tmp := b
|
||||||
|
b = c
|
||||||
|
c = tmp
|
||||||
|
n = -n
|
||||||
|
pos.append_array(PackedVector3Array([a, b, c]))
|
||||||
|
nrm.append_array(PackedVector3Array([n, n, n]))
|
||||||
|
var arrays := []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = pos
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = nrm
|
||||||
|
var m := ArrayMesh.new()
|
||||||
|
m.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
static func _addon_mesh(kind: Dictionary) -> ArrayMesh:
|
static func _addon_mesh(kind: Dictionary) -> ArrayMesh:
|
||||||
var geo := Node3D.new()
|
var geo := Node3D.new()
|
||||||
geo.set_script(load(GEO_SCRIPT))
|
geo.set_script(load(GEO_SCRIPT))
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ class_name BuildSnap
|
||||||
const TOL_M := 0.35 # so weit darf eine Ebene ÜBER dem Klickpunkt liegen
|
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 SHRINK_M := 0.05 # Grundflächen-Schnitt: minimale Überlappung ignorieren
|
||||||
const SAME_M := 0.08 # Toleranz für "gleiche Stelle"
|
const SAME_M := 0.08 # Toleranz für "gleiche Stelle"
|
||||||
|
const UEBERSTAND_M := 0.01 # Wand-Außenseite steht 1 cm vor der Plattenkante: keine deckungsgleichen
|
||||||
|
# Flächen (Z-Fighting/Flackern an Boden↔Wand)
|
||||||
|
|
||||||
|
|
||||||
## Ergebnis-Wörterbuch:
|
## Ergebnis-Wörterbuch:
|
||||||
|
|
@ -36,8 +38,12 @@ const SAME_M := 0.08 # Toleranz für "gleiche Stelle"
|
||||||
## gelaende: Callable(x: float, z: float) -> float
|
## gelaende: Callable(x: float, z: float) -> float
|
||||||
## dreh: Nutzerdrehung in Grad (wird auf 90° gerastet)
|
## dreh: Nutzerdrehung in Grad (wird auf 90° gerastet)
|
||||||
## kamera_dir: Blickrichtung des Klickstrahls (für Zell-Wahl an Wandkanten), optional
|
## kamera_dir: Blickrichtung des Klickstrahls (für Zell-Wahl an Wandkanten), optional
|
||||||
|
## ebene_fix: feste Geschosshöhe (Geschoss-Wahl im Bauen-Tab); NAN = automatisch.
|
||||||
|
## Dann hängt die Höhe NICHT mehr an Wänden/Böden darunter — Decken und Böden
|
||||||
|
## lassen sich auch mitten im großen Raum setzen.
|
||||||
static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
||||||
gelaende: Callable, hit_ist_objekt := false, kamera_dir := Vector3.ZERO) -> Dictionary:
|
gelaende: Callable, hit_ist_objekt := false, kamera_dir := Vector3.ZERO,
|
||||||
|
ebene_fix := NAN) -> Dictionary:
|
||||||
var art := BauKatalog.snap_art(oid)
|
var art := BauKatalog.snap_art(oid)
|
||||||
var gr := BauKatalog.groesse(oid)
|
var gr := BauKatalog.groesse(oid)
|
||||||
var q := int(roundf(dreh / 90.0))
|
var q := int(roundf(dreh / 90.0))
|
||||||
|
|
@ -97,11 +103,11 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
||||||
var v_mitte := Vector2(roundf(fx) * r, _raster_mitte(p.y, laenge)) # Kante entlang Z
|
var v_mitte := Vector2(roundf(fx) * r, _raster_mitte(p.y, laenge)) # Kante entlang Z
|
||||||
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte):
|
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte):
|
||||||
mitte = h_mitte
|
mitte = h_mitte
|
||||||
mitte.y += halbe_dicke if p.y >= h_mitte.y else -halbe_dicke
|
mitte.y += (halbe_dicke - UEBERSTAND_M) * (1.0 if p.y >= h_mitte.y else -1.0)
|
||||||
rot = 0.0
|
rot = 0.0
|
||||||
else:
|
else:
|
||||||
mitte = v_mitte
|
mitte = v_mitte
|
||||||
mitte.x += halbe_dicke if p.x >= v_mitte.x else -halbe_dicke
|
mitte.x += (halbe_dicke - UEBERSTAND_M) * (1.0 if p.x >= v_mitte.x else -1.0)
|
||||||
rot = 90.0
|
rot = 90.0
|
||||||
rot += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile)
|
rot += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile)
|
||||||
rot = fposmod(rot, 360.0)
|
rot = fposmod(rot, 360.0)
|
||||||
|
|
@ -133,7 +139,7 @@ static func berechne(oid: String, hit: Vector3, dreh: float, teile: Array,
|
||||||
ebenen.append((t["pos"] as Vector3).y)
|
ebenen.append((t["pos"] as Vector3).y)
|
||||||
else:
|
else:
|
||||||
ebenen.append(top)
|
ebenen.append(top)
|
||||||
var ebene := _waehle_ebene(ebenen, hit.y)
|
var ebene := ebene_fix if not is_nan(ebene_fix) else _waehle_ebene(ebenen, hit.y)
|
||||||
|
|
||||||
# Platz belegt? Gleiche Art + gleiche Stelle + gleiche Ebene → ersetzen/ablehnen.
|
# Platz belegt? Gleiche Art + gleiche Stelle + gleiche Ebene → ersetzen/ablehnen.
|
||||||
var ersetze := -1
|
var ersetze := -1
|
||||||
|
|
@ -234,3 +240,17 @@ static func sammle_teile(doc: Dictionary) -> Array:
|
||||||
"rot": float(SvmapIO.objekt_rotation(o as Dictionary).y),
|
"rot": float(SvmapIO.objekt_rotation(o as Dictionary).y),
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Höhe des untersten Geschosses (= Fußpunkt der tiefsten Platte/Wand im Umkreis von
|
||||||
|
## `radius` um `p`), sonst das Gelände bei `p`. Geschoss k liegt bei Basis + k·WAND_H.
|
||||||
|
static func geschoss_basis(teile: Array, p: Vector2, gelaende: Callable, radius := 40.0) -> float:
|
||||||
|
var tiefste := INF
|
||||||
|
for t in teile:
|
||||||
|
var tp: Vector3 = t["pos"]
|
||||||
|
if Vector2(tp.x, tp.z).distance_to(p) > radius:
|
||||||
|
continue
|
||||||
|
var oid := str(t["oid"])
|
||||||
|
if BauKatalog.ist_platte(oid) or BauKatalog.snap_art(oid) == "kante":
|
||||||
|
tiefste = minf(tiefste, tp.y)
|
||||||
|
return tiefste if tiefste < INF else float(gelaende.call(p.x, p.y))
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ var build_group_id := -1 # laufendes Gebäude; -1 = nächstes Teil be
|
||||||
var _build_last: Dictionary = {} # letztes BuildSnap-Ergebnis (pos/rot/ersetze/ok/grund)
|
var _build_last: Dictionary = {} # letztes BuildSnap-Ergebnis (pos/rot/ersetze/ok/grund)
|
||||||
var _build_hit: Dictionary = {} # letzter Treffer {punkt, obj, dir}
|
var _build_hit: Dictionary = {} # letzter Treffer {punkt, obj, dir}
|
||||||
var _build_hint := "" # letzte Statuszeile des Ghosts (kein Spam)
|
var _build_hint := "" # letzte Statuszeile des Ghosts (kein Spam)
|
||||||
|
var build_geschoss := -1 # -1 = Höhe automatisch, sonst festes Geschoss (0 = Boden, 1 = erste Decke …)
|
||||||
|
const BAU_GESCHOSS_MAX := 8
|
||||||
|
|
||||||
# Terrain-Höhen-Cache (Karte t_4092e3a0): der Objekt-Cursor trifft das GELÄNDE
|
# Terrain-Höhen-Cache (Karte t_4092e3a0): der Objekt-Cursor trifft das GELÄNDE
|
||||||
# (TerrainField.raycast, wie Terrain-/Bauen-/Spawn-Werkzeug), nicht mehr nur
|
# (TerrainField.raycast, wie Terrain-/Bauen-/Spawn-Werkzeug), nicht mehr nur
|
||||||
|
|
@ -252,6 +254,7 @@ func cancel_placement() -> void:
|
||||||
_ghost_active = false
|
_ghost_active = false
|
||||||
_build_last = {}
|
_build_last = {}
|
||||||
_build_hit = {}
|
_build_hit = {}
|
||||||
|
build_geschoss = -1
|
||||||
main.call("_highlight_library", "")
|
main.call("_highlight_library", "")
|
||||||
main.call("_base_tool_status")
|
main.call("_base_tool_status")
|
||||||
|
|
||||||
|
|
@ -449,6 +452,8 @@ func _build_ray_hit(viewport_pos: Vector2) -> Dictionary:
|
||||||
var origin := camera.project_ray_origin(viewport_pos)
|
var origin := camera.project_ray_origin(viewport_pos)
|
||||||
var dir := camera.project_ray_normal(viewport_pos)
|
var dir := camera.project_ray_normal(viewport_pos)
|
||||||
var ground: Variant = _ground_point(viewport_pos)
|
var ground: Variant = _ground_point(viewport_pos)
|
||||||
|
if build_geschoss >= 0:
|
||||||
|
return _build_ray_geschoss(origin, dir, ground)
|
||||||
var best_t := INF
|
var best_t := INF
|
||||||
var out := {}
|
var out := {}
|
||||||
if ground != null:
|
if ground != null:
|
||||||
|
|
@ -476,6 +481,55 @@ func _build_ray_hit(viewport_pos: Vector2) -> Dictionary:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Feste Geschosshöhe: der Strahl trifft die waagerechte Ebene dieses Geschosses,
|
||||||
|
## egal ob darunter Wand, Boden oder nur Gelände ist (Decke mitten im Raum).
|
||||||
|
func _build_ray_geschoss(origin: Vector3, dir: Vector3, ground: Variant) -> Dictionary:
|
||||||
|
var xz := Vector2(origin.x, origin.z)
|
||||||
|
if ground != null:
|
||||||
|
xz = Vector2((ground as Vector3).x, (ground as Vector3).z)
|
||||||
|
var y := _bau_geschoss_hoehe(xz)
|
||||||
|
if absf(dir.y) < 0.001:
|
||||||
|
return {}
|
||||||
|
var t := (y - origin.y) / dir.y
|
||||||
|
if t <= 0.0:
|
||||||
|
return {}
|
||||||
|
return {"punkt": origin + dir * t, "obj": -1, "dir": dir, "ebene": y}
|
||||||
|
|
||||||
|
|
||||||
|
func _bau_gelaende_fn() -> Callable:
|
||||||
|
return 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))
|
||||||
|
|
||||||
|
|
||||||
|
func _bau_geschoss_hoehe(xz: Vector2) -> float:
|
||||||
|
var basis := BuildSnap.geschoss_basis(BuildSnap.sammle_teile(main.doc), xz, _bau_gelaende_fn())
|
||||||
|
return basis + float(build_geschoss) * BauKatalog.WAND_H
|
||||||
|
|
||||||
|
|
||||||
|
## Bild↑/Bild↓ bzw. Knöpfe im Bauen-Tab: Geschoss wechseln (0 → zurück auf Automatik).
|
||||||
|
func bau_geschoss_aendern(delta: int) -> void:
|
||||||
|
var neu := build_geschoss
|
||||||
|
if delta > 0:
|
||||||
|
neu = 1 if build_geschoss < 0 else mini(build_geschoss + 1, BAU_GESCHOSS_MAX)
|
||||||
|
elif build_geschoss >= 0:
|
||||||
|
neu = build_geschoss - 1 # 0 → -1 (Automatik)
|
||||||
|
build_geschoss = neu
|
||||||
|
_build_hint = ""
|
||||||
|
if placing and not _build_hit.is_empty():
|
||||||
|
_bau_ghost_neu()
|
||||||
|
main.call("_set_status", bau_geschoss_text())
|
||||||
|
main.call("_refresh_bau_info")
|
||||||
|
|
||||||
|
|
||||||
|
func bau_geschoss_text() -> String:
|
||||||
|
if build_geschoss < 0:
|
||||||
|
return "Höhe: automatisch (Gelände bzw. Oberkante des Teils darunter). Bild↑ wählt ein festes Geschoss — dann sitzen Böden/Decken überall im Raum."
|
||||||
|
return "Höhe: festes Geschoss %d (%s). Bild↑/Bild↓ wechselt, bis „automatisch“ erreicht ist." % [
|
||||||
|
build_geschoss, "Erdgeschoss-Boden" if build_geschoss == 0 else "%d × %d m über dem Boden" % [build_geschoss, int(BauKatalog.WAND_H)]]
|
||||||
|
|
||||||
|
|
||||||
func _bau_ghost_update(viewport_pos: Vector2) -> void:
|
func _bau_ghost_update(viewport_pos: Vector2) -> void:
|
||||||
_build_hit = _build_ray_hit(viewport_pos)
|
_build_hit = _build_ray_hit(viewport_pos)
|
||||||
if _build_hit.is_empty():
|
if _build_hit.is_empty():
|
||||||
|
|
@ -490,13 +544,10 @@ func _bau_ghost_update(viewport_pos: Vector2) -> void:
|
||||||
func _bau_snap() -> Dictionary:
|
func _bau_snap() -> Dictionary:
|
||||||
if _build_hit.is_empty():
|
if _build_hit.is_empty():
|
||||||
return {}
|
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,
|
return BuildSnap.berechne(place_oid, _build_hit["punkt"] as Vector3, place_rot_deg,
|
||||||
BuildSnap.sammle_teile(main.doc), gelaende,
|
BuildSnap.sammle_teile(main.doc), _bau_gelaende_fn(),
|
||||||
int(_build_hit.get("obj", -1)) >= 0, _build_hit.get("dir", Vector3.ZERO) as Vector3)
|
int(_build_hit.get("obj", -1)) >= 0, _build_hit.get("dir", Vector3.ZERO) as Vector3,
|
||||||
|
float(_build_hit.get("ebene", NAN)))
|
||||||
|
|
||||||
|
|
||||||
func _bau_ghost_neu() -> void:
|
func _bau_ghost_neu() -> void:
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ var toolbar_mod # src/ui/panels/toolbar.gd
|
||||||
var status_mod # src/ui/panels/status_bar.gd
|
var status_mod # src/ui/panels/status_bar.gd
|
||||||
var selection_mod # src/ui/panels/selection_panel.gd (nur Logik, kein Tab mehr)
|
var selection_mod # src/ui/panels/selection_panel.gd (nur Logik, kein Tab mehr)
|
||||||
var settings_mod # src/ui/panels/settings_panel.gd (im Einstellungen-Fenster)
|
var settings_mod # src/ui/panels/settings_panel.gd (im Einstellungen-Fenster)
|
||||||
|
var drop_overlay: Control # Ablege-Fläche über der 3D-Ansicht (nur während Drag aktiv)
|
||||||
var settings_dialog: AcceptDialog # Fenster „Einstellungen“ (Knopf in der Kopfleiste)
|
var settings_dialog: AcceptDialog # Fenster „Einstellungen“ (Knopf in der Kopfleiste)
|
||||||
var sym_mod # src/ui/panels/symmetry_panel.gd (im Map-Tab)
|
var sym_mod # src/ui/panels/symmetry_panel.gd (im Map-Tab)
|
||||||
var map_mod # src/ui/panels/map_panel.gd
|
var map_mod # src/ui/panels/map_panel.gd
|
||||||
|
|
@ -485,7 +486,12 @@ func _open_settings() -> void:
|
||||||
if settings_dialog == null:
|
if settings_dialog == null:
|
||||||
return
|
return
|
||||||
settings_mod.call("refresh")
|
settings_mod.call("refresh")
|
||||||
settings_dialog.popup_centered(UIScale.v2i(560, 520))
|
# So groß, dass alles ohne Scrollen sichtbar ist (Inhalt + Titel/Knopf), begrenzt aufs Fenster.
|
||||||
|
var inhalt: Vector2 = settings_mod.get_combined_minimum_size()
|
||||||
|
var fenster := Vector2(get_window().size)
|
||||||
|
var breite := clampf(maxf(inhalt.x + UIScale.px(60), UIScale.px(640)), UIScale.px(640), fenster.x * 0.95)
|
||||||
|
var hoehe := clampf(inhalt.y + UIScale.px(110), UIScale.px(520), fenster.y * 0.95)
|
||||||
|
settings_dialog.popup_centered(Vector2i(int(breite), int(hoehe)))
|
||||||
|
|
||||||
|
|
||||||
func _build_dialogs_and_overlays() -> void:
|
func _build_dialogs_and_overlays() -> void:
|
||||||
|
|
@ -695,6 +701,18 @@ func _build_center_viewport() -> SubViewportContainer:
|
||||||
container.add_child(viewport_toolbar)
|
container.add_child(viewport_toolbar)
|
||||||
viewport_toolbar.setup(self)
|
viewport_toolbar.setup(self)
|
||||||
|
|
||||||
|
# Ablege-Fläche für Bibliotheks-Kacheln: Godot ruft can_drop/drop NICHT auf einem
|
||||||
|
# SubViewportContainer auf (Test-Befund 25.10.: Ziehen startete, Loslassen setzte
|
||||||
|
# nichts — echter Maus-Drag, tests/probe_drag_echt.gd). Eine durchsichtige Fläche
|
||||||
|
# darüber nimmt den Drop an; sie ist nur WÄHREND eines Ziehens aktiv (sonst
|
||||||
|
# bleibt die Maus für Kamera/Auswahl frei).
|
||||||
|
drop_overlay = Control.new()
|
||||||
|
drop_overlay.name = "AblegeFlaeche"
|
||||||
|
drop_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
drop_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
drop_overlay.set_drag_forwarding(Callable(), _viewport_can_drop, _viewport_drop)
|
||||||
|
container.add_child(drop_overlay)
|
||||||
|
|
||||||
# Bedien-Hinweis im Viewport (Overlay)
|
# Bedien-Hinweis im Viewport (Overlay)
|
||||||
help_label = Label.new()
|
help_label = Label.new()
|
||||||
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
|
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
|
||||||
|
|
@ -885,6 +903,11 @@ func _quit_editor() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _notification(what: int) -> void:
|
func _notification(what: int) -> void:
|
||||||
|
if drop_overlay != null and is_instance_valid(drop_overlay):
|
||||||
|
if what == NOTIFICATION_DRAG_BEGIN:
|
||||||
|
drop_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
elif what == NOTIFICATION_DRAG_END:
|
||||||
|
drop_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||||
_quit_editor()
|
_quit_editor()
|
||||||
|
|
||||||
|
|
@ -1516,6 +1539,11 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
_test_start_apply(float(tp.get("x")), float(tp.get("z")), str(tp.get("hinweis")))
|
_test_start_apply(float(tp.get("x")), float(tp.get("z")), str(tp.get("hinweis")))
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
if (key == KEY_PAGEUP or key == KEY_PAGEDOWN) and not ctrl and interaction.placing \
|
||||||
|
and BauKatalog.ist_bau(interaction.place_oid):
|
||||||
|
interaction.bau_geschoss_aendern(1 if key == KEY_PAGEUP else -1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
match key:
|
match key:
|
||||||
KEY_T:
|
KEY_T:
|
||||||
# Karte t_96e3c4ee: T schaltet das Test-Start-Werkzeug um (Editor).
|
# Karte t_96e3c4ee: T schaltet das Test-Start-Werkzeug um (Editor).
|
||||||
|
|
|
||||||
|
|
@ -636,10 +636,7 @@ func _tile_drag_data(_at_position: Vector2, oid: String, btn: Button) -> Variant
|
||||||
lbl.text = LibraryBrowser.entry_label(oid, (main.manifest.get("objects", {}) as Dictionary).get(oid, {}))
|
lbl.text = LibraryBrowser.entry_label(oid, (main.manifest.get("objects", {}) as Dictionary).get(oid, {}))
|
||||||
box.add_child(lbl)
|
box.add_child(lbl)
|
||||||
prev.modulate.a = 0.85
|
prev.modulate.a = 0.85
|
||||||
if btn.get_viewport().gui_is_dragging():
|
btn.set_drag_preview(prev) # gehört in den Drag-Start: gui_is_dragging() ist hier noch false
|
||||||
btn.set_drag_preview(prev)
|
|
||||||
else:
|
|
||||||
prev.free()
|
|
||||||
return {"svr_oid": oid}
|
return {"svr_oid": oid}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ extends "res://src/ui/panels/editor_panel.gd"
|
||||||
|
|
||||||
var bau_buttons: Dictionary = {} # oid → Button
|
var bau_buttons: Dictionary = {} # oid → Button
|
||||||
var info_label: Label
|
var info_label: Label
|
||||||
|
var geschoss_label: Label
|
||||||
var _group: ButtonGroup
|
var _group: ButtonGroup
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,11 +23,25 @@ func panel_title() -> String:
|
||||||
func build() -> void:
|
func build() -> void:
|
||||||
_group = ButtonGroup.new()
|
_group = ButtonGroup.new()
|
||||||
_group.allow_unpress = true
|
_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."))
|
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 (oder Bild↑/Bild↓: festes Geschoss, dann sitzen Decken mitten im Raum), Klick auf die Seite ersetzt das Teil (z. B. Wand → Tür). R dreht, Esc beendet."))
|
||||||
for gruppe in BauKatalog.GRUPPEN:
|
for gruppe in BauKatalog.GRUPPEN:
|
||||||
add_child(_field_label(str(gruppe["label"])))
|
add_child(_field_label(str(gruppe["label"])))
|
||||||
add_child(_knopf_raster(gruppe["oids"] as Array))
|
add_child(_knopf_raster(gruppe["oids"] as Array))
|
||||||
add_child(_separator())
|
add_child(_separator())
|
||||||
|
add_child(_header_label("Höhe / Geschoss"))
|
||||||
|
geschoss_label = _muted_label(main.interaction.call("bau_geschoss_text"))
|
||||||
|
add_child(geschoss_label)
|
||||||
|
var hoehe := HBoxContainer.new()
|
||||||
|
hoehe.add_theme_constant_override("separation", UIScale.px(8))
|
||||||
|
for spec in [["▲ Höher (Bild↑)", 1], ["▼ Tiefer (Bild↓)", -1]]:
|
||||||
|
var hb := _aktion(str(spec[0]), "Festes Geschoss wählen: Böden und Decken sitzen dann überall im Raum auf dieser Höhe (je 3 m). Ganz unten = automatisch.",
|
||||||
|
func() -> void:
|
||||||
|
main.interaction.call("bau_geschoss_aendern", int(spec[1]))
|
||||||
|
geschoss_label.text = main.interaction.call("bau_geschoss_text"))
|
||||||
|
hb.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hoehe.add_child(hb)
|
||||||
|
add_child(hoehe)
|
||||||
|
add_child(_separator())
|
||||||
add_child(_header_label("Gebäude"))
|
add_child(_header_label("Gebäude"))
|
||||||
info_label = _muted_label("")
|
info_label = _muted_label("")
|
||||||
add_child(info_label)
|
add_child(info_label)
|
||||||
|
|
@ -92,6 +107,8 @@ func refresh_info() -> void:
|
||||||
if info_label == null or main == null or main.doc.is_empty():
|
if info_label == null or main == null or main.doc.is_empty():
|
||||||
return
|
return
|
||||||
var gebaeude := BauOps.gebaeude_gruppen(main.doc)
|
var gebaeude := BauOps.gebaeude_gruppen(main.doc)
|
||||||
|
if geschoss_label != null:
|
||||||
|
geschoss_label.text = main.interaction.call("bau_geschoss_text")
|
||||||
var text := "%d Gebäude, %d Bauteile in der Map." % [gebaeude.size(), BauOps.teile_zahl(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"))
|
var gid := int(main.interaction.get("build_group_id"))
|
||||||
for g in gebaeude:
|
for g in gebaeude:
|
||||||
|
|
|
||||||
79
tests/probe_drag_echt.gd
Normal file
79
tests/probe_drag_echt.gd
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Echter Maus-Drag im sichtbaren Fenster (nicht headless): Kachel drücken, über die
|
||||||
|
## Szene ziehen, loslassen → genau ein Objekt gesetzt. Ausführen:
|
||||||
|
## godot4 --rendering-driver opengl3 --path . --script res://tests/probe_drag_echt.gd
|
||||||
|
|
||||||
|
func _mouse(pos: Vector2, pressed: bool, motion := false) -> void:
|
||||||
|
var ev: InputEvent
|
||||||
|
if motion:
|
||||||
|
var mm := InputEventMouseMotion.new()
|
||||||
|
mm.position = pos
|
||||||
|
mm.global_position = pos
|
||||||
|
mm.relative = Vector2(4, 4)
|
||||||
|
mm.button_mask = MOUSE_BUTTON_MASK_LEFT
|
||||||
|
ev = mm
|
||||||
|
else:
|
||||||
|
var mb := InputEventMouseButton.new()
|
||||||
|
mb.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
mb.position = pos
|
||||||
|
mb.global_position = pos
|
||||||
|
mb.pressed = pressed
|
||||||
|
mb.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
|
||||||
|
ev = mb
|
||||||
|
root.push_input(ev)
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
root.size = Vector2i(1600, 900)
|
||||||
|
var main: Control = (load("res://scenes/main.tscn") as PackedScene).instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for i in 10:
|
||||||
|
await process_frame
|
||||||
|
main.sym_mode = SymmetryCore.Mode.OFF
|
||||||
|
var vc0: Control = main.interaction.camera.get_viewport().get_parent()
|
||||||
|
var n0: int = (main.doc["objects"] as Array).size()
|
||||||
|
var oid := "barrel_001"
|
||||||
|
main.assets_mod._on_library_expand_all()
|
||||||
|
for i in 5:
|
||||||
|
await process_frame
|
||||||
|
var btn: Button = main.library_buttons.get(oid)
|
||||||
|
print("window=", root.size, " btn=", btn)
|
||||||
|
btn.get_parent().get_parent()
|
||||||
|
# Kachel in Sicht scrollen
|
||||||
|
var p := btn.get_parent()
|
||||||
|
while p != null and not (p is ScrollContainer):
|
||||||
|
p = p.get_parent()
|
||||||
|
if p != null:
|
||||||
|
(p as ScrollContainer).ensure_control_visible(btn)
|
||||||
|
for i in 5:
|
||||||
|
await process_frame
|
||||||
|
var r := btn.get_global_rect()
|
||||||
|
print("Kachel rect=", r)
|
||||||
|
var start := r.get_center()
|
||||||
|
var vc: Control = main.interaction.camera.get_viewport().get_parent()
|
||||||
|
var vr := vc.get_global_rect()
|
||||||
|
print("Szene rect=", vr)
|
||||||
|
var ziel := vr.get_center()
|
||||||
|
_mouse(start, false, true)
|
||||||
|
await process_frame
|
||||||
|
_mouse(start, true)
|
||||||
|
await process_frame
|
||||||
|
var steps := 25
|
||||||
|
for i in steps + 1:
|
||||||
|
var pos := start.lerp(ziel, float(i) / steps)
|
||||||
|
_mouse(pos, false, true)
|
||||||
|
await process_frame
|
||||||
|
for j in 6:
|
||||||
|
_mouse(ziel + Vector2(j * 3, j * 2), false, true)
|
||||||
|
await process_frame
|
||||||
|
var hov := root.gui_get_hovered_control()
|
||||||
|
print("Hover-Control: ", hov, " pfad=", hov.get_path() if hov != null else "-")
|
||||||
|
print("DragData=", root.gui_get_drag_data())
|
||||||
|
print("während Drag: dragging=", root.gui_is_dragging(), " placing=", main.interaction.placing)
|
||||||
|
_mouse(ziel, false)
|
||||||
|
for i in 4:
|
||||||
|
await process_frame
|
||||||
|
var n1: int = (main.doc["objects"] as Array).size()
|
||||||
|
print("Objekte: %d → %d placing=%s" % [n0, n1, main.interaction.placing])
|
||||||
|
print("ERGEBNIS: ", "OK" if n1 == n0 + 1 else "FEHLER")
|
||||||
|
quit(0 if n1 == n0 + 1 else 1)
|
||||||
|
|
@ -45,6 +45,9 @@ func _run_tests() -> void:
|
||||||
_test_snap_cells_and_edges()
|
_test_snap_cells_and_edges()
|
||||||
_test_snap_levels()
|
_test_snap_levels()
|
||||||
_test_replace_and_stack()
|
_test_replace_and_stack()
|
||||||
|
_test_dach_teile()
|
||||||
|
_test_geschoss_fix()
|
||||||
|
_test_wand_ueberstand()
|
||||||
_test_ops_group_undo()
|
_test_ops_group_undo()
|
||||||
_test_duplicate_building()
|
_test_duplicate_building()
|
||||||
_test_save_load()
|
_test_save_load()
|
||||||
|
|
@ -55,7 +58,7 @@ func _run_tests() -> void:
|
||||||
|
|
||||||
func _test_catalog() -> void:
|
func _test_catalog() -> void:
|
||||||
print("Katalog")
|
print("Katalog")
|
||||||
_check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog")
|
_check(BauKatalog.KINDS.size() == 23, "23 Bauteile im Katalog")
|
||||||
var in_gruppen := {}
|
var in_gruppen := {}
|
||||||
for g in BauKatalog.GRUPPEN:
|
for g in BauKatalog.GRUPPEN:
|
||||||
for oid in (g["oids"] as Array):
|
for oid in (g["oids"] as Array):
|
||||||
|
|
@ -99,7 +102,7 @@ func _test_meshes() -> void:
|
||||||
print("Meshes / Fußpunkt-Normalisierung")
|
print("Meshes / Fußpunkt-Normalisierung")
|
||||||
for oid in BauKatalog.oids():
|
for oid in BauKatalog.oids():
|
||||||
var m := BauKatalog.mesh(oid)
|
var m := BauKatalog.mesh(oid)
|
||||||
_check(m != null and m.get_faces().size() >= 36, "%s: Mesh vorhanden" % oid)
|
_check(m != null and m.get_faces().size() >= 18, "%s: Mesh vorhanden" % oid)
|
||||||
var aabb := m.get_aabb()
|
var aabb := m.get_aabb()
|
||||||
_check(absf(aabb.position.y) < 0.001, "%s: Unterkante y = 0" % oid)
|
_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,
|
_check(absf(aabb.position.x + aabb.size.x * 0.5) < 0.001 and absf(aabb.position.z + aabb.size.z * 0.5) < 0.001,
|
||||||
|
|
@ -186,13 +189,13 @@ func _test_snap_cells_and_edges() -> void:
|
||||||
_check(s["pos"] == Vector3(1.0, 0.0, 1.0), "Fundament: Zellmitte (1, 1)")
|
_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))
|
s = BuildSnap.berechne("bau_fundament", Vector3(-0.3, 0.0, -2.9), 0.0, kein, _flach(0.0))
|
||||||
_check(s["pos"] == Vector3(-1.0, 0.0, -3.0), "Fundament: negative Zelle (−1, −3)")
|
_check(s["pos"] == Vector3(-1.0, 0.0, -3.0), "Fundament: negative Zelle (−1, −3)")
|
||||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 0.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.09), 0.0, kein, _flach(0.0))
|
||||||
_check(s["pos"] == Vector3(1.0, 0.0, 0.1) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 10 cm in die Zelle gerückt (bündig)")
|
_check(_nah(s["pos"], Vector3(1.0, 0.0, 0.09)) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 9 cm in die Zelle gerückt (1 cm Überstand)")
|
||||||
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_wand", Vector3(0.09, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
||||||
_check(s["pos"] == Vector3(0.1, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 10 cm in die Zelle gerückt")
|
_check(_nah(s["pos"], Vector3(0.09, 0.0, 1.0)) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 9 cm in die Zelle gerückt (1 cm Überstand)")
|
||||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 90.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.09), 90.0, kein, _flach(0.0))
|
||||||
_check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.1), "R dreht die Wand um (180°), bleibt an ihrer Stelle")
|
_check(float(s["rot"]) == 180.0 and _nah(s["pos"], Vector3(1.0, 0.0, 0.09)), "R dreht die Wand um (180°), bleibt an ihrer Stelle")
|
||||||
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 90.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_wand", Vector3(0.09, 0.0, 1.0), 90.0, kein, _flach(0.0))
|
||||||
_check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°")
|
_check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°")
|
||||||
s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0))
|
||||||
_check(s["pos"] == Vector3(2.0, 0.0, 2.0), "Pfeiler: nächste Zellecke")
|
_check(s["pos"] == Vector3(2.0, 0.0, 2.0), "Pfeiler: nächste Zellecke")
|
||||||
|
|
@ -205,8 +208,8 @@ func _test_snap_cells_and_edges() -> void:
|
||||||
var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k)
|
var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k)
|
||||||
var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
|
var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
|
||||||
var wp: Vector3 = w["pos"]
|
var wp: Vector3 = w["pos"]
|
||||||
var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.1) < 0.001 # X nahe einer geraden Linie
|
var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.09) < 0.001 # X nahe einer geraden Linie
|
||||||
var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001
|
var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.09) < 0.001
|
||||||
var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
|
var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
|
||||||
var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001
|
var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001
|
||||||
if not ((ex and oz) or (ez and ox)):
|
if not ((ex and oz) or (ez and ox)):
|
||||||
|
|
@ -223,7 +226,7 @@ func _test_snap_cells_and_edges() -> void:
|
||||||
var ra := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wa["pos"], "rot": wa["rot"]})
|
var ra := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wa["pos"], "rot": wa["rot"]})
|
||||||
var rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]})
|
var rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]})
|
||||||
_check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt")
|
_check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt")
|
||||||
_check(absf(ra.position.x) < 0.001 and absf(ra.position.y) < 0.001 and absf(rb.position.x) < 0.001 and absf(rb.position.y) < 0.001,
|
_check(absf(ra.position.x) < 0.0111 and absf(ra.position.y) < 0.0111 and absf(rb.position.x) < 0.0111 and absf(rb.position.y) < 0.0111,
|
||||||
"Ecke: beide Wände beginnen bündig an der Außenkante (0,0)")
|
"Ecke: beide Wände beginnen bündig an der Außenkante (0,0)")
|
||||||
# Große Teile: Zellzahl bestimmt die Rasterung
|
# Große Teile: Zellzahl bestimmt die Rasterung
|
||||||
s = BuildSnap.berechne("bau_dach_giebel_4", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_dach_giebel_4", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
||||||
|
|
@ -231,7 +234,7 @@ func _test_snap_cells_and_edges() -> void:
|
||||||
s = BuildSnap.berechne("bau_dach_pyramide_6", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_dach_pyramide_6", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
||||||
_check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)")
|
_check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)")
|
||||||
s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0))
|
s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0))
|
||||||
_check(s["pos"] == Vector3(4.0, 0.0, 0.1), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt")
|
_check(_nah(s["pos"], Vector3(4.0, 0.0, 0.09)), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt")
|
||||||
# Gelände: höchste Höhe unter der Grundfläche zählt
|
# Gelände: höchste Höhe unter der Grundfläche zählt
|
||||||
s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0))
|
s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0))
|
||||||
_check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)")
|
_check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)")
|
||||||
|
|
@ -441,3 +444,64 @@ func _test_migration() -> void:
|
||||||
var leer := _neue_doc()
|
var leer := _neue_doc()
|
||||||
leer["gebaeude"] = []
|
leer["gebaeude"] = []
|
||||||
_check(BauOps.migriere_gebaeude(leer) == 0 and not leer.has("gebaeude"), "leere Sektion wird still entfernt")
|
_check(BauOps.migriere_gebaeude(leer) == 0 and not leer.has("gebaeude"), "leere Sektion wird still entfernt")
|
||||||
|
|
||||||
|
|
||||||
|
func _nah(a: Vector3, b: Vector3) -> bool:
|
||||||
|
return a.distance_to(b) < 0.0005
|
||||||
|
|
||||||
|
|
||||||
|
## Dach-Einzelteile: massiv (geschlossen, Volumen > 0, Normalen nach außen), 45°.
|
||||||
|
func _test_dach_teile() -> void:
|
||||||
|
print("Dach aus Einzelteilen")
|
||||||
|
for oid in ["bau_dach_schraege", "bau_dach_ecke_aussen", "bau_dach_ecke_innen", "bau_dach_plateau", "bau_dach_giebel_ende"]:
|
||||||
|
var m := BauKatalog.mesh(oid)
|
||||||
|
var f := m.get_faces()
|
||||||
|
var vol := 0.0
|
||||||
|
for i in range(0, f.size() - 2, 3):
|
||||||
|
vol += f[i].dot(f[i + 1].cross(f[i + 2])) / 6.0
|
||||||
|
_check(vol < -0.05, "%s: geschlossener Körper, Volumen %.2f m³ (Godot: Vorderseite im Uhrzeigersinn → negativ = nach außen)" % [oid, vol])
|
||||||
|
# jede Kante kommt in genau zwei Dreiecken vor (wasserdicht)
|
||||||
|
var kanten := {}
|
||||||
|
for i in range(0, f.size() - 2, 3):
|
||||||
|
for k in 3:
|
||||||
|
var a := f[i + k]
|
||||||
|
var b := f[i + (k + 1) % 3]
|
||||||
|
var key := "%s|%s" % [str(a.snapped(Vector3.ONE * 0.001)), str(b.snapped(Vector3.ONE * 0.001))]
|
||||||
|
kanten[key] = int(kanten.get(key, 0)) + 1
|
||||||
|
var offen := 0
|
||||||
|
for key in kanten:
|
||||||
|
var teile: PackedStringArray = (key as String).split("|")
|
||||||
|
if not kanten.has("%s|%s" % [teile[1], teile[0]]):
|
||||||
|
offen += 1
|
||||||
|
_check(offen == 0, "%s: wasserdicht (keine offene Kante, %d)" % [oid, offen])
|
||||||
|
var sch := BauKatalog.groesse("bau_dach_schraege")
|
||||||
|
_check(absf(sch.x - 2.0) < 0.01 and absf(sch.y - 2.0) < 0.01 and absf(sch.z - 2.0) < 0.01, "Schräge 2×2 m Grundfläche, 2 m hoch = 45°")
|
||||||
|
# Haus 4 m breit aus zwei Schrägen + Gegenschräge: Firsthöhe 2 m über Wandkrone
|
||||||
|
var kein: Array = []
|
||||||
|
var s := BuildSnap.berechne("bau_dach_schraege", Vector3(1.2, 3.0, 1.1), 90.0, kein, _flach(0.0))
|
||||||
|
_check(bool(s["ok"]) and float(s["rot"]) == 90.0, "Schräge lässt sich in 90°-Schritten drehen")
|
||||||
|
|
||||||
|
|
||||||
|
## Geschoss-Wahl: feste Höhe, unabhängig von Wänden darunter (Decke mitten im Raum).
|
||||||
|
func _test_geschoss_fix() -> void:
|
||||||
|
print("Geschoss fest (Decke mitten im Raum)")
|
||||||
|
var kein: Array = []
|
||||||
|
# Raum 6×6: nur Außenwände, in der Mitte steht nichts. Klick auf dem Boden (y=0).
|
||||||
|
var s := BuildSnap.berechne("bau_boden", Vector3(3.0, 0.0, 3.0), 0.0, kein, _flach(0.0), false, Vector3.ZERO, 3.0)
|
||||||
|
_check(float(s["ebene"]) == 3.0 and _nah(s["pos"], Vector3(3.0, 3.0, 3.0)), "Decke auf 3 m mitten im Raum (kein Teil darunter)")
|
||||||
|
var ohne := BuildSnap.berechne("bau_boden", Vector3(3.0, 0.0, 3.0), 0.0, kein, _flach(0.0))
|
||||||
|
_check(float(ohne["ebene"]) == 0.0, "Automatik bleibt wie bisher (Gelände)")
|
||||||
|
var teile := [{"i": 0, "oid": "bau_fundament", "pos": Vector3(1, 0.5, 1), "rot": 0.0}, {"i": 1, "oid": "bau_wand", "pos": Vector3(1, 0.5, 0.09), "rot": 0.0}]
|
||||||
|
_check(is_equal_approx(BuildSnap.geschoss_basis(teile, Vector2(5, 5), _flach(0.0)), 0.5), "Basis = tiefstes Teil in der Nähe (0,5 m)")
|
||||||
|
_check(is_equal_approx(BuildSnap.geschoss_basis([], Vector2(5, 5), _flach(2.0)), 2.0), "Basis ohne Teile = Gelände")
|
||||||
|
|
||||||
|
|
||||||
|
## Wand-Außenseite steht 1 cm vor der Plattenkante (kein Z-Fighting durch deckungsgleiche Flächen).
|
||||||
|
func _test_wand_ueberstand() -> void:
|
||||||
|
print("Wand ragt 1 cm über die Plattenkante")
|
||||||
|
var kein: Array = []
|
||||||
|
var w := BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0))
|
||||||
|
var b := BuildSnap.berechne("bau_fundament", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0))
|
||||||
|
var wr := BuildSnap.teil_rect({"oid": "bau_wand", "pos": w["pos"], "rot": w["rot"]})
|
||||||
|
var br := BuildSnap.teil_rect({"oid": "bau_fundament", "pos": b["pos"], "rot": b["rot"]})
|
||||||
|
_check(wr.position.y < br.position.y - 0.005, "Wand-Außenfläche liegt vor der Plattenkante (%.3f < %.3f)" % [wr.position.y, br.position.y])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue