extends SceneTree ## Editor-Tests (headless): Karte t_f5935023 — M1 Symmetrie-Fundament. ## Ausführen: godot --headless --path . --script res://tests/test_symmetry_core.gd ## Exit 0 = alles grün. Läuft OHNE UI (nur SymmetryCore/SvmapIO/SvmapDocument) — ## Toolbar/Statuszeile verifiziert der GUI-Smoke (--sym-demo kommt mit M2/M3). var failures := 0 var checks := 0 func _init() -> void: # user:// persistiert über Godot-Läufe — vor UND nach dem Test aufräumen # (Lektion aus t_5badf371: Stale-Reste maskierten sonst Verifikationen). _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_sym.svmap", "user://test_sym_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 _errors_contain(errors: Array, needle: String) -> bool: for e in errors: if str(e).contains(needle): return true return false func _run_tests() -> void: _test_modes_and_labels() _test_center() _test_mirror_point_double_identity() _test_mirror_rot_double_identity() _test_maps_to_self() _test_cell_mapping() _test_partner_link() _test_validator_sym() _test_roundtrip_with_sym() _test_legacy_map_unchanged() # ------------------------------------------------------------------ Tests --- func _test_modes_and_labels() -> void: print("== Modus-Katalog ==") _check(SymmetryCore.Mode.OFF == 0, "OFF = 0") _check(SymmetryCore.Mode.POINT == 1, "POINT = 1 (Standard, Sly 20.10.)") _check(SymmetryCore.Mode.AXIS_X == 2, "AXIS_X = 2") _check(SymmetryCore.Mode.AXIS_Z == 3, "AXIS_Z = 3") _check(SymmetryCore.MODE_LABELS.size() == 4, "4 Modus-Labels (Aus/Punkt/Achse X/Achse Z)") _check(SymmetryCore.mode_label(SymmetryCore.Mode.POINT) == "Punkt", "Label für POINT") _check(SymmetryCore.mode_label(9) == "Achse Z", "Unbekannter Modus clampt auf letzten Label") func _test_center() -> void: print("== Kartenmitte ==") var doc := SvmapDocument.create_new("SymTest", 1024.0, "mittag") var c := SymmetryCore.map_center(doc) _check(c.is_zero_approx(), "Zentrierte Map: Mitte = (0, 0) (bounds ±size/2)") # Fehlende bounds → Fallback (0, 0), kein Crash. _check(SymmetryCore.map_center({}).is_zero_approx(), "Defektes doc → Mitte (0, 0), kein Crash") # Asymmetrische bounds: Mitte = (min + max) / 2. var doc2 := doc.duplicate(true) doc2["bounds"] = {"min": [-100.0, -10.0, -50.0], "max": [300.0, 80.0, 50.0]} var c2 := SymmetryCore.map_center(doc2) _check(absf(c2.x - 100.0) < 0.001 and absf(c2.y - 0.0) < 0.001, "Asymmetrische bounds: Mitte (100, 0) korrekt") func _test_mirror_point_double_identity() -> void: print("== Zweimal spiegeln = Ausgangslage (Position, alle 3 Modi) ==") var doc := SvmapDocument.create_new("SymTest", 1024.0, "mittag") var center := SymmetryCore.map_center(doc) var probe: Vector3 = Vector3(123.5, 42.0, -77.25) for mode in [SymmetryCore.Mode.POINT, SymmetryCore.Mode.AXIS_X, SymmetryCore.Mode.AXIS_Z]: var once := SymmetryCore.mirror_point(mode, probe, center) var twice := SymmetryCore.mirror_point(mode, once, center) _check(twice.distance_to(probe) < 0.0001, "Modus %s: mirror(mirror(p)) == p (p=%s)" % [SymmetryCore.mode_label(mode), probe]) _check(absf(once.y - probe.y) < 0.0001, "Modus %s: y bleibt unverändert" % SymmetryCore.mode_label(mode)) # Mitte bleibt Mitte (Punktspiegelung). var mid := Vector3(0.0, 5.0, 0.0) var mid_m := SymmetryCore.mirror_point(SymmetryCore.Mode.POINT, mid, center) _check(mid_m.distance_to(mid) < 0.0001, "Mitte bleibt unter Punktspiegelung fix") # Achse X: z unverändert; Achse Z: x unverändert. var px := SymmetryCore.mirror_point(SymmetryCore.Mode.AXIS_X, probe, center) _check(absf(px.z - probe.z) < 0.0001, "Achse X: z unverändert") var pz := SymmetryCore.mirror_point(SymmetryCore.Mode.AXIS_Z, probe, center) _check(absf(pz.x - probe.x) < 0.0001, "Achse Z: x unverändert") # OFF = Identität. _check(SymmetryCore.mirror_point(SymmetryCore.Mode.OFF, probe, center).distance_to(probe) < 0.0001, "OFF: Position unverändert") func _test_mirror_rot_double_identity() -> void: print("== Zweimal spiegeln = Ausgangslage (Drehung, modulo 360) ==") var rots := [0.0, 45.0, 90.0, 179.0, -90.0, 359.0, -359.0] for mode in [SymmetryCore.Mode.POINT, SymmetryCore.Mode.AXIS_X, SymmetryCore.Mode.AXIS_Z]: for r in rots: var r_in: float = r var once := SymmetryCore.mirror_rot_y(mode, r_in) var twice := SymmetryCore.mirror_rot_y(mode, once) var diff: float = absf(fposmod(twice - r_in + 180.0, 360.0) - 180.0) _check(diff < 0.0001, "Modus %s: rot %.1f → zweimal → gleich (mod 360)" % [ SymmetryCore.mode_label(mode), r_in]) # Einzelfälle laut Spec: Punkt 0° → 180°, Achse 90° → −90° (≡ 270°). _check(absf(SymmetryCore.mirror_rot_y(SymmetryCore.Mode.POINT, 0.0) - 180.0) < 0.0001, "Spec: Punkt 0° → 180°") _check(absf(fposmod(SymmetryCore.mirror_rot_y(SymmetryCore.Mode.AXIS_X, 90.0) + 90.0, 360.0)) < 0.0001, "Spec: Achse X 90° → −90° (≡ 270°)") _check(absf(SymmetryCore.mirror_rot_y(SymmetryCore.Mode.OFF, 42.0) - 42.0) < 0.0001, "OFF: Drehung unverändert") func _test_maps_to_self() -> void: print("== Achse/Zentrum: nicht gedoppelt ==") var center := Vector2.ZERO _check(SymmetryCore.maps_to_self(SymmetryCore.Mode.POINT, Vector3(0.01, 5.0, 0.01), center), "Punkt: Zentrum (±0.01 m) → maps_to_self") _check(not SymmetryCore.maps_to_self(SymmetryCore.Mode.POINT, Vector3(10.0, 5.0, 0.0), center), "Punkt: 10 m neben der Mitte → wird gedoppelt") _check(SymmetryCore.maps_to_self(SymmetryCore.Mode.AXIS_X, Vector3(0.0, 5.0, 123.0), center), "Achse X: auf der Achse (x = 0) → maps_to_self") _check(not SymmetryCore.maps_to_self(SymmetryCore.Mode.AXIS_X, Vector3(7.0, 5.0, 123.0), center), "Achse X: 7 m neben der Achse → wird gedoppelt") _check(SymmetryCore.maps_to_self(SymmetryCore.Mode.AXIS_Z, Vector3(5.0, 5.0, 0.0), center), "Achse Z: auf der Achse (z = 0) → maps_to_self") _check(SymmetryCore.maps_to_self(SymmetryCore.Mode.OFF, Vector3(9.0, 5.0, 9.0), center), "OFF: nichts wird gedoppelt → maps_to_self") func _test_cell_mapping() -> void: print("== Zell-Abbildung (Terrain-Raster) ==") var res := 9 # Punkt: (i,j) → (res−1−i, res−1−j); zweimal = Ausgangszelle. var c := SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 2, 7, res) _check(c == Vector2i(6, 1), "Punkt: (2,7) → (6,1) bei res=9") _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, c.x, c.y, res) == Vector2i(2, 7), "Punkt: zweimal spiegeln = Ausgangszelle") # Achse X: nur i spiegelt; Achse Z: nur j spiegelt. _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_X, 2, 7, res) == Vector2i(6, 7), "Achse X: (2,7) → (6,7)") _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_Z, 2, 7, res) == Vector2i(2, 1), "Achse Z: (2,7) → (2,1)") # Rastermitte (ungerade res): mittlere Zelle bleibt fix. var mid_i := (res - 1) / 2 _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, mid_i, mid_i, res) == Vector2i(mid_i, mid_i), "Rastermitte (ungerade res) bleibt unter Punktspiegelung fix") _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_X, mid_i, 3, res) == Vector2i(mid_i, 3), "Achse X: Mittelspalte bleibt fix") _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.AXIS_Z, 3, mid_i, res) == Vector2i(3, mid_i), "Achse Z: Mittelzeile bleibt fix") # Gerade res: keine fixe Zelle. _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 3, 3, 8) == Vector2i(4, 4), "Gerade res=8: (3,3) → (4,4)") # OFF + Degenerate (res < 2) → unverändert, kein Crash. _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.OFF, 1, 2, 9) == Vector2i(1, 2), "OFF: Zelle unverändert") _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 0, 0, 1) == Vector2i(0, 0), "res=1: Degenerate-Fall, kein Crash") _check(SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, 0, 0, 0) == Vector2i(0, 0), "res=0: Degenerate-Fall, kein Crash") func _test_partner_link() -> void: print("== Partner-Link (sym-Feld) ==") var obj := {"id": "baum", "pos": [1.0, 0.0, 2.0], "rot_y_deg": 0.0} _check(SymmetryCore.sym_of(obj) == "", "Objekt ohne sym → leere Kennung") _check(SymmetryCore.sym_of({"id": "x", "sym": 42}) == "", "Defektes sym (Zahl) → leer, kein Crash") var key := SymmetryCore.next_sym_key({"objects": []}) _check(key == "sym_1", "Erste freie Kennung = sym_1") SymmetryCore.set_sym(obj, key) _check(SymmetryCore.sym_of(obj) == key, "set_sym setzt die Kennung") _check(obj.has("sym"), "sym-Feld vorhanden") SymmetryCore.clear_sym(obj) _check(not obj.has("sym"), "clear_sym entfernt das Feld komplett (wie alte Karten)") var doc := {"objects": [ {"id": "a", "pos": [0.0, 0.0, 0.0], "sym": "sym_1"}, {"id": "b", "pos": [0.0, 0.0, 0.0]}, {"id": "c", "pos": [0.0, 0.0, 0.0], "sym": "sym_2"}, {"id": "bau_wand", "pos": [0.0, 0.0, 0.0], "sym": "sym_3"}, ]} _check(SymmetryCore.next_sym_key(doc) == "sym_4", "next_sym_key überspringt belegte Kennungen (Bauteile sind normale Objekte)") _check(SymmetryCore.find_partner_index(doc["objects"], "sym_2") == 2, "find_partner_index findet den Partner") _check(SymmetryCore.find_partner_index(doc["objects"], "sym_2", 2) == -1, "find_partner_index schließt den Index selbst aus") _check(SymmetryCore.find_partner_index(doc["objects"], "sym_3") == 3, "find_partner_index findet auch Bauteile") _check(SymmetryCore.find_partner_index(doc["objects"], "") == -1, "Leere Kennung → kein Partner") func _test_validator_sym() -> void: print("== Validator V16 (sym-Feld an Objekten) ==") var doc := SvmapDocument.create_new("SymTest", 1024.0, "mittag") doc["objects"] = [{ "id": "air_defense_001", "pos": [100.0, 0.0, 100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_1", }] var errors := SvmapIO._validate_core(doc) _check(errors.is_empty(), "Objekt mit gültiger sym-Kennung → keine Fehler (%s)" % [str(errors)]) # sym = kein String → V16. doc["objects"][0]["sym"] = 42 errors = SvmapIO._validate_core(doc) _check(_errors_contain(errors, "V16") and _errors_contain(errors, "muss ein String sein"), "sym als Zahl → V16 'muss ein String sein'") # sym leer → V16. doc["objects"][0]["sym"] = " " errors = SvmapIO._validate_core(doc) _check(_errors_contain(errors, "V16") and _errors_contain(errors, "nicht leer"), "sym leer/Whitespace → V16 'darf nicht leer sein'") # sym zu lang → V16. doc["objects"][0]["sym"] = "s".repeat(SvmapIO.SYM_KEY_MAX_LEN + 1) errors = SvmapIO._validate_core(doc) _check(_errors_contain(errors, "V16") and _errors_contain(errors, "länger"), "sym > %d Zeichen → V16" % SvmapIO.SYM_KEY_MAX_LEN) # Drei Objekte mit derselben Kennung → V16 (Paar = genau 2). doc["objects"] = [ {"id": "air_defense_001", "pos": [100.0, 0.0, 100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_9"}, {"id": "air_defense_001", "pos": [-100.0, 0.0, -100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_9"}, {"id": "air_defense_001", "pos": [0.0, 0.0, 100.0], "rot_y_deg": 0.0, "scale": 1.0, "sym": "sym_9"}, ] errors = SvmapIO._validate_core(doc) _check(_errors_contain(errors, "V16") and _errors_contain(errors, "genau 2"), "3 Objekte mit gleicher Kennung → V16 'genau 2'") # Zwei Objekte (echtes Paar) → ok. doc["objects"].remove_at(2) errors = SvmapIO._validate_core(doc) _check(errors.is_empty(), "2 Objekte mit gleicher Kennung (Paar) → keine Fehler") func _test_roundtrip_with_sym() -> void: print("== Roundtrip: speichern → laden → sym bleibt ==") var doc := SvmapDocument.create_new("SymRT", 1024.0, "abend") doc["objects"] = [ {"id": "air_defense_001", "pos": [100.0, 0.0, 100.0], "rot_y_deg": 45.0, "scale": 1.0, "sym": "sym_1"}, {"id": "air_defense_001", "pos": [-100.0, 0.0, -100.0], "rot_y_deg": 225.0, "scale": 1.0, "sym": "sym_1"}, ] doc["gebaeude"] = [ {"id": "wand", "pos": [2.0, 0.0, 4.0], "rot": 90.0, "punkte": [], "sym": "sym_2"}, {"id": "wand", "pos": [-2.0, 0.0, -4.0], "rot": 270.0, "punkte": [], "sym": "sym_2"}, ] var save := SvmapIO.save_to_file("user://test_sym.svmap", doc) _check(save["ok"], "save_to_file mit sym ok: %s" % str(save["errors"])) var load := SvmapIO.load_from_file("user://test_sym.svmap") _check(load["ok"], "load_from_file mit sym ok: %s" % str(load["errors"])) var doc2: Dictionary = load["data"] _check((doc2["objects"] as Array).size() == 2, "2 Objekte im Roundtrip") _check(str(doc2["objects"][0]["sym"]) == "sym_1", "Objekt-sym im Roundtrip erhalten") _check(str(doc2["objects"][1]["sym"]) == "sym_1", "Partner-sym im Roundtrip erhalten") _check((doc2["gebaeude"] as Array).size() == 2, "2 Bau-Teile im Roundtrip") _check(str(doc2["gebaeude"][0]["sym"]) == "sym_2", "Bau-Teil-sym im Roundtrip erhalten") # Re-Save → byte-identisch (Kanonisierung stabil). var save2 := SvmapIO.save_to_file("user://test_sym2.svmap", doc2) _check(save2["ok"], "Re-Save ok") var t1 := FileAccess.get_file_as_string("user://test_sym.svmap") var t2 := FileAccess.get_file_as_string("user://test_sym2.svmap") _check(t1 == t2, "Re-Save ist byte-identisch (Prüfsumme über sym stabil)") func _test_legacy_map_unchanged() -> void: print("== Alte Karte (ohne sym) lädt unverändert ==") var doc := SvmapDocument.create_new("Legacy", 512.0, "nacht") doc["objects"] = [ {"id": "air_defense_001", "pos": [50.0, 0.0, 50.0], "rot_y_deg": 10.0, "scale": 1.0}, ] var save := SvmapIO.save_to_file("user://test_sym_legacy.svmap", doc) _check(save["ok"], "Legacy-save ok: %s" % str(save["errors"])) var legacy_text := FileAccess.get_file_as_string("user://test_sym_legacy.svmap") var load := SvmapIO.load_from_file("user://test_sym_legacy.svmap") _check(load["ok"], "Legacy-load ok") var doc2: Dictionary = load["data"] _check((doc2["objects"] as Array).size() == 1, "Legacy: 1 Objekt") _check(not doc2["objects"][0].has("sym"), "Legacy: kein sym-Feld entstanden") # Roundtrip byte-identisch → alte Karten speichern sich unverändert weiter. var save2 := SvmapIO.save_to_file("user://test_sym_legacy2.svmap", doc2) _check(save2["ok"], "Legacy re-save ok") var text2 := FileAccess.get_file_as_string("user://test_sym_legacy2.svmap") _check(legacy_text == text2, "Legacy: speichern→laden→speichern byte-identisch")