- Tabs Auswahl/Prüfen/Test-Start/Einstellungen/Symmetrie weg; Spawn-Zonen, Test-Start, Symmetrie im Map-Tab; Einstellungen-Fenster (Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe, Raster-Snap, Gizmo-Schritt, Karte prüfen) - Bauen-Tab nur Bauteile; Objekt-Werkzeuge entfernt - Klick in der Map-Inhalt-Liste wählt in der Szene aus (SELECT_MULTI feuert nur multi_selected) - Objekt-Kachel per Ziehen in die Szene setzen - Wände bündig in der Zelle, gleiche Geschosshöhe mit/ohne Decke, Dächer/Giebelwände 4 m und 6 m Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
443 lines
24 KiB
GDScript
443 lines
24 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): Bauen-Modus (Umbau 25.10. — Bauteile sind normale
|
||
## Bibliotheks-Objekte `bau_*`, Gebäude = Gruppe).
|
||
## Ausführen: godot --headless --path . --script res://tests/test_bauen.gd
|
||
## Exit 0 = alles grün. Läuft OHNE UI (BauKatalog/BuildSnap/BauOps/SvmapIO/
|
||
## ActionHistory) — Ghost/Panel prüft der Desktop-Smoke (--bauen-demo).
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
|
||
|
||
func _init() -> void:
|
||
_cleanup()
|
||
_run_tests()
|
||
_cleanup()
|
||
if failures == 0:
|
||
print("ALL OK: %d checks" % checks)
|
||
quit(0)
|
||
else:
|
||
print("FAILED: %d/%d checks" % [failures, checks])
|
||
quit(1)
|
||
|
||
|
||
func _cleanup() -> void:
|
||
for path in ["user://test_bauen.svmap"]:
|
||
if FileAccess.file_exists(path):
|
||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
checks += 1
|
||
if cond:
|
||
print(" ok: %s" % label)
|
||
else:
|
||
failures += 1
|
||
print(" FAIL: %s" % label)
|
||
|
||
|
||
func _run_tests() -> void:
|
||
_test_catalog()
|
||
_test_registry()
|
||
_test_meshes()
|
||
_test_openings()
|
||
_test_stairs()
|
||
_test_snap_cells_and_edges()
|
||
_test_snap_levels()
|
||
_test_replace_and_stack()
|
||
_test_ops_group_undo()
|
||
_test_duplicate_building()
|
||
_test_save_load()
|
||
_test_migration()
|
||
|
||
|
||
# ------------------------------------------------------------------ Tests ---
|
||
|
||
func _test_catalog() -> void:
|
||
print("Katalog")
|
||
_check(BauKatalog.KINDS.size() == 18, "18 Bauteile im Katalog")
|
||
var in_gruppen := {}
|
||
for g in BauKatalog.GRUPPEN:
|
||
for oid in (g["oids"] as Array):
|
||
in_gruppen[oid] = true
|
||
_check(BauKatalog.KINDS.has(oid), "Gruppe verweist auf bekanntes Teil %s" % oid)
|
||
_check(in_gruppen.size() == BauKatalog.KINDS.size(), "jedes Teil steht in genau einer Gruppe (%d)" % in_gruppen.size())
|
||
for oid in BauKatalog.oids():
|
||
_check(str(oid).begins_with("bau_"), "%s: ID mit bau_-Präfix" % oid)
|
||
_check(BauKatalog.label(oid).length() > 3, "%s: deutscher Name" % oid)
|
||
_check(["zelle", "kante", "ecke", "treppe"].has(BauKatalog.snap_art(oid)), "%s: Snap-Art" % oid)
|
||
|
||
|
||
func _test_registry() -> void:
|
||
print("Registry / Dateien")
|
||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string("res://library/objects.json"))
|
||
var lib: Dictionary = (parsed as Dictionary).get("objects", {})
|
||
for oid in BauKatalog.oids():
|
||
_check(lib.has(oid), "%s steht in objects.json" % oid)
|
||
var e: Dictionary = lib.get(oid, {})
|
||
var soll := BauKatalog.registry_eintrag(oid)
|
||
var gleich := int(e.get("poly", -1)) == int(soll["poly"]) and str(e.get("glb", "")) == str(soll["glb"]) \
|
||
and str(e.get("name_de", "")) == str(soll["name_de"]) \
|
||
and str((e.get("collision", {}) as Dictionary).get("quelle", "")) == str((soll["collision"] as Dictionary)["quelle"])
|
||
var sz: Array = (e.get("collision", {}) as Dictionary).get("size", [])
|
||
var sz_soll: Array = (soll["collision"] as Dictionary)["size"]
|
||
for k in 3:
|
||
gleich = gleich and sz.size() == 3 and absf(float(sz[k]) - float(sz_soll[k])) < 0.002
|
||
_check(gleich, "%s: Registry = Katalog (sonst tools/gen_bauteile.gd neu laufen lassen)" % oid)
|
||
_check(str(e.get("kategorie", "")) == "bauteile", "%s: Kategorie bauteile" % oid)
|
||
var col: Dictionary = e.get("collision", {})
|
||
_check(str(col.get("shape", "")) == "trimesh", "%s: Trimesh-Kollision" % oid)
|
||
var path := "res://library/%s" % str(col.get("quelle", ""))
|
||
_check(FileAccess.file_exists(path), "%s: Kollisionsdatei vorhanden" % oid)
|
||
var kj: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||
var verts: Array = (kj as Dictionary).get("vertices", []) if kj is Dictionary else []
|
||
_check(verts.size() == int(e.get("poly", -1)) * 3, "%s: Kollisions-Dreiecke = poly (%d)" % [oid, int(e.get("poly", -1))])
|
||
_check(int(e.get("poly", 0)) <= 260, "%s: Quest-Budget (≤ 260 Dreiecke)" % oid)
|
||
|
||
|
||
func _test_meshes() -> void:
|
||
print("Meshes / Fußpunkt-Normalisierung")
|
||
for oid in BauKatalog.oids():
|
||
var m := BauKatalog.mesh(oid)
|
||
_check(m != null and m.get_faces().size() >= 36, "%s: Mesh vorhanden" % oid)
|
||
var aabb := m.get_aabb()
|
||
_check(absf(aabb.position.y) < 0.001, "%s: Unterkante y = 0" % oid)
|
||
_check(absf(aabb.position.x + aabb.size.x * 0.5) < 0.001 and absf(aabb.position.z + aabb.size.z * 0.5) < 0.001,
|
||
"%s: XZ zentriert" % oid)
|
||
var arr := m.surface_get_arrays(0)
|
||
var verts: PackedVector3Array = arr[Mesh.ARRAY_VERTEX]
|
||
var nrms: PackedVector3Array = arr[Mesh.ARRAY_NORMAL]
|
||
var falsch := 0
|
||
for t in range(0, verts.size() - 2, 3):
|
||
if (verts[t + 2] - verts[t]).cross(verts[t + 1] - verts[t]).dot(nrms[t]) < 0.0:
|
||
falsch += 1
|
||
_check(falsch == 0, "%s: Wicklung passt zur Normale (kein Culling-Fehler)" % oid)
|
||
var mat := m.surface_get_material(0) as StandardMaterial3D
|
||
_check(mat != null and mat.albedo_color.a > 0.99 and mat.albedo_color != Color.WHITE, "%s: Grundfarbe im Material" % oid)
|
||
var w := BauKatalog.groesse("bau_wand")
|
||
_check(absf(w.x - 2.0) < 0.01 and absf(w.y - 3.0) < 0.01 and absf(w.z - 0.2) < 0.01, "Wand 2×3×0,2 m")
|
||
var f := BauKatalog.groesse("bau_fundament")
|
||
_check(absf(f.x - 2.0) < 0.01 and absf(f.y - 0.2) < 0.01 and absf(f.z - 2.0) < 0.01, "Fundament 2×0,2×2 m")
|
||
var b := BauKatalog.groesse("bau_bruestung")
|
||
_check(absf(b.y - 1.1) < 0.01, "Brüstung 1,1 m hoch")
|
||
|
||
|
||
## Strahl durch das Kollisions-Mesh: true = trifft (Wand), false = frei (Öffnung).
|
||
func _trifft(oid: String, origin: Vector3, dir: Vector3) -> bool:
|
||
var faces := BauKatalog.mesh(oid).get_faces()
|
||
for i in int(faces.size() / 3):
|
||
if SelectionMath.ray_tri(origin, dir, faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2]) >= 0.0:
|
||
return true
|
||
return false
|
||
|
||
|
||
func _test_openings() -> void:
|
||
print("Türen / Fenster / Scharte sind echte Löcher")
|
||
var quer := Vector3(0.0, 0.0, 1.0)
|
||
# Tür: Mitte (x=0) frei von Boden bis 2,2 m, daneben (x=0.8) massiv, über der Tür (y=2.6) massiv.
|
||
_check(not _trifft("bau_wand_tuer", Vector3(0.0, 0.5, -1.0), quer), "Tür: Durchgang knapp über dem Boden frei")
|
||
_check(not _trifft("bau_wand_tuer", Vector3(0.0, 2.0, -1.0), quer), "Tür: Durchgang auf 2,0 m frei")
|
||
_check(_trifft("bau_wand_tuer", Vector3(0.0, 2.6, -1.0), quer), "Tür: über der Tür ist Wand")
|
||
_check(_trifft("bau_wand_tuer", Vector3(0.8, 1.0, -1.0), quer), "Tür: neben dem Rahmen ist Wand")
|
||
# Fenster: 1,1–2,3 m offen.
|
||
_check(not _trifft("bau_wand_fenster", Vector3(0.0, 1.7, -1.0), quer), "Fenster: Öffnung auf 1,7 m frei")
|
||
_check(_trifft("bau_wand_fenster", Vector3(0.0, 0.5, -1.0), quer), "Fenster: unter der Brüstung ist Wand")
|
||
_check(_trifft("bau_wand_fenster", Vector3(0.0, 2.6, -1.0), quer), "Fenster: darüber ist Wand")
|
||
# Schießscharte: Schlitz auf 1,3–1,7 m.
|
||
_check(not _trifft("bau_wand_scharte", Vector3(0.0, 1.5, -1.0), quer), "Scharte: Schlitz auf 1,5 m frei")
|
||
_check(_trifft("bau_wand_scharte", Vector3(0.0, 1.0, -1.0), quer), "Scharte: darunter Wand")
|
||
_check(_trifft("bau_wand", Vector3(0.0, 1.5, -1.0), quer), "Volle Wand ist dicht")
|
||
|
||
|
||
func _test_stairs() -> void:
|
||
print("Treppe begehbar")
|
||
var faces := BauKatalog.mesh("bau_treppe").get_faces()
|
||
var g := BauKatalog.groesse("bau_treppe")
|
||
_check(absf(g.x - 4.0) < 0.01 and absf(g.y - 3.0) < 0.01 and absf(g.z - 2.0) < 0.01, "Treppe 4×3×2 m")
|
||
# Oberste Höhe unter x entlang der Mittellinie (z=0): steigt monoton, Stufen ≤ 22 cm.
|
||
var letzte := 0.0
|
||
var max_sprung := 0.0
|
||
var steigt := true
|
||
for k in range(1, 40):
|
||
var x := -2.0 + 4.0 * float(k) / 40.0
|
||
var top := -1.0
|
||
for i in int(faces.size() / 3):
|
||
var t := SelectionMath.ray_tri(Vector3(x, 10.0, 0.0), Vector3.DOWN,
|
||
faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2])
|
||
if t >= 0.0:
|
||
top = maxf(top, 10.0 - t)
|
||
if top < letzte - 0.001:
|
||
steigt = false
|
||
max_sprung = maxf(max_sprung, top - letzte)
|
||
letzte = top
|
||
_check(steigt, "Lauffläche steigt monoton")
|
||
_check(max_sprung <= 0.22, "Stufenhöhe ≤ 22 cm (größter Sprung %.3f)" % max_sprung)
|
||
_check(letzte > 2.7, "Oben ≥ 2,7 m erreicht (%.2f)" % letzte)
|
||
|
||
|
||
func _flach(h: float) -> Callable:
|
||
return func(_x: float, _z: float) -> float: return h
|
||
|
||
|
||
func _test_snap_cells_and_edges() -> void:
|
||
print("Snap: Zelle / Kante / Ecke / Treppe")
|
||
var kein: Array = []
|
||
var s := BuildSnap.berechne("bau_fundament", Vector3(0.3, 0.0, 0.4), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(1.0, 0.0, 1.0), "Fundament: Zellmitte (1, 1)")
|
||
s = BuildSnap.berechne("bau_fundament", Vector3(-0.3, 0.0, -2.9), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(-1.0, 0.0, -3.0), "Fundament: negative Zelle (−1, −3)")
|
||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(1.0, 0.0, 0.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))
|
||
_check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°")
|
||
s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(2.0, 0.0, 2.0), "Pfeiler: nächste Zellecke")
|
||
s = BuildSnap.berechne("bau_treppe", Vector3(3.3, 0.0, 1.2), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(4.0, 0.0, 1.0) and float(s["rot"]) == 0.0, "Treppe: 4×2-Fläche entlang X")
|
||
s = BuildSnap.berechne("bau_treppe", Vector3(1.2, 0.0, 3.3), 90.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(1.0, 0.0, 4.0) and float(s["rot"]) == 90.0, "Treppe: gedreht 90° → entlang Z")
|
||
# Alle Fälle: Wandmitte = 10 cm neben einer Kantenmitte (Wand liegt in der Zelle)
|
||
for k in range(60):
|
||
var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k)
|
||
var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0))
|
||
var wp: Vector3 = w["pos"]
|
||
var ex := absf(absf(wp.x - roundf(wp.x / 2.0) * 2.0) - 0.1) < 0.001 # X nahe einer geraden Linie
|
||
var ez := absf(absf(wp.z - roundf(wp.z / 2.0) * 2.0) - 0.1) < 0.001
|
||
var ox := absf(fposmod(wp.x, 2.0) - 1.0) < 0.001 # X auf Zellmitte
|
||
var oz := absf(fposmod(wp.z, 2.0) - 1.0) < 0.001
|
||
if not ((ex and oz) or (ez and ox)):
|
||
_check(false, "Wand-Mitte liegt in der Zelle an der Kante (%s)" % str(wp))
|
||
return
|
||
# und zwar in der Zelle, in der die Maus war (Außenseite bündig auf der Kante)
|
||
if (ez and absf(wp.z - p.z) > 1.01) or (ex and absf(wp.x - p.x) > 1.01):
|
||
_check(false, "Wand liegt auf der Maus-Seite (%s zu %s)" % [str(wp), str(p)])
|
||
return
|
||
_check(true, "60 Zufallsklicks: Wand liegt immer in der Zelle der Maus, bündig an einer Kante")
|
||
# Ecke: zwei Wände in einer Zellecke schließen sich ohne Lücke (äußere Ecke voll)
|
||
var wa := BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.3), 0.0, kein, _flach(0.0))
|
||
var wb := BuildSnap.berechne("bau_wand", Vector3(0.3, 0.0, 1.0), 0.0, kein, _flach(0.0))
|
||
var ra := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wa["pos"], "rot": wa["rot"]})
|
||
var rb := BuildSnap.teil_rect({"oid": "bau_wand", "pos": wb["pos"], "rot": wb["rot"]})
|
||
_check(ra.has_point(Vector2(0.01, 0.01)) and rb.has_point(Vector2(0.01, 0.01)), "Ecke: äußere Ecke (0,0) ist von BEIDEN Wänden gedeckt")
|
||
_check(absf(ra.position.x) < 0.001 and absf(ra.position.y) < 0.001 and absf(rb.position.x) < 0.001 and absf(rb.position.y) < 0.001,
|
||
"Ecke: beide Wände beginnen bündig an der Außenkante (0,0)")
|
||
# Große Teile: Zellzahl bestimmt die Rasterung
|
||
s = BuildSnap.berechne("bau_dach_giebel_4", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(4.0, 0.0, 4.0), "Satteldach 4×4: Mitte auf Zellecke (4, 4)")
|
||
s = BuildSnap.berechne("bau_dach_pyramide_6", Vector3(3.2, 0.0, 3.2), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(3.0, 0.0, 3.0), "Pyramidendach 6×6: Mitte einer Zelle (3, 3)")
|
||
s = BuildSnap.berechne("bau_giebelwand_4", Vector3(3.0, 0.0, 0.5), 0.0, kein, _flach(0.0))
|
||
_check(s["pos"] == Vector3(4.0, 0.0, 0.1), "Giebelwand 4 m: Mitte auf gerader Linie (4), in die Zelle gerückt")
|
||
# Gelände: höchste Höhe unter der Grundfläche zählt
|
||
s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0))
|
||
_check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)")
|
||
|
||
|
||
func _teil(i: int, oid: String, pos: Vector3, rot: float) -> Dictionary:
|
||
return {"i": i, "oid": oid, "pos": pos, "rot": rot}
|
||
|
||
|
||
func _test_snap_levels() -> void:
|
||
print("Snap: Ebenen statt Klickhöhe")
|
||
var teile: Array = [
|
||
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0),
|
||
_teil(1, "bau_wand", Vector3(1, 0, 0.1), 0.0),
|
||
_teil(2, "bau_wand", Vector3(1.9, 0, 1), 90.0),
|
||
]
|
||
# Klick mitten auf die Seite der Wand (y=1,5): Boden darf NICHT in der Luft landen
|
||
var s := BuildSnap.berechne("bau_boden", Vector3(0.9, 1.5, 0.6), 0.0, teile, _flach(0.0))
|
||
_check(absf(float(s["ebene"]) - 0.2) < 0.001, "Boden nach Klick auf Wandseite liegt auf dem Fundament (0,2), nicht bei 1,5")
|
||
# Klick auf die Oberkante der Wand: nächste Geschoss-Ebene
|
||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 3.0, 0.1), 0.0, teile, _flach(0.0), true, Vector3(0, -0.5, 1))
|
||
_check(absf(float(s["ebene"]) - 3.0) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,0")
|
||
_check(s["pos"] == Vector3(1.0, 3.0, 1.0), "…und in der Zelle, in der die Wand steht (1, 1) — die Decke liegt auf der Wand")
|
||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 1.0, 1.0), 0.0, [_teil(9, "bau_pfeiler", Vector3(2, 0, 2), 0.0)], _flach(0.0), true, Vector3(0, -0.5, 1))
|
||
_check(s["pos"].z == 1.0, "Treffer auf Nicht-Wand: Zelle der Kameraseite (Pfeiler an der Ecke)")
|
||
# Wand auf Wand (Stapeln): genau über der Wand darunter
|
||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.0, 0.1), 0.0, teile, _flach(0.0), true)
|
||
_check(absf(float(s["ebene"]) - 3.0) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante (3,0)")
|
||
_check(s["pos"] == Vector3(1.0, 3.0, 0.1), "…und exakt über der Wand darunter (gleiche Lage)")
|
||
# Test-Befund 25.10.: obere Wände sitzen gleich hoch, egal ob auf der Nachbarwand
|
||
# eine Decke liegt oder nicht (Decke liegt AUF der Wand, Wände zählen ab Deckenunterkante).
|
||
var mit_decke: Array = teile.duplicate()
|
||
mit_decke.append(_teil(3, "bau_boden", Vector3(1, 3.0, -1), 0.0)) # Decke über der Wand vorn (Zelle −1)
|
||
var ohne := BuildSnap.berechne("bau_wand", Vector3(1.0, 3.0, 0.1), 0.0, mit_decke, _flach(0.0), true)
|
||
var mit := BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, -0.1), 0.0, mit_decke, _flach(0.0), true)
|
||
_check(absf(float(ohne["ebene"]) - 3.0) < 0.001, "Obergeschoss-Wand ohne Decke: Höhe 3,0 (%.2f)" % float(ohne["ebene"]))
|
||
_check(absf(float(mit["ebene"]) - 3.0) < 0.001, "Obergeschoss-Wand AUF der Decke: ebenfalls 3,0 (%.2f)" % float(mit["ebene"]))
|
||
# Ein Dach ist kein Träger: nichts darf auf der Dachspitze landen
|
||
teile.append(_teil(4, "bau_dach_giebel", Vector3(1, 3.0, 1), 0.0))
|
||
s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.0, 1.0), 0.0, teile, _flach(0.0))
|
||
_check(float(s["ebene"]) <= 3.0 + 0.001, "Dachspitze (4,0) ist keine Ebene (Ebene %.2f)" % float(s["ebene"]))
|
||
|
||
|
||
func _test_replace_and_stack() -> void:
|
||
print("Ersetzen / doppelt")
|
||
var teile: Array = [
|
||
_teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0),
|
||
_teil(1, "bau_wand", Vector3(1, 0, 0.1), 0.0),
|
||
]
|
||
var s := BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0))
|
||
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür in vorhandene Wand → ersetzt Teil 1")
|
||
# Klick direkt auf die Wand (Objekt-Treffer) übernimmt ihre Lage, auch von der anderen Seite
|
||
s = BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.0), 0.0, teile, _flach(0.0), true, Vector3(0, 0, 1))
|
||
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür: Klick auf die Außenseite der Wand ersetzt dieselbe Wand")
|
||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0))
|
||
_check(not bool(s["ok"]) and str(s["grund"]).contains("schon"), "gleiche Wand nochmal → abgelehnt mit Erklärung")
|
||
s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 90.0, teile, _flach(0.0))
|
||
_check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Wand von der anderen Seite (R) ersetzt = dreht um")
|
||
s = BuildSnap.berechne("bau_wand_tuer", Vector3(3.0, 0.0, 0.1), 0.0, teile, _flach(0.0))
|
||
_check(bool(s["ok"]) and int(s["ersetze"]) == -1, "Nachbarkante bleibt frei (kein Ersetzen)")
|
||
|
||
|
||
func _neue_doc() -> Dictionary:
|
||
return SvmapDocument.create_new("Bautest", 256.0, "mittag")
|
||
|
||
|
||
func _test_ops_group_undo() -> void:
|
||
print("BauOps: Gruppe, Undo, Ersetzen")
|
||
var doc := _neue_doc()
|
||
var hist := ActionHistory.new()
|
||
hist.begin(doc)
|
||
var snap := BuildSnap.berechne("bau_fundament", Vector3(1, 0, 1), 0.0, [], _flach(0.0))
|
||
var before := hist.snapshot(["objects", "group_names"])
|
||
if not before.has("group_names"):
|
||
before["group_names"] = null
|
||
var gid := BauOps.gruppe_neu(doc)
|
||
_check(GroupState.group_name(doc, gid) == "Gebäude 1", "Gebäude-Gruppe heißt „Gebäude 1“")
|
||
var r := BauOps.setze_teil(doc, "bau_fundament", snap, gid, SymmetryCore.Mode.OFF)
|
||
hist.commit("Fundament bauen", before, hist.snapshot(["objects", "group_names"]))
|
||
var objs: Array = doc["objects"]
|
||
var n0 := objs.size()
|
||
_check(n0 >= 1 and int(r["index"]) == n0 - 1, "Teil als Objekt angehängt")
|
||
_check(GroupOps.group_id(objs[n0 - 1]) == gid, "Teil gehört zum Gebäude (Gruppe %d)" % gid)
|
||
_check(BauOps.teile_zahl(doc) == 1, "teile_zahl = 1")
|
||
# Wand setzen, dann Tür ersetzt dieselbe Stelle (Index bleibt)
|
||
var teile := BuildSnap.sammle_teile(doc)
|
||
var ws := BuildSnap.berechne("bau_wand", Vector3(1, 0.2, 0.15), 0.0, teile, _flach(0.0))
|
||
var before2 := hist.snapshot(["objects", "group_names"])
|
||
var wr := BauOps.setze_teil(doc, "bau_wand", ws, gid, SymmetryCore.Mode.OFF)
|
||
hist.commit("Wand bauen", before2, hist.snapshot(["objects", "group_names"]))
|
||
var widx := int(wr["index"])
|
||
teile = BuildSnap.sammle_teile(doc)
|
||
var ts := BuildSnap.berechne("bau_wand_tuer", Vector3(1, 1.5, 0.15), 0.0, teile, _flach(0.0))
|
||
_check(int(ts["ersetze"]) == widx, "Tür-Klick findet die Wand")
|
||
var before3 := hist.snapshot(["objects", "group_names"])
|
||
var tr := BauOps.setze_teil(doc, "bau_wand_tuer", ts, gid, SymmetryCore.Mode.OFF)
|
||
hist.commit("Tür einsetzen", before3, hist.snapshot(["objects", "group_names"]))
|
||
_check(bool(tr["ersetzt"]) and (doc["objects"] as Array).size() == n0 + 1, "Ersetzen fügt kein zusätzliches Objekt hinzu")
|
||
_check(str((doc["objects"] as Array)[widx]["id"]) == "bau_wand_tuer", "Objekt ist jetzt die Tür-Wand")
|
||
_check(GroupOps.group_id((doc["objects"] as Array)[widx]) == gid, "Gruppe bleibt beim Ersetzen")
|
||
hist.undo()
|
||
_check(str((doc["objects"] as Array)[widx]["id"]) == "bau_wand", "Undo: wieder normale Wand")
|
||
hist.undo()
|
||
hist.undo()
|
||
_check((doc["objects"] as Array).is_empty(), "Undo ×3: alles weg")
|
||
_check(not doc.has("group_names") or (doc["group_names"] as Dictionary).is_empty(), "Undo entfernt auch den Gebäude-Namen")
|
||
hist.redo()
|
||
hist.redo()
|
||
hist.redo()
|
||
_check(BauOps.teile_zahl(doc) == 2, "Redo ×3 stellt Fundament + Tür-Wand wieder her")
|
||
# Gebäude-Info
|
||
var geb := BauOps.gebaeude_gruppen(doc)
|
||
_check(geb.size() == 1 and (geb[0]["indizes"] as Array).size() == 2, "gebaeude_gruppen: 1 Gebäude mit 2 Teilen")
|
||
_check(BauOps.gebaeude_von(doc, 0).size() == 2, "gebaeude_von liefert alle Mitglieder")
|
||
# Haus komplett
|
||
var d2 := _neue_doc()
|
||
var gid2 := BauOps.haus_demo(d2, null, Vector3(4.0, 0.0, 4.0))
|
||
_check(BauOps.teile_zahl(d2) >= 13, "Demo-Haus: %d Teile über die echte Snap-Logik" % BauOps.teile_zahl(d2))
|
||
_check(BauOps.gebaeude_von(d2, 0).size() == BauOps.teile_zahl(d2), "alle Teile des Hauses in einer Gruppe (%d)" % gid2)
|
||
var doppelt := {}
|
||
var hat_doppelte := false
|
||
for t in BuildSnap.sammle_teile(d2):
|
||
var key := "%s|%s|%s" % [str((t["pos"] as Vector3).snapped(Vector3(0.01, 0.01, 0.01))), str(roundf(float(t["rot"]))), BauKatalog.snap_art(str(t["oid"]))]
|
||
if doppelt.has(key):
|
||
hat_doppelte = true
|
||
doppelt[key] = true
|
||
_check(not hat_doppelte, "Haus hat keine doppelt gesetzten Teile")
|
||
|
||
|
||
func _test_duplicate_building() -> void:
|
||
print("Gebäude kopieren / löschen")
|
||
var doc := _neue_doc()
|
||
var gid := BauOps.haus_demo(doc, null, Vector3(4.0, 0.0, 4.0))
|
||
var anzahl := (doc["objects"] as Array).size()
|
||
var alle := BauOps.gebaeude_von(doc, 0)
|
||
var neue := ObjectOps.duplicate_objects(doc, alle, false, 8.0)
|
||
_check(neue.size() == anzahl, "alle %d Teile kopiert" % anzahl)
|
||
var gid_kopie := GroupOps.group_id((doc["objects"] as Array)[int(neue[0])])
|
||
_check(gid_kopie != gid and gid_kopie != GroupOps.UNGROUPED, "Kopie bekommt eine EIGENE Gruppe (%d ≠ %d)" % [gid_kopie, gid])
|
||
_check(GroupOps.members_of(doc, gid).size() == anzahl and GroupOps.members_of(doc, gid_kopie).size() == anzahl,
|
||
"Original und Kopie haben je %d Teile" % anzahl)
|
||
_check(GroupState.display_name(doc, gid_kopie).contains("Kopie"), "Kopie heißt „… (Kopie)“")
|
||
_check(BauOps.gebaeude_gruppen(doc).size() == 2, "zwei Gebäude in der Map")
|
||
var o0: Dictionary = (doc["objects"] as Array)[0]
|
||
var k0: Dictionary = (doc["objects"] as Array)[int(neue[0])]
|
||
_check(absf(float(k0["pos"][0]) - float(o0["pos"][0]) - 8.0) < 0.001, "Kopie um 8 m versetzt")
|
||
# Löschen: Gebäude ohne Teile verliert den Namen
|
||
var objects: Array = doc["objects"]
|
||
var del := GroupOps.members_of(doc, gid_kopie)
|
||
del.sort()
|
||
del.reverse()
|
||
for i in del:
|
||
objects.remove_at(i)
|
||
GroupState.prune_orphans(doc)
|
||
_check(BauOps.gebaeude_gruppen(doc).size() == 1, "nach Löschen: 1 Gebäude")
|
||
_check(not GroupState.names_dict(doc).has(str(gid_kopie)), "Gruppenname der gelöschten Kopie ist weg")
|
||
|
||
|
||
func _test_save_load() -> void:
|
||
print("Speichern / Laden")
|
||
var doc := _neue_doc()
|
||
BauOps.haus_demo(doc, null, Vector3(4.0, 0.0, 4.0))
|
||
var n := (doc["objects"] as Array).size()
|
||
var res := SvmapIO.save_to_file("user://test_bauen.svmap", doc)
|
||
_check(bool(res["ok"]), "Map mit Haus speichern (Validierung V1–V17 grün)")
|
||
var back := SvmapIO.load_from_file("user://test_bauen.svmap")
|
||
_check(bool(back["ok"]), "…und wieder laden")
|
||
if bool(back["ok"]):
|
||
var d: Dictionary = back["data"]
|
||
_check((d["objects"] as Array).size() == n, "alle %d Teile nach dem Laden da" % n)
|
||
_check(BauOps.gebaeude_gruppen(d).size() == 1, "Gebäude-Gruppe erhalten")
|
||
_check(GroupState.display_name(d, 0).begins_with("Gebäude"), "Gebäude-Name erhalten")
|
||
_check(not d.has("gebaeude"), "keine alte gebaeude-Sektion mehr")
|
||
# Poly-Rechnung: Bauteile zählen im Budget wie jedes Objekt
|
||
var manifest: Variant = JSON.parse_string(FileAccess.get_file_as_string("res://library/objects.json"))
|
||
var poly := SvmapDocument.poly_usage(doc, manifest as Dictionary)
|
||
_check(absf(poly - float(BauOps.dreiecke_gesamt(doc))) < 0.5, "Poly-Budget = Summe der Bauteil-Dreiecke (%d)" % int(poly))
|
||
|
||
|
||
func _test_migration() -> void:
|
||
print("Migration alte gebaeude-Sektion")
|
||
var doc := _neue_doc()
|
||
doc["gebaeude"] = [
|
||
{"id": "fundament", "pos": [3.0, 0.0, 3.0], "rot": 0.0, "punkte": []},
|
||
{"id": "wand", "pos": [2.0, 0.2, 2.0], "rot": 0.0, "punkte": []},
|
||
{"id": "tuer", "pos": [4.0, 0.2, 4.0], "rot": 90.0, "punkte": []},
|
||
{"id": "unbekannt", "pos": [0.0, 0.0, 0.0], "rot": 0.0},
|
||
]
|
||
var n := BauOps.migriere_gebaeude(doc)
|
||
_check(n == 3, "3 bekannte Teile übernommen (unbekannte ignoriert)")
|
||
_check(not doc.has("gebaeude"), "alte Sektion entfernt")
|
||
var objs: Array = doc["objects"]
|
||
var wand: Dictionary = {}
|
||
for o in objs:
|
||
if str(o["id"]) == "bau_wand":
|
||
wand = o
|
||
_check(not wand.is_empty() and absf(float(wand["pos"][0]) - 3.0) < 0.001 and absf(float(wand["pos"][2]) - 2.0) < 0.001,
|
||
"Wand-Ursprung wandert in die Wandmitte (2,2 → 3,2)")
|
||
var tuer: Dictionary = {}
|
||
for o in objs:
|
||
if str(o["id"]) == "bau_wand_tuer":
|
||
tuer = o
|
||
_check(not tuer.is_empty() and absf(float(tuer["pos"][0]) - 4.0) < 0.001 and absf(float(tuer["pos"][2]) - 3.0) < 0.001,
|
||
"gedrehte Tür-Wand (90°): Mitte (4, 3)")
|
||
_check(BauOps.gebaeude_gruppen(doc).size() == 1, "alle Teile in einem Gebäude")
|
||
_check(BauOps.migriere_gebaeude(doc) == 0, "zweiter Lauf tut nichts")
|
||
var leer := _neue_doc()
|
||
leer["gebaeude"] = []
|
||
_check(BauOps.migriere_gebaeude(leer) == 0 and not leer.has("gebaeude"), "leere Sektion wird still entfernt")
|