scopedvr-map-editor/tests/test_bauen.gd

225 lines
11 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends SceneTree
## Editor-Tests (headless): Karte t_97ea59c5 — Bauen-Modus (Modular-Builder).
## Ausführen: godot --headless --path . --script res://tests/test_bauen.gd
## Exit 0 = alles grün. Läuft OHNE UI (BuildOps/SvmapIO/ActionHistory) —
## GUI-Feinheiten (Ghost, Panel) verifiziert der Desktop-Smoke (--bauen-demo).
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", "user://test_bauen_legacy.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_make_piece_isolation()
_test_piece_meshes()
_test_snap()
_test_bake_and_ray_pick()
_test_undo_redo()
_test_save_load_roundtrip()
_test_legacy_map_without_gebaeude()
# ------------------------------------------------------------------ Tests ---
func _test_catalog() -> void:
print("Katalog")
_check(BuildOps.KINDS.size() == 10, "10 Bauteil-Typen im Katalog")
for kind_id in BuildOps.KINDS.keys():
var kind: Dictionary = BuildOps.KINDS[kind_id]
_check(str(kind.get("label", "")).length() > 3, "Label für %s" % str(kind_id))
_check((kind.get("punkte", []) as Array).size() >= 2, "Punkte für %s" % str(kind_id))
_check((kind.get("raster", "") == "ecke") or (kind.get("raster", "") == "mitte"),
"Raster-Typ für %s" % str(kind_id))
# Abnahme-Typen vorhanden: Fundament/Wand/Dach/Treppe/Tür
for need in ["fundament", "wand", "dach_giebel", "treppe", "tuer"]:
_check(BuildOps.KINDS.has(need), "Katalog enthält %s" % need)
func _test_make_piece_isolation() -> void:
print("make_piece")
var teil := BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 90.0)
_check(not teil.is_empty(), "Bauteil erzeugt")
_check(str(teil["id"]) == "wand", "id übernommen")
# pos kommt aus einem Vector3 (float32!) — Vergleiche mit Toleranz.
_check(absf(float(teil["pos"][0]) - 2.0) < 0.001 and absf(float(teil["pos"][1]) - 0.2) < 0.001 \
and absf(float(teil["pos"][2]) - 4.0) < 0.001, "Position übernommen")
_check(float(teil["rot"]) == 90.0, "Drehung übernommen")
# Punkte sind KOPIEN (kein Aliasing mit dem Katalog)
var before_x: float = (BuildOps.KINDS["wand"]["punkte"] as Array)[1][0]
(teil["punkte"] as Array)[1][0] = 99.0
_check(float((BuildOps.KINDS["wand"]["punkte"] as Array)[1][0]) == before_x,
"Katalog-Punkte bleiben unangetastet (keine Alias-Falle)")
var tuer := BuildOps.make_piece("tuer", Vector3.ZERO, 0.0)
_check((tuer["details"] as Array).size() == 1, "Tür-Wand hat 1 Öffnung")
_check(int(((tuer["details"] as Array)[0] as Dictionary)["typ"]) == BuildOps.DETAIL_DOOR,
"Öffnung ist eine Tür")
_check(BuildOps.make_piece("gibts_nicht", Vector3.ZERO, 0.0).is_empty(),
"Unbekannte ID → leeres Teil (kein Crash)")
func _test_piece_meshes() -> void:
print("Meshes")
for kind_id in BuildOps.KINDS.keys():
var mesh := BuildOps.piece_mesh(BuildOps.make_piece(str(kind_id), Vector3.ZERO, 0.0))
_check(mesh != null and mesh.get_surface_count() > 0, "Mesh für %s" % str(kind_id))
var wand := BuildOps.piece_mesh(BuildOps.make_piece("wand", Vector3.ZERO, 0.0))
var tuer := BuildOps.piece_mesh(BuildOps.make_piece("tuer", Vector3.ZERO, 0.0))
var wand_tris := int(wand.get_faces().size() / 3)
var tuer_tris := int(tuer.get_faces().size() / 3)
_check(wand_tris > 0 and tuer_tris > 0, "Wand/Tür-Meshes haben Dreiecke")
_check(wand_tris != tuer_tris, "Tür-Öffnung verändert das Wand-Mesh (%d vs %d Tris)" % [wand_tris, tuer_tris])
# Fundament: 2×2×0,2 m — AABB prüfen (lokale Geometrie, Ursprung Fußpunkt)
var f := BuildOps.piece_mesh(BuildOps.make_piece("fundament", Vector3.ZERO, 0.0))
var size := f.get_aabb().size
_check(absf(size.x - 2.0) < 0.01 and absf(size.z - 2.0) < 0.01 and absf(size.y - 0.2) < 0.01,
"Fundament-AABB 2×0,2×2 m (ist %.2f×%.2f×%.2f)" % [size.x, size.y, size.z])
func _test_snap() -> void:
print("Raster-Snap")
# "ecke" (Wand) → gerade Meter; "mitte" (Fundament) → ungerade Meter.
var w := BuildOps.snapped_pos("wand", Vector3(2.7, 0.11, -3.4))
_check(w.x == 2.0 and w.z == -4.0, "Wand rastet auf Zellen-Ecken (2, −4)")
_check(absf(w.y - 0.10) < 0.001, "Y rastet auf 5 cm")
var f := BuildOps.snapped_pos("fundament", Vector3(2.7, 0.0, 3.2))
_check(f.x == 3.0 and f.z == 3.0, "Fundament rastet auf Zellen-Mitte (3, 3)")
# Verbund: Fundament Mitte (3,3) hat Ecken bei 2 und 4 → Wand auf (2, y, 4)
# grenzt bündig. (Konvention, dokumentiert in build_ops.gd.)
func _test_bake_and_ray_pick() -> void:
print("Baken + Ray-Pick")
var doc := SvmapDocument.create_new("Bautest", 256.0, "mittag")
# Kleines Haus: Fundament → 3 Wände + Tür-Wand → 2 Giebelwände → Dach + Treppe
var pieces: Array = BuildOps.ensure_block(doc)
pieces.append(BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0))
pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 2.0), 0.0)) # Süd
pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 0.0)) # Nord
pieces.append(BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 90.0)) # West
pieces.append(BuildOps.make_piece("tuer", Vector3(4.0, 0.2, 4.0), 90.0)) # Ost + Tür
pieces.append(BuildOps.make_piece("giebelwand", Vector3(2.0, 2.2, 2.0), 0.0))
pieces.append(BuildOps.make_piece("giebelwand", Vector3(2.0, 2.2, 4.0), 0.0))
pieces.append(BuildOps.make_piece("dach_giebel", Vector3(3.0, 2.2, 3.0), 0.0))
pieces.append(BuildOps.make_piece("treppe", Vector3(10.0, 0.0, 9.0), 0.0))
var baked := BuildOps.bake_faces(doc)
var tris := int((baked["faces"] as PackedVector3Array).size() / 3)
_check(tris > 100, "Haus gebacken: %d Dreiecke" % tris)
_check((baked["offsets"] as Array).size() == 9, "Offsets je Teil (9)")
_check(int(baked["offsets"][0]) == 0, "erstes Teil startet bei Dreieck 0")
_check(int(baked["offsets"][8]) < tris, "letztes Teil liegt im Bereich")
# Ray von oben → trifft Dach (Teil-Index 7)
var pick := BuildOps.ray_pick(baked, Vector3(3.0, 50.0, 3.0), Vector3(0.0, -1.0, 0.0))
_check(not pick.is_empty(), "Ray von oben trifft das Haus")
_check(int(pick["piece"]) >= 0 and int(pick["piece"]) < 9, "Treffer gehört zu einem Teil (Index %d)" % int(pick["piece"]))
# Ray von der Seite auf Wandhöhe, daneben der Türöffnung → Teil 4 (Tür-Wand)
var side := BuildOps.ray_pick(baked, Vector3(50.0, 1.0, 3.6), Vector3(-1.0, 0.0, 0.0))
_check(not side.is_empty() and int(side["piece"]) == 4,
"Seiten-Ray trifft die Tür-Wand (Index %s)" % str(side.get("piece", "?")))
# … und DURCH die Türöffnung hindurch auf die West-Wand (Teil 3)
var door := BuildOps.ray_pick(baked, Vector3(50.0, 1.0, 3.0), Vector3(-1.0, 0.0, 0.0))
_check(not door.is_empty() and int(door["piece"]) == 3,
"Ray durch die Türöffnung trifft die West-Wand (Index %s)" % str(door.get("piece", "?")))
# Leer → kein Treffer
_check(BuildOps.ray_pick({"faces": PackedVector3Array(), "offsets": []},
Vector3.ZERO, Vector3.DOWN).is_empty(), "Leeres Baken → kein Treffer")
# AABB eines Teils liegt an der richtigen Stelle (Dach über 2,2 m)
var aabb := BuildOps.piece_aabb(pieces[7])
_check(aabb.position.y > 2.0, "Dach-AABB liegt über den Wänden (y=%.2f)" % aabb.position.y)
func _test_undo_redo() -> void:
print("Undo/Redo")
var doc := SvmapDocument.create_new("Undo", 256.0, "mittag")
var hist := ActionHistory.new()
hist.begin(doc)
# Teil 1: erster Snapshot MUSS den (leeren) Block enthalten, damit Strg+Z
# das erste Bauteil komplett entfernen kann.
var before: Dictionary = hist.snapshot(["gebaeude"])
_check(before.has("gebaeude"), "Snapshot enthält den leeren gebaeude-Block")
BuildOps.ensure_block(doc).append(BuildOps.make_piece("fundament", Vector3.ZERO, 0.0))
hist.commit("Fundament (2×2 m) gesetzt", before, hist.snapshot(["gebaeude"]))
# Teil 2 + 3
for i in 2:
before = hist.snapshot(["gebaeude"])
BuildOps.ensure_block(doc).append(BuildOps.make_piece("wand", Vector3(float(i) * 2.0, 0.2, 0.0), 0.0))
hist.commit("Wand (2 m) gesetzt", before, hist.snapshot(["gebaeude"]))
_check(BuildOps.piece_count(doc) == 3, "3 Bauteile gesetzt")
hist.undo()
_check(BuildOps.piece_count(doc) == 2, "Undo → 2 Bauteile")
hist.undo()
hist.undo()
_check(BuildOps.piece_count(doc) == 0, "3× Undo → alles weg (inkl. erstes Teil)")
hist.redo()
hist.redo()
_check(BuildOps.piece_count(doc) == 2, "2× Redo → 2 Bauteile wieder da")
# Löschen + Undo (wie build_tool.delete_selected)
before = hist.snapshot(["gebaeude"])
(BuildOps.pieces(doc) as Array).remove_at(0)
hist.commit("Bauteil gelöscht", before, hist.snapshot(["gebaeude"]))
_check(BuildOps.piece_count(doc) == 1, "Löschen → 1 Teil")
hist.undo()
_check(BuildOps.piece_count(doc) == 2, "Undo holt gelöschtes Teil zurück")
func _test_save_load_roundtrip() -> void:
print("Speichern/Laden")
var doc := SvmapDocument.create_new("Bauen-Save", 256.0, "mittag")
var pieces: Array = BuildOps.ensure_block(doc)
pieces.append(BuildOps.make_piece("fundament", Vector3(3.0, 0.0, 3.0), 0.0))
pieces.append(BuildOps.make_piece("tuer", Vector3(2.0, 0.2, 2.0), 90.0))
pieces.append(BuildOps.make_piece("dach_pyramide", Vector3(3.0, 2.2, 3.0), 0.0))
var save := SvmapIO.save_to_file("user://test_bauen.svmap", doc)
_check(bool(save["ok"]), "Map mit Bauteilen speichert (ohne Fehler)")
var load := SvmapIO.load_from_file("user://test_bauen.svmap")
_check(bool(load["ok"]), "Map mit Bauteilen lädt (V1–V15 + Prüfsumme grün)")
var loaded: Dictionary = load["data"]
_check(BuildOps.piece_count(loaded) == 3, "3 Bauteile überleben .svmap")
var src: Dictionary = pieces[1]
var dst: Dictionary = (BuildOps.pieces(loaded) as Array)[1]
_check(str(dst["id"]) == "tuer" and float(dst["rot"]) == 90.0, "Bauteil-Felder identisch")
_check((dst["punkte"] as Array) == (src["punkte"] as Array), "Punkte byte-gleich über den Roundtrip")
_check(((dst["details"] as Array)[0] as Dictionary)["w"] == 1.0, "Tür-Öffnung überlebt den Roundtrip")
# Gebackenes Mesh aus GELADENEN Teilen = wie vor dem Speichern (deterministisch)
var before_faces: PackedVector3Array = BuildOps.bake_faces(doc)["faces"]
var after_faces: PackedVector3Array = BuildOps.bake_faces(loaded)["faces"]
_check(before_faces == after_faces, "Baken ist deterministisch (vor/nach Speichern identisch)")
func _test_legacy_map_without_gebaeude() -> void:
print("Legacy-Maps")
var doc := SvmapDocument.create_new("Legacy", 256.0, "mittag")
doc.erase("gebaeude") # alte Map ohne die Sektion
var save := SvmapIO.save_to_file("user://test_bauen_legacy.svmap", doc)
_check(bool(save["ok"]), "Map OHNE gebaeude speichert")
var load := SvmapIO.load_from_file("user://test_bauen_legacy.svmap")
_check(bool(load["ok"]), "Map OHNE gebaeude lädt")
_check(BuildOps.piece_count(load["data"]) == 0, "Legacy-Map: 0 Bauteile, kein Crash")
if FileAccess.file_exists("user://test_bauen_legacy.svmap"):
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_bauen_legacy.svmap"))