scopedvr-map-editor/tests/test_mal_fein.gd
slyphax 4e973795f6 Malen: echter weicher Übergang am Pinselrand (Misch-Schicht statt Streuung)
Sly 25.10.: „weich“ soll ein Textur-Verlauf am Pinselrand sein, nicht fleckig;
gerade Strichkanten wirkten weiter hart (1-m-Übergang).
- Format (optional, alte Karten gültig): materials_b_r8_b64 (zweites Material
  je Punkt) + materials_w_r8_b64 (Anteil 0–255); ohne Mischung fallen die
  Felder weg. V15 prüft Länge/IDs/Paarigkeit.
- TerrainField.apply_material_brush_mix: innen voll, außen Smoothstep-Anteil;
  nur verstärken, nie abschwächen; Gras drübermalen mischt zurück.
- Shader: Top-2 aus 4 Nachbarpunkten × (a, b) mit Anteilen → nur noch 2
  Albedo-Fetches; Warp bleibt.
- Pinsel/Live-Vorschau, Spiegeln, Symmetrie-Analyse/-Anwenden, Resize,
  Anheben, Undo (terrain-Snapshot) tragen die Misch-Schicht mit.
- Streu-Rand (vorheriger Commit) entfernt.
- Tests: test_mal_fein 30/30 (Verlauf fällt weich, nie abschwächen,
  zurückmischen, Kodierung), Editor-Suite unverändert grün bis auf die
  bekannten Umgebungs-Tests; Kette Editor → Server → Client per
  run_map_download_e2e 15/15 mit gemischter Karte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 00:43:26 +02:00

198 lines
8 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): feines Material-Raster (Karte 24.10., Sly: „feiner malen“).
## godot --headless --path . -s res://tests/test_mal_fein.gd
##
## F1 Ziel-Raster: 1024 m/129 → 1025 (1 m), 256 m/129 → 257 (1 m), 2048 m → 1025.
## F2 Neue Map startet mit feinem Raster und ist sofort gültig (V1–V16).
## F3 Alte Karte (Raster = Höhenraster) wird beim Anheben optisch identisch
## übernommen und ist danach gültig.
## F4 Pinsel 2 m auf 1024-m-Map ändert nur ~π·2² Punkte (nicht eine 8-m-Zelle).
## F5 V15 lehnt kaputte material_resolution / falsche Länge ab.
## F6 Speichern → Laden → identische Schicht (Roundtrip mit Prüfsumme).
## F7 Resize (1024 → 2048 m) hält die gemalte Stelle an derselben Weltposition.
var failures := 0
var checks := 0
func _initialize() -> void:
_f1()
_f2_f4_f6_f7()
_f3()
_f5()
_f8_weich()
print("test_mal_fein: %d/%d ok" % [checks - failures, checks])
quit(1 if failures > 0 else 0)
func _check(cond: bool, label: String) -> void:
checks += 1
print("%s: %s" % ["ok " if cond else "FAIL", label])
if not cond:
failures += 1
func _f1() -> void:
_check(SvmapIO.material_resolution_for(129, 1024.0, 1024.0) == 1025, "F1 1024 m/129 → 1025")
_check(SvmapIO.material_resolution_for(129, 256.0, 256.0) == 257, "F1 256 m/129 → 257")
_check(SvmapIO.material_resolution_for(129, 2048.0, 2048.0) == 1025, "F1 2048 m → gedeckelt 1025")
_check(SvmapIO.material_resolution_for(129, 64.0, 64.0) == 129, "F1 kleine Map bleibt beim Höhenraster")
func _f2_f4_f6_f7() -> void:
var doc := SvmapDocument.create_new("Fein", 1024.0, "mittag")
var t: Dictionary = doc["terrain"]
_check(SvmapIO.material_resolution(t) == 1025, "F2 neue Map: material_resolution 1025")
_check(SvmapIO._validate_core(doc).is_empty(), "F2 neue Map gültig (%s)" % str(SvmapIO._validate_core(doc)))
var mats := SvmapIO.decode_materials(t)
_check(mats.size() == 1025 * 1025, "F2 Schicht hat 1025² Punkte")
# F4: Pinsel 2 m Sand mitten auf die Map
var changed := TerrainField.apply_material_brush(mats, 1025, 1024.0, 1024.0, 10.3, -7.6, 2.0, 1)
var n := 0
for v in mats:
if v == 1:
n += 1
_check(changed and n >= 8 and n <= 20, "F4 2-m-Pinsel ändert %d Punkte (≈ π·2² = 12,6; alt: 1 Zelle à 8 m)" % n)
t["materials_r8_b64"] = SvmapIO.encode_materials(mats)
# F6: Roundtrip
var path := "user://test_mal_fein.svmap"
var sv := SvmapIO.save_to_file(path, doc)
_check(bool(sv["ok"]), "F6 speichern ok (%s)" % str(sv.get("errors", [])))
var ld := SvmapIO.load_from_file(path)
_check(bool(ld["ok"]), "F6 laden ok (%s)" % str(ld.get("errors", [])))
if bool(ld["ok"]):
var m2 := SvmapIO.decode_materials(ld["data"]["terrain"])
_check(m2 == mats, "F6 Schicht bitgleich nach Roundtrip")
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
# F7: Resize auf 2048 m — Sand bleibt bei (10.3, -7.6)
var big := SvmapDocument.resize_terrain(doc, 2048.0, 2048.0)
var bt: Dictionary = big["terrain"]
var bm := SvmapIO.material_resolution(bt)
var bmats := SvmapIO.decode_materials(bt)
var gx := int(roundf((10.3 / 2048.0 + 0.5) * float(bm - 1)))
var gz := int(roundf((-7.6 / 2048.0 + 0.5) * float(bm - 1)))
_check(bmats.size() == bm * bm and int(bmats[gz * bm + gx]) == 1,
"F7 Resize 2048 m (Raster %d): Sand an derselben Weltstelle" % bm)
_check(SvmapIO._validate_core(big).is_empty(),
"F7 Resize-Ergebnis gültig")
func _f3() -> void:
# Alte Karte simulieren: Material-Raster = Höhenraster (129), ohne Feld.
var doc := SvmapDocument.create_new("Alt", 1024.0, "mittag")
var t: Dictionary = doc["terrain"]
var old := PackedByteArray()
old.resize(129 * 129)
for i in old.size():
old[i] = (i * 7) % 20
t.erase("material_resolution")
t["materials_r8_b64"] = SvmapIO.encode_materials(old)
_check(SvmapIO._validate_core(doc).is_empty(), "F3 alte Karte (ohne Feld) gültig")
_check(SvmapIO.upgrade_material_layer(t), "F3 Anheben meldet Änderung")
_check(SvmapIO.material_resolution(t) == 1025, "F3 danach Raster 1025")
var m := SvmapIO.decode_materials(t)
var same := true
for z in 129:
for x in 129:
if SvmapIO.material_at_height_point(m, 1025, 129, x, z) != int(old[z * 129 + x]):
same = false
_check(same, "F3 jeder Höhenpunkt behält sein Material (optisch identisch)")
_check(SvmapIO._validate_core(doc).is_empty(), "F3 angehobene Karte gültig")
_check(not SvmapIO.upgrade_material_layer(t), "F3 zweites Anheben ändert nichts")
func _f5() -> void:
var doc := SvmapDocument.create_new("Kaputt", 1024.0, "mittag")
var t: Dictionary = doc["terrain"]
t["material_resolution"] = 1000 # (1000−1) kein Vielfaches von 128
_check(_has_v15(SvmapIO._validate_core(doc)), "F5 material_resolution nicht auf dem Raster → V15")
t["material_resolution"] = 2049
_check(_has_v15(SvmapIO._validate_core(doc)), "F5 material_resolution > 1025 → V15")
t["material_resolution"] = 1025
var short := PackedByteArray()
short.resize(129 * 129)
t["materials_r8_b64"] = SvmapIO.encode_materials(short)
_check(_has_v15(SvmapIO._validate_core(doc)), "F5 Schichtlänge passt nicht → V15")
func _has_v15(errs: PackedStringArray) -> bool:
for e in errs:
if str(e).begins_with("V15"):
return true
return false
func _only_checksum(errs: PackedStringArray) -> bool:
for e in errs:
if not str(e).begins_with("V13"):
return false
return true
## F8 (25.10.): Kante hart ↔ weich = ECHTER Verlauf (Misch-Schicht b + Anteil w).
func _f8_weich() -> void:
var r := 1025
var a := PackedByteArray()
a.resize(r * r)
var b := PackedByteArray()
b.resize(r * r)
var w := PackedByteArray()
w.resize(r * r)
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
var c := (r - 1) / 2
var mitte := c * r + c
_check(a[mitte] == 1 and w[mitte] == 0, "F8 Pinselmitte voll Sand")
var anteil := func(i: int) -> float:
return (float(255 - w[i]) / 255.0 if a[i] == 1 else 0.0) + (float(w[i]) / 255.0 if b[i] == 1 else 0.0)
var s4: float = anteil.call(c * r + c + 4)
var s7: float = anteil.call(c * r + c + 7)
var s9: float = anteil.call(c * r + c + 9)
_check(s4 > s7 and s7 > s9 and s9 > 0.0 and s4 < 1.0,
"F8 Sand-Anteil fällt weich nach außen (4 m %.2f, 7 m %.2f, 9 m %.2f)" % [s4, s7, s9])
_check(anteil.call(c * r + c + 11) == 0.0, "F8 außerhalb des Radius unberührt")
# Nachmalen schwächer am Rand darf nichts abschwächen
var vorher := w.duplicate()
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 3.0, 0.0, 10.0, 1, 0.8)
_check(anteil.call(c * r + c + 7) >= s7, "F8 Nachmalen verstärkt, schwächt nie ab")
# Rückmalen mit Gras (Strg+Klick) am Rand mischt zurück
var vor4: float = anteil.call(c * r + c + 4)
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 10.0, 0.0, 10.0, 0, 0.8)
var nach4: float = anteil.call(c * r + c + 4)
_check(nach4 < vor4 and nach4 > 0.0, "F8 Gras drübermalen mischt Sand zurück (%.2f → %.2f)" % [vor4, nach4])
# Kodierung: ohne Mischung keine Felder, mit Mischung gültig
var doc := SvmapDocument.create_new("Mix", 1024.0, "mittag")
var t: Dictionary = doc["terrain"]
t["materials_r8_b64"] = SvmapIO.encode_materials(a)
SvmapIO.encode_mix(t, b, w)
_check(t.has("materials_w_r8_b64") and SvmapIO._validate_core(doc).is_empty(),
"F8 Misch-Schicht gespeichert und gültig")
var leer := PackedByteArray()
leer.resize(r * r)
SvmapIO.encode_mix(t, leer, leer)
_check(not t.has("materials_w_r8_b64"), "F8 ohne Mischung keine Zusatzfelder")
var eins := PackedByteArray()
eins.resize(r * r)
TerrainField.apply_material_brush(eins, r, 1024.0, 1024.0, 0.3, 0.2, 1.0, 1)
_check(_count(eins) >= 1 and _count(eins) <= 5, "F8 1-m-Pinsel trifft %d Punkte" % _count(eins))
func _count(m: PackedByteArray) -> int:
var n := 0
for v in m:
if v != 0:
n += 1
return n
func _ring(m: PackedByteArray, r: int, a: float, b: float) -> float:
var c := (r - 1) / 2
var tot := 0
var hit := 0
for z in range(c - 12, c + 13):
for x in range(c - 12, c + 13):
var d := Vector2(x - c, z - c).length()
if d >= a and d < b:
tot += 1
if m[z * r + x] != 0:
hit += 1
return float(hit) / float(maxi(tot, 1))