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() == 12, "12 Bauteile im Katalog") var in_gruppen := {} for g in BauKatalog.GRUPPEN: for oid in (g["oids"] as Array): in_gruppen[oid] = true _check(BauKatalog.KINDS.has(oid), "Gruppe verweist auf bekanntes Teil %s" % oid) _check(in_gruppen.size() == BauKatalog.KINDS.size(), "jedes Teil steht in genau einer Gruppe (%d)" % in_gruppen.size()) for oid in BauKatalog.oids(): _check(str(oid).begins_with("bau_"), "%s: ID mit bau_-Präfix" % oid) _check(BauKatalog.label(oid).length() > 3, "%s: deutscher Name" % oid) _check(["zelle", "kante", "ecke", "treppe"].has(BauKatalog.snap_art(oid)), "%s: Snap-Art" % oid) func _test_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.0) and float(s["rot"]) == 0.0, "Wand: Kante entlang X, Drehung 0") s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 0.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(0.0, 0.0, 1.0) and float(s["rot"]) == 90.0, "Wand: Kante entlang Z, Drehung 90") s = BuildSnap.berechne("bau_wand", Vector3(1.0, 0.0, 0.1), 90.0, kein, _flach(0.0)) _check(float(s["rot"]) == 180.0 and s["pos"] == Vector3(1.0, 0.0, 0.0), "R dreht die Wand um (180°), bleibt auf der Kante") s = BuildSnap.berechne("bau_wand", Vector3(0.1, 0.0, 1.0), 90.0, kein, _flach(0.0)) _check(float(s["rot"]) == 270.0, "R: Z-Kante 90° → 270°") s = BuildSnap.berechne("bau_pfeiler", Vector3(1.8, 0.0, 2.3), 0.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(2.0, 0.0, 2.0), "Pfeiler: nächste Zellecke") s = BuildSnap.berechne("bau_treppe", Vector3(3.3, 0.0, 1.2), 0.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(4.0, 0.0, 1.0) and float(s["rot"]) == 0.0, "Treppe: 4×2-Fläche entlang X") s = BuildSnap.berechne("bau_treppe", Vector3(1.2, 0.0, 3.3), 90.0, kein, _flach(0.0)) _check(s["pos"] == Vector3(1.0, 0.0, 4.0) and float(s["rot"]) == 90.0, "Treppe: gedreht 90° → entlang Z") # Alle Fälle: gerasterte Mitten (Wände auf Kanten = ein gerader + ein ungerader Meter) for k in range(60): var p := Vector3(-9.0 + 0.31 * k, 0.0, 7.0 - 0.27 * k) var w := BuildSnap.berechne("bau_wand", p, 0.0, kein, _flach(0.0)) var wp: Vector3 = w["pos"] var gerade_x := int(roundf(wp.x)) % 2 == 0 var gerade_z := int(roundf(wp.z)) % 2 == 0 if not (gerade_x != gerade_z): _check(false, "Wand-Mitte liegt auf Kantenmitte (%s)" % str(wp)) return _check(true, "60 Zufallsklicks: Wandmitte liegt immer auf einer Kantenmitte") # Gelände: höchste Höhe unter der Grundfläche zählt s = BuildSnap.berechne("bau_fundament", Vector3(1.0, 5.0, 1.0), 0.0, kein, _flach(5.0)) _check(absf(float(s["ebene"]) - 5.0) < 0.001, "Gelände-Höhe wird die Ebene (5,0)") func _teil(i: int, oid: String, pos: Vector3, rot: float) -> Dictionary: return {"i": i, "oid": oid, "pos": pos, "rot": rot} func _test_snap_levels() -> void: print("Snap: Ebenen statt Klickhöhe") var teile: Array = [ _teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0), _teil(1, "bau_wand", Vector3(1, 0.2, 0), 0.0), _teil(2, "bau_wand", Vector3(2, 0.2, 1), 90.0), ] # Klick mitten auf die Seite der Wand (y=1,5): Boden darf NICHT in der Luft landen var s := BuildSnap.berechne("bau_boden", Vector3(0.9, 1.5, 0.6), 0.0, teile, _flach(0.0)) _check(absf(float(s["ebene"]) - 0.2) < 0.001, "Boden nach Klick auf Wandseite liegt auf dem Fundament (0,2), nicht bei 1,5") # Klick auf die Oberkante der Wand: nächste Geschoss-Ebene s = BuildSnap.berechne("bau_boden", Vector3(1.0, 3.2, 0.1), 0.0, teile, _flach(0.0), true, Vector3(0, -0.5, 1)) _check(absf(float(s["ebene"]) - 3.2) < 0.001, "Boden nach Klick auf Wandoberkante = Decke auf 3,2") _check(s["pos"] == Vector3(1.0, 3.2, -1.0), "…und in der Zelle auf der Kameraseite (1, −1)") # Wand auf Wand (Stapeln) s = BuildSnap.berechne("bau_wand", Vector3(1.0, 3.2, 0.0), 0.0, teile, _flach(0.0)) _check(absf(float(s["ebene"]) - 3.2) < 0.001 and int(s["ersetze"]) == -1, "Wand stapelt sich auf die Oberkante") # Ein Dach ist kein Träger: nichts darf auf der Dachspitze landen teile.append(_teil(3, "bau_dach_giebel", Vector3(1, 3.2, 1), 0.0)) s = BuildSnap.berechne("bau_boden", Vector3(1.0, 4.2, 1.0), 0.0, teile, _flach(0.0)) _check(float(s["ebene"]) <= 3.2 + 0.001, "Dachspitze (4,2) ist keine Ebene (Ebene %.2f)" % float(s["ebene"])) func _test_replace_and_stack() -> void: print("Ersetzen / doppelt") var teile: Array = [ _teil(0, "bau_fundament", Vector3(1, 0, 1), 0.0), _teil(1, "bau_wand", Vector3(1, 0.2, 0), 0.0), ] var s := BuildSnap.berechne("bau_wand_tuer", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0)) _check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Tür in vorhandene Wand → ersetzt Teil 1") s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 0.0, teile, _flach(0.0)) _check(not bool(s["ok"]) and str(s["grund"]).contains("schon"), "gleiche Wand nochmal → abgelehnt mit Erklärung") s = BuildSnap.berechne("bau_wand", Vector3(1.0, 1.5, 0.1), 90.0, teile, _flach(0.0)) _check(bool(s["ok"]) and int(s["ersetze"]) == 1, "Wand von der anderen Seite (R) ersetzt = dreht um") s = BuildSnap.berechne("bau_wand_tuer", Vector3(3.0, 0.0, 0.1), 0.0, teile, _flach(0.0)) _check(bool(s["ok"]) and int(s["ersetze"]) == -1, "Nachbarkante bleibt frei (kein Ersetzen)") func _neue_doc() -> Dictionary: return SvmapDocument.create_new("Bautest", 256.0, "mittag") func _test_ops_group_undo() -> void: print("BauOps: Gruppe, Undo, Ersetzen") var doc := _neue_doc() var hist := ActionHistory.new() hist.begin(doc) var snap := BuildSnap.berechne("bau_fundament", Vector3(1, 0, 1), 0.0, [], _flach(0.0)) var before := hist.snapshot(["objects", "group_names"]) if not before.has("group_names"): before["group_names"] = null var gid := BauOps.gruppe_neu(doc) _check(GroupState.group_name(doc, gid) == "Gebäude 1", "Gebäude-Gruppe heißt „Gebäude 1“") var r := BauOps.setze_teil(doc, "bau_fundament", snap, gid, SymmetryCore.Mode.OFF) hist.commit("Fundament bauen", before, hist.snapshot(["objects", "group_names"])) var objs: Array = doc["objects"] var n0 := objs.size() _check(n0 >= 1 and int(r["index"]) == n0 - 1, "Teil als Objekt angehängt") _check(GroupOps.group_id(objs[n0 - 1]) == gid, "Teil gehört zum Gebäude (Gruppe %d)" % gid) _check(BauOps.teile_zahl(doc) == 1, "teile_zahl = 1") # Wand setzen, dann Tür ersetzt dieselbe Stelle (Index bleibt) var teile := BuildSnap.sammle_teile(doc) var ws := BuildSnap.berechne("bau_wand", Vector3(1, 0.2, 0.05), 0.0, teile, _flach(0.0)) var before2 := hist.snapshot(["objects", "group_names"]) var wr := BauOps.setze_teil(doc, "bau_wand", ws, gid, SymmetryCore.Mode.OFF) hist.commit("Wand bauen", before2, hist.snapshot(["objects", "group_names"])) var widx := int(wr["index"]) teile = BuildSnap.sammle_teile(doc) var ts := BuildSnap.berechne("bau_wand_tuer", Vector3(1, 1.5, 0.05), 0.0, teile, _flach(0.0)) _check(int(ts["ersetze"]) == widx, "Tür-Klick findet die Wand") var before3 := hist.snapshot(["objects", "group_names"]) var tr := BauOps.setze_teil(doc, "bau_wand_tuer", ts, gid, SymmetryCore.Mode.OFF) hist.commit("Tür einsetzen", before3, hist.snapshot(["objects", "group_names"])) _check(bool(tr["ersetzt"]) and (doc["objects"] as Array).size() == n0 + 1, "Ersetzen fügt kein zusätzliches Objekt hinzu") _check(str((doc["objects"] as Array)[widx]["id"]) == "bau_wand_tuer", "Objekt ist jetzt die Tür-Wand") _check(GroupOps.group_id((doc["objects"] as Array)[widx]) == gid, "Gruppe bleibt beim Ersetzen") hist.undo() _check(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) >= 16, "Demo-Haus: %d Teile über die echte Snap-Logik" % BauOps.teile_zahl(d2)) _check(BauOps.gebaeude_von(d2, 0).size() == BauOps.teile_zahl(d2), "alle Teile des Hauses in einer Gruppe (%d)" % gid2) var doppelt := {} var hat_doppelte := false for t in BuildSnap.sammle_teile(d2): var key := "%s|%s|%s" % [str((t["pos"] as Vector3).snapped(Vector3(0.01, 0.01, 0.01))), str(roundf(float(t["rot"]))), BauKatalog.snap_art(str(t["oid"]))] if doppelt.has(key): hat_doppelte = true doppelt[key] = true _check(not hat_doppelte, "Haus hat keine doppelt gesetzten Teile") func _test_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")