scopedvr-map-editor/tests/test_mal_fein.gd
slyphax 4a7b99ee3e Malen: Regler „Kante hart ↔ weich“ + Pinsel ab 1 m
Sly 25.10.: harte Kanten an Strichrändern, Pinsel auf 1 m, Regler für die
Kantenschärfe.
- apply_material_brush(…, weichheit): im äußeren Anteil des Radius wird
  gestreut ausgedünnt (fester Hash je Punkt → deterministisch, Nachmalen
  füllt nicht auf); der Shader macht daraus einen ausfransenden Übergang.
  Kein Formatwechsel (ein Material je Punkt bleibt).
- Panel: Regler 0–100 % (Default 40 %), nur im Malen-Modus; Pinselgröße ab 1 m.
- test_mal_fein 26/26 (Dichte fällt nach außen, deterministisch, 1-m-Pinsel),
  test_terrain_mirror an die Tool-Weichheit angepasst.

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

180 lines
7.1 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): 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. 0 = voller Kreis, weich = Innenkern voll,
## Rand gestreut, Ausdünnung nimmt nach außen zu, deterministisch.
func _f8_weich() -> void:
var r := 1025
var hart := PackedByteArray()
hart.resize(r * r)
TerrainField.apply_material_brush(hart, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.0)
var weich := PackedByteArray()
weich.resize(r * r)
TerrainField.apply_material_brush(weich, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
var weich2 := PackedByteArray()
weich2.resize(r * r)
TerrainField.apply_material_brush(weich2, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
var n_h := _count(hart)
var n_w := _count(weich)
_check(n_w < n_h and n_w > n_h / 4, "F8 weich malt weniger Punkte als hart (%d vs %d)" % [n_w, n_h])
_check(weich == weich2, "F8 deterministisch (gleicher Strich = gleiches Ergebnis)")
# Ring-Dichten: innen (d<2) voll, Mitte (4..6) teils, außen (8..10) dünn
var d_in := _ring(weich, r, 0.0, 2.0)
var d_mid := _ring(weich, r, 4.0, 6.0)
var d_out := _ring(weich, r, 8.0, 10.0)
_check(d_in > 0.99 and d_mid > d_out and d_mid < 1.0 and d_out < 0.5,
"F8 Dichte fällt nach außen (innen %.2f, Mitte %.2f, Rand %.2f)" % [d_in, d_mid, d_out])
var eins := PackedByteArray()
eins.resize(r * r)
TerrainField.apply_material_brush(eins, r, 1024.0, 1024.0, 0.3, 0.2, 1.0, 1, 0.0)
_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))