499 lines
No EOL
20 KiB
GDScript
499 lines
No EOL
20 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): M2 Terrain spiegeln (Karte t_f9eff526, Spec
|
||
## /opt/data/scopedvr/KARTEN_MIRROR_vc27.md) — im Symmetrie-Modus schreibt
|
||
## jeder Terrain-Strich (Heben/Senken/Glätten/Malen) seine Zellen EXAKT
|
||
## gespiegelt in die andere Kartenseite.
|
||
## Ausführen: godot --headless --path . --script res://tests/test_terrain_mirror.gd
|
||
## Exit 0 = alles grün.
|
||
##
|
||
## Geprüft wird (Spec-Abnahme):
|
||
## 1. Strich in einer Hälfte → beide Zellhälften identisch (Höhe UND Material)
|
||
## für alle drei Modi (Punkt / Achse X / Achse Z) und alle vier Pinsel.
|
||
## 2. Strich genau auf der Rastermitte bzw. auf der Spiegelachse dupliziert
|
||
## NICHT — das Ergebnis ist byte-identisch zu EINER Pinsel-Anwendung
|
||
## (Referenz über TerrainField direkt, Gegenprobe gegen den Primitive).
|
||
## 3. Strich am Rand: Ränder bleiben exakt gespiegelt, kein Absturz.
|
||
## 4. Modus AUS = Verhalten wie vorher: byte-identisches Ergebnis zur
|
||
## direkten TerrainField-Anwendung (Gegenprobe; zusätzlich wird der
|
||
## gleiche Probe-Strich gegen einen origin/main-Checkout gefahren —
|
||
## siehe /opt/data/scopedvr/work/m2_beweise/).
|
||
## 5. Live-Rebuild-Pfad: die per _process an update_terrain gegebenen
|
||
## Mesh-Inputs (Höhen UND Material-Schicht) sind bereits gespiegelt.
|
||
## 6. Ein Strich = EIN Undo-Schritt, Undo stellt BEIDE Hälften zurück.
|
||
## 7. Zellgleichheit auch wenn der Pinsel-Fußabdruck die Mitte ÜBERLAPPT
|
||
## (Strich-nahe Seite gewinnt, deterministisch).
|
||
|
||
const RES := 33
|
||
const SIZE := 320.0
|
||
const HMIN := 0.0
|
||
const HMAX := 60.0
|
||
const STRENGTH := 8.0 # terrain_tool-Default (m/s Heben/Senken)
|
||
const STROKE_SEC := 0.5 # Strichlänge der Proben
|
||
const RADIUS := 30.0
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
|
||
|
||
func _init() -> void:
|
||
_run_tests()
|
||
if failures == 0:
|
||
print("ALL OK: %d checks" % checks)
|
||
quit(0)
|
||
else:
|
||
print("FAILED: %d/%d checks" % [failures, checks])
|
||
quit(1)
|
||
|
||
|
||
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_half_stroke_all_modes()
|
||
_test_center_stroke_no_duplicate()
|
||
_test_axis_stroke_no_duplicate()
|
||
_test_edge_stroke()
|
||
_test_overlap_stroke_near_center()
|
||
_test_off_mode_unchanged()
|
||
_test_live_rebuild_inputs()
|
||
_test_undo_one_step()
|
||
|
||
|
||
# ------------------------------------------------------------- Hilfsbau -----
|
||
|
||
class ToolMain:
|
||
var doc: Dictionary = {}
|
||
var sym_mode: int = SymmetryCore.Mode.OFF
|
||
var history: ActionHistory = ActionHistory.new()
|
||
var commits := 0
|
||
|
||
func _set_status(_text: String, _err := false) -> void:
|
||
pass
|
||
|
||
func _on_terrain_stroke_committed() -> void:
|
||
commits += 1
|
||
|
||
|
||
class ToolBuilder:
|
||
var last_heights := PackedFloat32Array()
|
||
var last_mats := PackedByteArray()
|
||
var calls := 0
|
||
|
||
func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedByteArray()) -> void:
|
||
last_heights = heights
|
||
last_mats = mats
|
||
calls += 1
|
||
|
||
|
||
## Kleine Test-Map (33² über 320 m, flach, alles Gras) — schnell genug für
|
||
## Voll-Raster-Vergleiche, mit Rastermitte (ungerade res → Selbstabbildung).
|
||
func _make_doc() -> Dictionary:
|
||
var doc := SvmapDocument.create_new("Mirror", SIZE, "mittag")
|
||
var terrain: Dictionary = doc["terrain"]
|
||
terrain["resolution"] = RES
|
||
terrain["size_x_m"] = SIZE
|
||
terrain["size_z_m"] = SIZE
|
||
terrain["min_height_m"] = HMIN
|
||
terrain["max_height_m"] = HMAX
|
||
var flat := PackedFloat32Array()
|
||
flat.resize(RES * RES)
|
||
terrain["heightmap_r16_b64"] = TerrainField.encode(flat, HMIN, HMAX)
|
||
var mats := PackedByteArray()
|
||
mats.resize(RES * RES)
|
||
terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats)
|
||
return doc
|
||
|
||
|
||
func _make_tool(sym: int) -> Array:
|
||
var doc := _make_doc()
|
||
var tmain := ToolMain.new()
|
||
tmain.doc = doc
|
||
tmain.sym_mode = sym
|
||
tmain.history.begin(doc)
|
||
var builder := ToolBuilder.new()
|
||
var ttool := Node3D.new()
|
||
ttool.set_script(load("res://src/editor/terrain_tool.gd"))
|
||
ttool.setup(tmain, Camera3D.new(), builder, null)
|
||
var activated: bool = ttool.call("activate")
|
||
_check(activated, "terrain_tool aktiviert (Sym-Modus %d)" % sym)
|
||
ttool.call("set_radius", RADIUS)
|
||
return [ttool, tmain, builder, doc]
|
||
|
||
|
||
## Programmgesteuerter Strich über den ECHTEN Pfad (demo_stroke: begin →
|
||
## Schritte → end → Commit → Refresh — exakt wie ein Maus-Strich).
|
||
func _stroke(ttool, x: float, z: float, mode: int, seconds: float) -> void:
|
||
ttool.call("demo_stroke", x, z, mode, seconds)
|
||
|
||
|
||
func _decode_heights(doc: Dictionary) -> PackedFloat32Array:
|
||
return SvmapIO.decode_heights(doc["terrain"])
|
||
|
||
|
||
func _decode_mats(doc: Dictionary) -> PackedByteArray:
|
||
return SvmapIO.decode_materials(doc["terrain"])
|
||
|
||
|
||
## Anzahl Zellpaare, deren gespiegelte Schwester einen ANDEREN Wert trägt
|
||
## (0 = beide Hälften exakt zellgleich). Höhe.
|
||
func _count_asym_heights(heights: PackedFloat32Array, mode: int) -> int:
|
||
var bad := 0
|
||
for z: int in RES:
|
||
for x: int in RES:
|
||
var mc: Vector2i = SymmetryCore.mirror_cell(mode, x, z, RES)
|
||
if heights[z * RES + x] != heights[mc.y * RES + mc.x]:
|
||
bad += 1
|
||
return bad
|
||
|
||
|
||
## Anzahl Zellpaare, deren gespiegelte Schwester ein ANDERES Material trägt.
|
||
func _count_asym_mats(mats: PackedByteArray, mode: int) -> int:
|
||
var bad := 0
|
||
for z: int in RES:
|
||
for x: int in RES:
|
||
var mc: Vector2i = SymmetryCore.mirror_cell(mode, x, z, RES)
|
||
if int(mats[z * RES + x]) != int(mats[mc.y * RES + mc.x]):
|
||
bad += 1
|
||
return bad
|
||
|
||
|
||
## Referenz: N Einzelschritte DIREKT über TerrainField (der Primitive vor M2) —
|
||
## für die Gegenproben „Mitte dupliziert nicht" und „AUS wie vorher".
|
||
func _reference_heights(steps: int, x: float, z: float, mode: int) -> PackedFloat32Array:
|
||
var ref := PackedFloat32Array()
|
||
ref.resize(RES * RES)
|
||
var dt := STROKE_SEC / float(maxi(steps, 1))
|
||
var amount := STRENGTH * dt
|
||
for i in steps:
|
||
TerrainField.apply_brush(ref, RES, SIZE, SIZE, -100000.0, 100000.0,
|
||
x, z, RADIUS, mode, amount)
|
||
return ref
|
||
|
||
|
||
# --------------------------------------------------------------- Tests ------
|
||
|
||
## 1) Strich in einer Hälfte → beide Zellhälften exakt identisch (Höhe UND
|
||
## Material) für alle drei Symmetrie-Modi und alle vier Pinsel-Modi.
|
||
func _test_half_stroke_all_modes() -> void:
|
||
print("Testgruppe: Strich in einer Hälfte → Hälften zellgleich (3 Sym-Modi × 4 Pinsel)")
|
||
var sym_modes := [
|
||
["Punkt", SymmetryCore.Mode.POINT],
|
||
["Achse X", SymmetryCore.Mode.AXIS_X],
|
||
["Achse Z", SymmetryCore.Mode.AXIS_Z],
|
||
]
|
||
var brush_modes := [
|
||
["Heben", TerrainField.Mode.RAISE],
|
||
["Senken", TerrainField.Mode.LOWER],
|
||
["Malen", TerrainField.Mode.PAINT],
|
||
]
|
||
for entry0 in sym_modes:
|
||
var sym_name: String = entry0[0]
|
||
var sym: int = entry0[1]
|
||
for entry1 in brush_modes:
|
||
var brush_name: String = entry1[0]
|
||
var brush: int = entry1[1]
|
||
var parts := _make_tool(sym)
|
||
var ttool = parts[0]
|
||
var doc: Dictionary = parts[3]
|
||
if brush == TerrainField.Mode.PAINT:
|
||
ttool.call("set_paint_material", 1) # Sand statt Default-Gras
|
||
_stroke(ttool, -100.0, -60.0, brush, STROKE_SEC)
|
||
var heights := _decode_heights(doc)
|
||
var mats := _decode_mats(doc)
|
||
_check(_count_asym_heights(heights, sym) == 0,
|
||
"%s/%s: Höhen-Hälften exakt zellgleich" % [sym_name, brush_name])
|
||
_check(_count_asym_mats(mats, sym) == 0,
|
||
"%s/%s: Material-Hälften exakt zellgleich" % [sym_name, brush_name])
|
||
var moved := false
|
||
if brush == TerrainField.Mode.PAINT:
|
||
var pm := _decode_mats(doc)
|
||
for m in pm:
|
||
if int(m) != 0:
|
||
moved = true
|
||
break
|
||
else:
|
||
for v in heights:
|
||
if v != 0.0:
|
||
moved = true
|
||
break
|
||
_check(moved, "%s/%s: Strich hat tatsächlich gewirkt" % [sym_name, brush_name])
|
||
ttool.free()
|
||
# Malen (Sand) als eigener Material-Strich pro Sym-Modus:
|
||
var parts_p := _make_tool(sym)
|
||
var ttool_p = parts_p[0]
|
||
var doc_p: Dictionary = parts_p[3]
|
||
ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras
|
||
_stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3)
|
||
var mats_p := _decode_mats(doc_p)
|
||
var sand := 0
|
||
for m in mats_p:
|
||
if int(m) == 1:
|
||
sand += 1
|
||
_check(sand > 0, "%s/Malen: Sand gemalt (%d Zellen)" % [sym_name, sand])
|
||
_check(_count_asym_mats(mats_p, sym) == 0,
|
||
"%s/Malen: Material-Hälften exakt zellgleich (Sand)" % sym_name)
|
||
var painted_heights := _decode_heights(doc_p)
|
||
var flat := true
|
||
for v in painted_heights:
|
||
if v != 0.0:
|
||
flat = false
|
||
break
|
||
_check(flat, "%s/Malen: Höhe bleibt unangetastet" % sym_name)
|
||
ttool_p.free()
|
||
# Glätten braucht Form unter dem Pinsel — symmetrisch vorgeformter Hügel:
|
||
for entry in sym_modes:
|
||
var sym_name2: String = entry[0]
|
||
var sym2: int = entry[1]
|
||
var parts_s := _make_tool(sym2)
|
||
var ttool_s = parts_s[0]
|
||
var tmain_s = parts_s[1]
|
||
var doc_s: Dictionary = parts_s[3]
|
||
var terrain: Dictionary = doc_s["terrain"]
|
||
var hs := _decode_heights(doc_s)
|
||
var hill := Vector3(-70.0, 0.0, -40.0)
|
||
var center := SymmetryCore.map_center(doc_s)
|
||
var twin := SymmetryCore.mirror_point(sym2, hill, center)
|
||
TerrainField.apply_brush(hs, RES, SIZE, SIZE, -100000.0, 100000.0,
|
||
hill.x, hill.z, 25.0, TerrainField.Mode.RAISE, 10.0)
|
||
TerrainField.apply_brush(hs, RES, SIZE, SIZE, -100000.0, 100000.0,
|
||
twin.x, twin.z, 25.0, TerrainField.Mode.RAISE, 10.0)
|
||
terrain["heightmap_r16_b64"] = TerrainField.encode(hs, HMIN, HMAX)
|
||
ttool_s.call("refresh_from_doc")
|
||
var before := _decode_heights(doc_s)
|
||
_stroke(ttool_s, -52.0, -28.0, TerrainField.Mode.SMOOTH, STROKE_SEC)
|
||
var after := _decode_heights(doc_s)
|
||
var smoothed := false
|
||
for i in after.size():
|
||
if after[i] != before[i]:
|
||
smoothed = true
|
||
break
|
||
_check(smoothed, "%s/Glätten: Strich hat geglättet" % sym_name2)
|
||
_check(_count_asym_heights(after, sym2) == 0,
|
||
"%s/Glätten: Höhen-Hälften exakt zellgleich" % sym_name2)
|
||
ttool_s.free()
|
||
|
||
# Undo-/Commit-Verhalten testet _test_undo_one_step (Strich-Summe oben OK).
|
||
|
||
|
||
## 2) Strich GENAU auf der Rastermitte (Punkt) dupliziert NICHT: Ergebnis ist
|
||
## byte-identisch zu EINER Pinsel-Anwendung pro Schritt (Referenz).
|
||
func _test_center_stroke_no_duplicate() -> void:
|
||
print("Testgruppe: Strich genau auf der Mitte dupliziert nicht (Punkt)")
|
||
var parts := _make_tool(SymmetryCore.Mode.POINT)
|
||
var ttool = parts[0]
|
||
var doc: Dictionary = parts[3]
|
||
_stroke(ttool, 0.0, 0.0, TerrainField.Mode.RAISE, STROKE_SEC)
|
||
var steps := int(ceil(STROKE_SEC / 0.05))
|
||
var ref := _reference_heights(steps, 0.0, 0.0, TerrainField.Mode.RAISE)
|
||
var ref_b64 := TerrainField.encode(ref, HMIN, HMAX)
|
||
var got_b64 := str(doc["terrain"]["heightmap_r16_b64"])
|
||
_check(got_b64 == ref_b64,
|
||
"Mitte-Strich (Punkt): byte-identisch zu EINER Anwendung (kein Doppel-Heben)")
|
||
var heights := _decode_heights(doc)
|
||
_check(_count_asym_heights(heights, SymmetryCore.Mode.POINT) == 0,
|
||
"Mitte-Strich (Punkt): Hälften exakt zellgleich")
|
||
var center_h := heights[(RES / 2) * RES + (RES / 2)]
|
||
_check(absf(center_h - (STRENGTH * STROKE_SEC)) < 0.05,
|
||
"Mitte-Zelle exakt EINmal gehoben (%.3f m statt %.3f m)" % [
|
||
center_h, 2.0 * STRENGTH * STROKE_SEC])
|
||
ttool.free()
|
||
|
||
|
||
## 2b) Strich GENAU auf der Spiegelachse dupliziert nicht (Achse X und Z).
|
||
func _test_axis_stroke_no_duplicate() -> void:
|
||
print("Testgruppe: Strich auf der Spiegelachse dupliziert nicht (Achse X/Z)")
|
||
var cases := [
|
||
["Achse X", SymmetryCore.Mode.AXIS_X, Vector2(0.0, -80.0)],
|
||
["Achse Z", SymmetryCore.Mode.AXIS_Z, Vector2(80.0, 0.0)],
|
||
]
|
||
for case in cases:
|
||
var sym_name: String = case[0]
|
||
var sym: int = case[1]
|
||
var at: Vector2 = case[2]
|
||
var parts := _make_tool(sym)
|
||
var ttool = parts[0]
|
||
var doc: Dictionary = parts[3]
|
||
_stroke(ttool, at.x, at.y, TerrainField.Mode.RAISE, STROKE_SEC)
|
||
var steps := int(ceil(STROKE_SEC / 0.05))
|
||
var ref := _reference_heights(steps, at.x, at.y, TerrainField.Mode.RAISE)
|
||
var ref_b64 := TerrainField.encode(ref, HMIN, HMAX)
|
||
_check(str(doc["terrain"]["heightmap_r16_b64"]) == ref_b64,
|
||
"%s: Strich auf der Achse byte-identisch zu EINER Anwendung" % sym_name)
|
||
var heights := _decode_heights(doc)
|
||
_check(_count_asym_heights(heights, sym) == 0,
|
||
"%s: Hälften exakt zellgleich" % sym_name)
|
||
ttool.free()
|
||
|
||
|
||
## 3) Strich am Rand: gespiegelte Randzellen exakt, gegenüberliegende Ecke
|
||
## bekommt den Zwilling, kein Absturz, kein Duplikat-Effekt.
|
||
func _test_edge_stroke() -> void:
|
||
print("Testgruppe: Strich am Rand (Ecke, Punkt-Modus)")
|
||
var parts := _make_tool(SymmetryCore.Mode.POINT)
|
||
var ttool = parts[0]
|
||
var doc: Dictionary = parts[3]
|
||
_stroke(ttool, 150.0, 150.0, TerrainField.Mode.RAISE, STROKE_SEC)
|
||
var heights := _decode_heights(doc)
|
||
_check(_count_asym_heights(heights, SymmetryCore.Mode.POINT) == 0,
|
||
"Rand-Strich: beide Hälften exakt zellgleich (auch Randzellen)")
|
||
var corner := heights[(RES - 1) * RES + (RES - 1)] # +X/+Z-Ecke
|
||
var opposite := heights[0] # −X/−Z-Ecke (Zwilling)
|
||
_check(corner > 0.5 and absf(corner - opposite) < 0.0001,
|
||
"Rand: Ecke %.3f m, Zwilling-Ecke %.3f m (identisch)" % [corner, opposite])
|
||
_check(absf(heights[(RES / 2) * RES + (RES / 2)]) < 0.0001,
|
||
"Rand: Rastermitte unberührt (Fußabdruck reicht nicht hin)")
|
||
ttool.free()
|
||
|
||
|
||
## 7) Fußabdruck ÜBERLAPPT die Mitte (Strich nahe dran): die strichnahe Seite
|
||
## gewinnt, das Ergebnis bleibt exakt zellgleich — auch am Selbstabbild-
|
||
## Punkt der ungeraden Rastermitte.
|
||
func _test_overlap_stroke_near_center() -> void:
|
||
print("Testgruppe: Überlapp-Strich nahe der Mitte (alle 3 Modi)")
|
||
var cases := [
|
||
["Punkt", SymmetryCore.Mode.POINT, Vector2(12.0, 12.0)],
|
||
["Achse X", SymmetryCore.Mode.AXIS_X, Vector2(12.0, -90.0)],
|
||
["Achse Z", SymmetryCore.Mode.AXIS_Z, Vector2(-90.0, 12.0)],
|
||
]
|
||
for case in cases:
|
||
var sym_name: String = case[0]
|
||
var sym: int = case[1]
|
||
var at: Vector2 = case[2]
|
||
var parts := _make_tool(sym)
|
||
var ttool = parts[0]
|
||
var doc: Dictionary = parts[3]
|
||
_stroke(ttool, at.x, at.y, TerrainField.Mode.RAISE, STROKE_SEC)
|
||
var heights := _decode_heights(doc)
|
||
_check(_count_asym_heights(heights, sym) == 0,
|
||
"%s: Überlapp-Fußabdruck — Hälften exakt zellgleich" % sym_name)
|
||
# Strich-Seite ist Master: die Zelle unter dem Cursor ist sichtbar
|
||
# gehoben UND trägt denselben Wert wie ihr Zwilling.
|
||
var under := _world_to_cell(at)
|
||
var mc: Vector2i = SymmetryCore.mirror_cell(sym, under.x, under.y, RES)
|
||
_check(heights[under.y * RES + under.x] > 0.5
|
||
and heights[under.y * RES + under.x] == heights[mc.y * RES + mc.x],
|
||
"%s: Strich-Zelle und Zwilling identisch (%.3f m)" % [
|
||
sym_name, heights[under.y * RES + under.x]])
|
||
ttool.free()
|
||
|
||
|
||
## 4) Modus AUS = Verhalten wie vorher: byte-identisch zur direkten
|
||
## TerrainField-Anwendung (Gegenprobe), NICHTS wird gespiegelt.
|
||
func _test_off_mode_unchanged() -> void:
|
||
print("Testgruppe: Modus AUS = Verhalten wie vorher (Gegenprobe)")
|
||
# Höhen-Strich:
|
||
var parts := _make_tool(SymmetryCore.Mode.OFF)
|
||
var ttool = parts[0]
|
||
var doc: Dictionary = parts[3]
|
||
_stroke(ttool, -100.0, -60.0, TerrainField.Mode.RAISE, STROKE_SEC)
|
||
var steps := int(ceil(STROKE_SEC / 0.05))
|
||
var ref := _reference_heights(steps, -100.0, -60.0, TerrainField.Mode.RAISE)
|
||
var ref_b64 := TerrainField.encode(ref, HMIN, HMAX)
|
||
_check(str(doc["terrain"]["heightmap_r16_b64"]) == ref_b64,
|
||
"AUS/Heben: byte-identisch zur direkten TerrainField-Anwendung")
|
||
var heights := _decode_heights(doc)
|
||
var twin := SymmetryCore.mirror_point(SymmetryCore.Mode.POINT,
|
||
Vector3(-100.0, 0.0, -60.0), Vector2.ZERO)
|
||
var twin_h := TerrainField.height_at(heights, RES, SIZE, SIZE, twin.x, twin.z)
|
||
_check(absf(twin_h) < 0.0001,
|
||
"AUS/Heben: Zwilling-Position bleibt FLACH (nichts gespiegelt)")
|
||
ttool.free()
|
||
# Malen-Strich:
|
||
var parts_p := _make_tool(SymmetryCore.Mode.OFF)
|
||
var ttool_p = parts_p[0]
|
||
var doc_p: Dictionary = parts_p[3]
|
||
ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras
|
||
_stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3)
|
||
var ref_mats := PackedByteArray()
|
||
ref_mats.resize(RES * RES)
|
||
for i in int(ceil(0.3 / 0.05)):
|
||
TerrainField.apply_material_brush(ref_mats, RES, SIZE, SIZE,
|
||
-100.0, -60.0, RADIUS, 1)
|
||
var ref_mats_b64 := SvmapIO.encode_materials(ref_mats)
|
||
_check(str(doc_p["terrain"]["materials_r8_b64"]) == ref_mats_b64,
|
||
"AUS/Malen: Material-Schicht byte-identisch zur direkten Anwendung")
|
||
var got_mats := _decode_mats(doc_p)
|
||
var twin_cell := _world_to_cell(Vector2(100.0, 60.0))
|
||
_check(int(got_mats[twin_cell.y * RES + twin_cell.x]) == 0,
|
||
"AUS/Malen: Zwilling-Position bleibt Gras (nichts gespiegelt)")
|
||
ttool_p.free()
|
||
|
||
|
||
## 5) Live-Rebuild-Pfad: was _process pro Frame an update_terrain gibt
|
||
## (Höhen UND Material-Schicht), ist bereits gespiegelt — der Live-Mesh-
|
||
## Input ist identisch zum Vollrebuild (Falle t_55ed041a bleibt zu).
|
||
func _test_live_rebuild_inputs() -> void:
|
||
print("Testgruppe: Live-Pfad — Mesh-Inputs pro Frame gespiegelt + Schicht dabei")
|
||
var parts := _make_tool(SymmetryCore.Mode.POINT)
|
||
var ttool = parts[0]
|
||
var tmain = parts[1]
|
||
var builder = parts[2]
|
||
ttool.set("_cursor_valid", true)
|
||
ttool.set("_cursor", Vector3(-100.0, 0.0, -60.0))
|
||
ttool.call("_begin_stroke", TerrainField.Mode.RAISE)
|
||
for frame in 3:
|
||
ttool.call("_process", 0.016)
|
||
var live: PackedFloat32Array = builder.get("last_heights")
|
||
var live_mats: PackedByteArray = builder.get("last_mats")
|
||
_check(live.size() == RES * RES,
|
||
"Live-Frame %d: Höhen-Feld komplett (33²)" % (frame + 1))
|
||
_check(_count_asym_heights(live, SymmetryCore.Mode.POINT) == 0,
|
||
"Live-Frame %d: Mesh-Input-Höhen bereits gespiegelt" % (frame + 1))
|
||
_check(live_mats.size() == RES * RES,
|
||
"Live-Frame %d: Material-Schicht MITGEBEN (t_55ed041a-Vertrag)" % (frame + 1))
|
||
# Malen live: auch der Mal-Frame gibt Höhe + (gespiegelte) Schicht weiter.
|
||
ttool.call("_end_stroke")
|
||
ttool.call("set_paint_material", 1) # Sand — Gras über Gras wäre ein No-op
|
||
ttool.set("_cursor", Vector3(-100.0, 0.0, -60.0))
|
||
ttool.call("_begin_stroke", TerrainField.Mode.PAINT)
|
||
ttool.call("_process", 0.016)
|
||
var paint_mats: PackedByteArray = builder.get("last_mats")
|
||
_check(_count_asym_mats(paint_mats, SymmetryCore.Mode.POINT) == 0,
|
||
"Live-Mal-Frame: Mesh-Input-Material bereits gespiegelt")
|
||
ttool.call("_end_stroke")
|
||
_check(int(tmain.get("commits")) == 2,
|
||
"Zwei Striche → zwei Commits (Undo-Vertrag bleibt)")
|
||
ttool.free()
|
||
# builder/tmain sind RefCounted (wie in test_terrain.gd) — kein free().
|
||
|
||
|
||
## 6) Ein gespiegelter Strich = EIN Undo-Schritt; Undo nimmt BEIDE Hälften
|
||
## zurück, Redo bringt sie byte-identisch zurück.
|
||
func _test_undo_one_step() -> void:
|
||
print("Testgruppe: Gespiegelter Strich = EIN Undo-Schritt (beide Hälften)")
|
||
var parts := _make_tool(SymmetryCore.Mode.POINT)
|
||
var ttool = parts[0]
|
||
var tmain = parts[1]
|
||
var doc: Dictionary = parts[3]
|
||
_stroke(ttool, -100.0, -60.0, TerrainField.Mode.RAISE, STROKE_SEC)
|
||
_check(int(tmain.get("commits")) == 1, "Ein Strich → EIN Commit")
|
||
var hist: ActionHistory = tmain.get("history")
|
||
_check(hist.entries.size() == 1, "Ein Strich → EIN History-Eintrag (beide Hälften)")
|
||
var raised_b64 := str(doc["terrain"]["heightmap_r16_b64"])
|
||
var desc := hist.undo()
|
||
_check(desc.contains("Boden geformt"), "Undo-Desc nennt den Strich (%s)" % desc)
|
||
var restored := _decode_heights(doc)
|
||
var flat := true
|
||
for v in restored:
|
||
if v != 0.0:
|
||
flat = false
|
||
break
|
||
_check(flat, "Undo: BEIDE Hälften wieder flach (ganzer gespiegelter Strich)")
|
||
var redo_desc := hist.redo()
|
||
_check(redo_desc.contains("Boden geformt"), "Redo-Desc stimmt")
|
||
_check(str(doc["terrain"]["heightmap_r16_b64"]) == raised_b64,
|
||
"Redo: byte-identisches gespiegeltes Höhenfeld zurück")
|
||
ttool.free()
|
||
|
||
|
||
## Welt-X/Z → Rasterzelle (gleiche Konvention wie terrain_tool._world_to_grid).
|
||
func _world_to_cell(at: Vector2) -> Vector2i:
|
||
var fx: int = clampi(int(roundf((at.x / SIZE + 0.5) * float(RES - 1))), 0, RES - 1)
|
||
var fz: int = clampi(int(roundf((at.y / SIZE + 0.5) * float(RES - 1))), 0, RES - 1)
|
||
return Vector2i(fx, fz) |