scopedvr-map-editor/tests/test_symmetry_core.gd

300 lines
14 KiB
GDScript
Raw 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_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": [], "gebaeude": []})
_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"},
], "gebaeude": [
{"id": "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 über BEIDE Blöcke")
_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["gebaeude"], "sym_3") == 0,
"find_partner_index funktioniert auch im gebaeude-Block")
_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")