Terrain: Material-Farben bleiben LIVE während Heben/Senk-Stroke (t_55ed041a) — Live-Rebuild bekommt die Material-Schicht mit
Sly-Befund: Beim Heben/Senken über gemalten Sand/Fels verschwanden die Material-Farben live (alles Gras), erst am Stroke-Ende kehrten sie mit dem Vollrebuild zurück. Root-Cause: terrain_tool rief im Live-Pfad (Heben/Senken/Glätten) update_terrain(_heights) OHNE Material-Schicht auf — _mats wurde nur im Malen-Modus dekodiert — und terrain_mesh fällt bei leerer Schicht auf alles-Gras zurück (Legacy-Fallback). Fix (zwei Ebenen): - terrain_tool._refresh_from_doc dekodiert die Material-Schicht immer, der Live-Stroke übergibt sie mit (Heben/Senken/Glätten wie Malen). - terrain_mesh cached die zuletzt gesehene Schicht: Live-Updates ohne mats-Param nutzen denselben Farb-Pfad wie der Idle-Rebuild; Legacy-v1 (nie gesehene Schicht) bleibt alles Gras — kein Phantom-Cache. Test: tests/test_terrain.gd neue Gruppe „Live-Material-Farben" — per-Frame-Check (5 Frames Heben über Sand-Zone, nicht nur Endzustand), terrain_tool-Integration (Heben-Stroke übergibt Sand-Schicht, Stroke-Ende = EIN Commit), Legacy-Gras ohne Phantom-Cache. Suite: 99 checks ALL OK; alle weiteren Headless-Suiten grün (test_glb_view 12/22 FAIL ist vorbestehend auf origin/main — GLB-Quellen fehlen im Workspace).
This commit is contained in:
parent
db6bcec971
commit
e1c737df61
4 changed files with 155 additions and 5 deletions
|
|
@ -38,7 +38,9 @@ var _stroking := false
|
|||
var _stroke_mode: int = TerrainField.Mode.RAISE
|
||||
var _stroke_material: int = 0
|
||||
var _heights := PackedFloat32Array()
|
||||
var _mats := PackedByteArray() # Material-Schicht (res²) während des Mal-Strichs
|
||||
var _mats := PackedByteArray() # Material-Schicht (res²) — seit _refresh_
|
||||
# _from_doc immer dekodiert (t_55ed041a),
|
||||
# Mal-Strich schreibt live hinein.
|
||||
var _res := 0
|
||||
var _sx := 0.0
|
||||
var _sz := 0.0
|
||||
|
|
@ -116,6 +118,10 @@ func _refresh_from_doc() -> bool:
|
|||
if _res < 2 or _sx <= 0.0 or _sz <= 0.0 or _hmax <= _hmin:
|
||||
return false
|
||||
_heights = SvmapIO.decode_heights(terrain)
|
||||
# Material-Schicht mit dekodieren (Karte t_55ed041a): der Live-Rebuild
|
||||
# beim Heben/Senken/Glätten braucht sie, sonst fällt das Mesh während
|
||||
# des Strichs auf alles-Gras zurück (Sand/Fels „verschwinden" live).
|
||||
_mats = SvmapIO.decode_materials(terrain)
|
||||
return _heights.size() >= _res * _res
|
||||
|
||||
|
||||
|
|
@ -248,7 +254,11 @@ func _process(delta: float) -> void:
|
|||
_cursor.x, _cursor.z, brush_radius, _stroke_mode, amount)
|
||||
if changed:
|
||||
_expand_range_if_needed()
|
||||
view_builder.call("update_terrain", _heights)
|
||||
# Karte t_55ed041a: Material-Schicht MITGEBEN — ohne sie hat der
|
||||
# Live-Rebuild alles als Gras gerendert (Sand/Fels „verschwanden"
|
||||
# während des Heben/Senk-Strichs und kehrten erst am Stroke-Ende
|
||||
# mit dem Vollrebuild zurück).
|
||||
view_builder.call("update_terrain", _heights, _mats)
|
||||
_ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ func _build_terrain(data: Dictionary) -> void:
|
|||
## Live-Update während eines Pinsel-Strichs (aus terrain_tool.gd): nur die
|
||||
## Vertices werden neu geschrieben — Grid/Objekte/Marker bleiben unangetastet.
|
||||
## mats (optional) = Material-Schicht (Karte t_603ec1a8, Malen-Modus) —
|
||||
## leer = Farben nur aus der Höhe (Legacy).
|
||||
## leer = terrain_mesh verwendet die zuletzt gesehene Schicht weiter (Cache,
|
||||
## Karte t_55ed041a); nur bei NIE gesehener Schicht (Legacy-v1) gilt Gras.
|
||||
func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedByteArray()) -> void:
|
||||
if terrain_view != null and is_instance_valid(terrain_view):
|
||||
terrain_view.call("update_heights", heights, 0, 0.0, 0.0, mats)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ var _mi: MeshInstance3D
|
|||
var _res := 0
|
||||
var _size_x := 0.0
|
||||
var _size_z := 0.0
|
||||
var _mats_cache := PackedByteArray() # zuletzt gesehene Material-Schicht
|
||||
# (res²) — Live-Pfad-Fallback, Karte
|
||||
# t_55ed041a.
|
||||
var _triangles := 0
|
||||
|
||||
|
||||
|
|
@ -61,8 +64,11 @@ func rebuild_from_doc(data: Dictionary) -> void:
|
|||
|
||||
## Mesh aus einem (neuen) Höhenfeld bauen — auch live während des Strichs.
|
||||
## res/size 0 = zuletzt verwendete Werte weiterverwenden (Live-Pfad).
|
||||
## mats = Material-Schicht (res² Bytes, 0=Gras/1=Sand/2=Fels); leer/zu klein
|
||||
## → alles Gras (Legacy-Verhalten).
|
||||
## mats = Material-Schicht (res² Bytes, 0=Gras/1=Sand/2=Fels); leer → die
|
||||
## zuletzt gesehene Schicht (Cache) wird weiterverwendet, damit Live-Updates
|
||||
## ohne explizite Schicht (z. B. Heben/Senken-Stroke) die Material-Farben
|
||||
## BEHALTEN statt live auf alles-Gras zurückzufallen (Karte t_55ed041a).
|
||||
## Nur wenn NIE eine Schicht gesehen wurde (Legacy-v1-Map) gilt alles Gras.
|
||||
func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0.0,
|
||||
size_z: float = 0.0, mats: PackedByteArray = PackedByteArray()) -> void:
|
||||
if res < 2:
|
||||
|
|
@ -78,6 +84,13 @@ func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0
|
|||
_res = res
|
||||
_size_x = size_x
|
||||
_size_z = size_z
|
||||
if mats.is_empty() and _mats_cache.size() == res * res:
|
||||
mats = _mats_cache
|
||||
elif mats.size() == res * res:
|
||||
_mats_cache = mats
|
||||
elif not mats.is_empty():
|
||||
_mats_cache = mats # Größe passt nicht (res-Wechsel) → trotzdem
|
||||
# merken; der Index-Clamp unten schützt.
|
||||
var last := float(res - 1)
|
||||
var positions := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func _run_tests() -> void:
|
|||
_test_paint_undo_redo()
|
||||
_test_material_format()
|
||||
_test_mesh_winding_live_grid()
|
||||
_test_live_material_colors()
|
||||
_test_grid_toggle()
|
||||
# _test_ray_terrain_mapping läuft in _finish() — braucht Viewport im Baum.
|
||||
|
||||
|
|
@ -359,6 +360,131 @@ func _has_err(errors: PackedStringArray, needle: String) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
## Stubs für die terrain_tool-Integration (gleiche Konvention wie
|
||||
## test_shortcuts.gd ProbeMain/ProbeBuilder): der Builder zeichnet AUF,
|
||||
## was der Live-Stroke an update_terrain übergibt.
|
||||
class ToolBuilder extends Node3D:
|
||||
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
|
||||
|
||||
|
||||
class ToolMain:
|
||||
var doc: Dictionary = {}
|
||||
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
|
||||
|
||||
|
||||
## Karte t_55ed041a: Material-Farben bleiben DURCHGEHEND sandfarben, wenn
|
||||
## über eine gemalte Sand-Zone gehoben wird. Sly-Befund: während des
|
||||
## Heben/Senk-Strichs „verschwanden" Sand/Stein live (alles Gras), erst am
|
||||
## Stroke-Ende kehrten sie zurück — der Live-Rebuild rief update_heights
|
||||
## OHNE Material-Schicht auf. Geprüft wird PER FRAME (nicht nur Endzustand):
|
||||
## a) terrain_mesh-Cache: Live-Update ohne mats-Param behält Sand-Farben.
|
||||
## b) terrain_tool: Heben-Stroke übergibt die Material-Schicht mit.
|
||||
## c) Legacy (nie gesehene Schicht) bleibt alles Gras — kein Phantom-Cache.
|
||||
func _test_live_material_colors() -> void:
|
||||
print("Testgruppe: Live-Material-Farben während Heben/Senk (t_55ed041a)")
|
||||
var res := 33
|
||||
var size := 320.0
|
||||
var center := 16 * res + 16
|
||||
var heights := PackedFloat32Array()
|
||||
heights.resize(res * res)
|
||||
var mats := PackedByteArray()
|
||||
mats.resize(res * res)
|
||||
TerrainField.apply_material_brush(mats, res, size, size, 0.0, 0.0, 40.0, 1)
|
||||
_check(mats[center] == 1, "Vorbereitung: Zentrum = Sand gemalt")
|
||||
|
||||
# --- a) Live-Stroke am Mesh: update_heights OHNE mats (exakter Bug-Pfad) ---
|
||||
var view := Node3D.new()
|
||||
view.set_script(load("res://src/viewport/terrain_mesh.gd"))
|
||||
view.call("update_heights", heights, res, size, size, mats) # Vollrebuild mit Schicht
|
||||
var sand_seen := false
|
||||
for frame in 5:
|
||||
TerrainField.apply_brush(heights, res, size, size, -100000.0, 100000.0,
|
||||
0.0, 0.0, 40.0, TerrainField.Mode.RAISE, 4.0)
|
||||
# Der alte Live-Aufruf: OHNE Material-Schicht (so rief terrain_tool
|
||||
# vorher bei Heben/Senken auf) — der Cache muss Sand liefern.
|
||||
view.call("update_heights", heights)
|
||||
var col: PackedColorArray = (view.get("_mi").mesh.surface_get_arrays(0))[Mesh.ARRAY_COLOR]
|
||||
var hmin := INF
|
||||
var hmax := -INF
|
||||
for v in heights:
|
||||
hmin = minf(hmin, v)
|
||||
hmax = maxf(hmax, v)
|
||||
var shade := lerpf(0.72, 1.15, (heights[center] - hmin) / maxf(hmax - hmin, 0.0001))
|
||||
var expected := Color(0.78, 0.70, 0.45) * shade # Sand × Höhen-Shading
|
||||
var c := col[center]
|
||||
_check(absf(c.r - expected.r) < 0.005 and absf(c.g - expected.g) < 0.005
|
||||
and absf(c.b - expected.b) < 0.005,
|
||||
"Live-Frame %d: Zentrum sandfarben (ist %s, soll ~%s)" % [
|
||||
frame + 1, c, expected])
|
||||
sand_seen = true
|
||||
_check(sand_seen, "Live-Stroke: 5 Frames geprüft (per-Frame, nicht nur Endzustand)")
|
||||
view.free()
|
||||
|
||||
# --- b) terrain_tool-Integration: Heben-Stroke gibt die Schicht mit ---
|
||||
var doc := SvmapDocument.create_new("LiveMats", 320.0, "mittag")
|
||||
var terrain: Dictionary = doc["terrain"]
|
||||
terrain["resolution"] = res
|
||||
terrain["size_x_m"] = size
|
||||
terrain["size_z_m"] = size
|
||||
terrain["min_height_m"] = 0.0
|
||||
terrain["max_height_m"] = 10.0
|
||||
terrain["heightmap_r16_b64"] = TerrainField.encode(heights, 0.0, 10.0)
|
||||
terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats)
|
||||
var tmain := ToolMain.new()
|
||||
tmain.doc = doc
|
||||
var builder := ToolBuilder.new()
|
||||
var ttool := Node3D.new()
|
||||
ttool.set_script(load("res://src/editor/terrain_tool.gd"))
|
||||
# Bewusst NICHT in den Baum: sonst feuert Godots automatisches _process
|
||||
# zwischen den manuellen Calls und der Live-Update-Zähler wäre fragil.
|
||||
ttool.setup(tmain, Camera3D.new(), builder, null)
|
||||
_check(bool(ttool.call("activate")), "terrain_tool: aktiviert (Test-Map)")
|
||||
var tmats: PackedByteArray = ttool.get("_mats")
|
||||
_check(tmats.size() == res * res and tmats[center] == 1,
|
||||
"terrain_tool: _refresh_from_doc dekodiert die Material-Schicht (Sand)")
|
||||
ttool.set("_cursor_valid", true)
|
||||
ttool.set("_cursor", Vector3(0.0, 0.0, 0.0))
|
||||
ttool.call("_begin_stroke", TerrainField.Mode.RAISE)
|
||||
ttool.call("_process", 0.016)
|
||||
ttool.call("_process", 0.016)
|
||||
_check(int(builder.get("calls")) == 2, "terrain_tool: Live-Updates sind angekommen")
|
||||
var bmat: PackedByteArray = builder.get("last_mats")
|
||||
_check(bmat.size() == res * res and bmat[center] == 1,
|
||||
"terrain_tool: Heben-Stroke übergibt Material-Schicht (Zentrum = Sand)")
|
||||
ttool.call("_end_stroke")
|
||||
_check(int(tmain.get("commits")) == 1, "terrain_tool: Stroke-Ende = EIN Commit (unangetastet)")
|
||||
ttool.free()
|
||||
builder.free()
|
||||
|
||||
# --- c) Legacy: nie gesehene Schicht → alles Gras (kein Phantom-Cache) ---
|
||||
var view2 := Node3D.new()
|
||||
view2.set_script(load("res://src/viewport/terrain_mesh.gd"))
|
||||
var flat := PackedFloat32Array()
|
||||
flat.resize(res * res)
|
||||
view2.call("update_heights", flat, res, size, size) # OHNE Schicht, Cache leer
|
||||
var col2: PackedColorArray = (view2.get("_mi").mesh.surface_get_arrays(0))[Mesh.ARRAY_COLOR]
|
||||
var grass_expected := Color(0.30, 0.45, 0.22) * 0.72 # flach: t=0 → shade=SHADING_LOW
|
||||
var c2 := col2[center]
|
||||
_check(absf(c2.r - grass_expected.r) < 0.005 and absf(c2.g - grass_expected.g) < 0.005
|
||||
and absf(c2.b - grass_expected.b) < 0.005,
|
||||
"Legacy (nie gesehene Schicht): Gras-Farbe bleibt (kein Phantom-Cache)")
|
||||
view2.free()
|
||||
|
||||
|
||||
## Karte t_7ac70014: Winding-Order + Normale + Live-Pinsel am ECHTEN Mesh.
|
||||
## Gerätesymptom war: dunkle Platte + Gitter (Terrain von oben komplett
|
||||
## ge-cullt, weil die Dreiecke von oben Counter-Clockwise liefen = Backface),
|
||||
|
|
|
|||
Loading…
Reference in a new issue