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 (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 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) |
|
||||
|
||||
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
|
||||
liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder
|
||||
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
|
||||
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",
|
||||
"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": {
|
||||
"poly": 48,
|
||||
"collision": {
|
||||
|
|
@ -3536,6 +3566,36 @@
|
|||
"kategorie": "bauteile",
|
||||
"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": {
|
||||
"poly": 64,
|
||||
"collision": {
|
||||
|
|
@ -3581,6 +3641,21 @@
|
|||
"kategorie": "bauteile",
|
||||
"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": {
|
||||
"poly": 24,
|
||||
"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_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_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"]},
|
||||
]
|
||||
|
||||
|
|
@ -139,6 +141,29 @@ const KINDS := {
|
|||
"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),
|
||||
},
|
||||
# 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": {
|
||||
"label": "Treppe", "info": "4 m lang, 2 m breit, 3 m hoch — führt in ein Geschoss darüber",
|
||||
"snap": "treppe", "traeger": false,
|
||||
|
|
@ -190,7 +215,13 @@ static func mesh(oid: String) -> ArrayMesh:
|
|||
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 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)
|
||||
_mesh_cache[oid] = fertig
|
||||
return fertig
|
||||
|
|
@ -204,6 +235,79 @@ static func _box_mesh(size: Vector3) -> ArrayMesh:
|
|||
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:
|
||||
var geo := Node3D.new()
|
||||
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 SHRINK_M := 0.05 # Grundflächen-Schnitt: minimale Überlappung ignorieren
|
||||
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:
|
||||
|
|
@ -36,8 +38,12 @@ const SAME_M := 0.08 # Toleranz für "gleiche Stelle"
|
|||
## 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
|
||||
## 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,
|
||||
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 gr := BauKatalog.groesse(oid)
|
||||
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
|
||||
if p.distance_squared_to(h_mitte) <= p.distance_squared_to(v_mitte):
|
||||
mitte = h_mitte
|
||||
mitte.y += halbe_dicke if p.y >= h_mitte.y else -halbe_dicke
|
||||
mitte.y += (halbe_dicke - UEBERSTAND_M) * (1.0 if p.y >= h_mitte.y else -1.0)
|
||||
rot = 0.0
|
||||
else:
|
||||
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 += 180.0 * float(viertel % 2) # R dreht die Wand um (Blickseite der Teile)
|
||||
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)
|
||||
else:
|
||||
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.
|
||||
var ersetze := -1
|
||||
|
|
@ -234,3 +240,17 @@ static func sammle_teile(doc: Dictionary) -> Array:
|
|||
"rot": float(SvmapIO.objekt_rotation(o as Dictionary).y),
|
||||
})
|
||||
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_hit: Dictionary = {} # letzter Treffer {punkt, obj, dir}
|
||||
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
|
||||
# (TerrainField.raycast, wie Terrain-/Bauen-/Spawn-Werkzeug), nicht mehr nur
|
||||
|
|
@ -252,6 +254,7 @@ func cancel_placement() -> void:
|
|||
_ghost_active = false
|
||||
_build_last = {}
|
||||
_build_hit = {}
|
||||
build_geschoss = -1
|
||||
main.call("_highlight_library", "")
|
||||
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 dir := camera.project_ray_normal(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 out := {}
|
||||
if ground != null:
|
||||
|
|
@ -476,6 +481,55 @@ func _build_ray_hit(viewport_pos: Vector2) -> Dictionary:
|
|||
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:
|
||||
_build_hit = _build_ray_hit(viewport_pos)
|
||||
if _build_hit.is_empty():
|
||||
|
|
@ -490,13 +544,10 @@ func _bau_ghost_update(viewport_pos: Vector2) -> void:
|
|||
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)
|
||||
BuildSnap.sammle_teile(main.doc), _bau_gelaende_fn(),
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ var toolbar_mod # src/ui/panels/toolbar.gd
|
|||
var status_mod # src/ui/panels/status_bar.gd
|
||||
var selection_mod # src/ui/panels/selection_panel.gd (nur Logik, kein Tab mehr)
|
||||
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 sym_mod # src/ui/panels/symmetry_panel.gd (im Map-Tab)
|
||||
var map_mod # src/ui/panels/map_panel.gd
|
||||
|
|
@ -485,7 +486,12 @@ func _open_settings() -> void:
|
|||
if settings_dialog == null:
|
||||
return
|
||||
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:
|
||||
|
|
@ -695,6 +701,18 @@ func _build_center_viewport() -> SubViewportContainer:
|
|||
container.add_child(viewport_toolbar)
|
||||
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)
|
||||
help_label = Label.new()
|
||||
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:
|
||||
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:
|
||||
_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")))
|
||||
get_viewport().set_input_as_handled()
|
||||
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:
|
||||
KEY_T:
|
||||
# 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, {}))
|
||||
box.add_child(lbl)
|
||||
prev.modulate.a = 0.85
|
||||
if btn.get_viewport().gui_is_dragging():
|
||||
btn.set_drag_preview(prev)
|
||||
else:
|
||||
prev.free()
|
||||
btn.set_drag_preview(prev) # gehört in den Drag-Start: gui_is_dragging() ist hier noch false
|
||||
return {"svr_oid": oid}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ extends "res://src/ui/panels/editor_panel.gd"
|
|||
|
||||
var bau_buttons: Dictionary = {} # oid → Button
|
||||
var info_label: Label
|
||||
var geschoss_label: Label
|
||||
var _group: ButtonGroup
|
||||
|
||||
|
||||
|
|
@ -22,11 +23,25 @@ func panel_title() -> String:
|
|||
func build() -> void:
|
||||
_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."))
|
||||
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:
|
||||
add_child(_field_label(str(gruppe["label"])))
|
||||
add_child(_knopf_raster(gruppe["oids"] as Array))
|
||||
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"))
|
||||
info_label = _muted_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():
|
||||
return
|
||||
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 gid := int(main.interaction.get("build_group_id"))
|
||||
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_levels()
|
||||
_test_replace_and_stack()
|
||||
_test_dach_teile()
|
||||
_test_geschoss_fix()
|
||||
_test_wand_ueberstand()
|
||||
_test_ops_group_undo()
|
||||
_test_duplicate_building()
|
||||
_test_save_load()
|
||||
|
|
@ -55,7 +58,7 @@ func _run_tests() -> void:
|
|||
|
||||
func _test_catalog() -> void:
|
||||
print("Katalog")
|
||||
_check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog")
|
||||
_check(BauKatalog.KINDS.size() == 23, "23 Bauteile im Katalog")
|
||||
var in_gruppen := {}
|
||||
for g in BauKatalog.GRUPPEN:
|
||||
for oid in (g["oids"] as Array):
|
||||
|
|
@ -99,7 +102,7 @@ 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)
|
||||
_check(m != null and m.get_faces().size() >= 18, "%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,
|
||||
|
|
@ -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)")
|
||||
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.1) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0, 10 cm in die Zelle gerückt (bündig)")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(0.1, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90, 10 cm in die Zelle gerückt")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 90.0, kein, _flach(0.0))
|
||||
_check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.1), "R dreht die Wand um (180°), bleibt an ihrer Stelle")
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 90.0, kein, _flach(0.0))
|
||||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.09), 0.0, kein, _flach(0.0))
|
||||
_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.09, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
||||
_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.09), 90.0, kein, _flach(0.0))
|
||||
_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.09, 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")
|
||||
|
|
@ -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 w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
|
||||
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 ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001
|
||||
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.09) < 0.001
|
||||
var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
|
||||
var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001
|
||||
if not ((ex and oz) or (ez and ox)):
|
||||
|
|
@ -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 rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]})
|
||||
_check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt")
|
||||
_check(absf(ra.position.x) < 0.001 and absf(ra.position.y) < 0.001 and absf(rb.position.x) < 0.001 and absf(rb.position.y) < 0.001,
|
||||
_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)")
|
||||
# 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))
|
||||
|
|
@ -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))
|
||||
_check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)")
|
||||
s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0))
|
||||
_check(s["pos"] == Vector3(4.0, 0.0, 0.1), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt")
|
||||
_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
|
||||
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)")
|
||||
|
|
@ -441,3 +444,64 @@ func _test_migration() -> void:
|
|||
var leer := _neue_doc()
|
||||
leer["gebaeude"] = []
|
||||
_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