From 69668eb25054806ad139ba867cb4395e1402b173 Mon Sep 17 00:00:00 2001 From: slyphax Date: Sun, 25 Oct 2026 00:07:25 +0200 Subject: [PATCH] =?UTF-8?q?Feines=20Material-Raster:=20Malen=20auf=20~1=20?= =?UTF-8?q?m=20statt=20H=C3=B6henraster=20(bei=201024=20m=20vorher=208=20m?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sly: „feiner malen“. Die Material-Schicht bekommt ein eigenes Raster (terrain.material_resolution, optional; Default = resolution → alte Karten unverändert gültig): ~1 m pro Punkt, auf dem Höhenraster ausgerichtet, max. 1025 (1024 m → 1025, 256 m → 257, 2048 m → 1025 = 2 m). - SvmapIO: material_resolution / material_resolution_for / resample / upgrade_material_layer / material_at_height_point; V15 prüft Raster + Länge. - Neue Karten starten fein; beim Öffnen werden alte Karten optisch identisch angehoben (Datei ändert sich erst beim Speichern); Resize rechnet die Schicht auf ihrem eigenen Raster an der Weltposition um. - Pinsel, Spiegeln (Symmetrie live + „Symmetrie anwenden“) und Live-Splat arbeiten auf dem Material-Raster; Vertex-Farben lesen den passenden Punkt. - Malen-Hinweis zeigt die echte feinste Einheit. - Tests: test_mal_fein 22/22 (Raster, Anheben, 2-m-Pinsel ≈ 13 Punkte, V15, Roundtrip, Resize); test_terrain/test_terrain_mirror/ test_symmetry_onclick auf das neue Raster nachgezogen; Identitäts-Runner OK. Kette Editor → Server (Prüfsumme) → Client per run_map_download_e2e 15/15. Format-Spiegel: Match-Server svmap_loader (V15) + Client svmap_terrain. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/editor/symmetry_ops.gd | 48 ++++++++++-- src/editor/terrain_tool.gd | 53 ++++++++----- src/svmap/map_document.gd | 29 ++++++-- src/svmap/svmap_io.gd | 88 +++++++++++++++++++++- src/ui/main.gd | 5 ++ src/ui/panels/symmetry_panel.gd | 2 +- src/ui/panels/terrain_panel.gd | 4 +- src/viewport/terrain_mesh.gd | 30 +++++--- tests/probe_fein_export.gd | 22 ++++++ tests/probe_fein_export.gd.uid | 1 + tests/test_klick_exakt.gd.uid | 1 + tests/test_mal_fein.gd | 128 ++++++++++++++++++++++++++++++++ tests/test_mal_fein.gd.uid | 1 + tests/test_mal_hinweis.gd.uid | 1 + tests/test_symmetry_onclick.gd | 20 +++-- tests/test_terrain.gd | 30 +++++--- tests/test_terrain_mirror.gd | 1 + 17 files changed, 397 insertions(+), 67 deletions(-) create mode 100644 tests/probe_fein_export.gd create mode 100644 tests/probe_fein_export.gd.uid create mode 100644 tests/test_klick_exakt.gd.uid create mode 100644 tests/test_mal_fein.gd create mode 100644 tests/test_mal_fein.gd.uid create mode 100644 tests/test_mal_hinweis.gd.uid diff --git a/src/editor/symmetry_ops.gd b/src/editor/symmetry_ops.gd index b044c0f..4aebe3f 100644 --- a/src/editor/symmetry_ops.gd +++ b/src/editor/symmetry_ops.gd @@ -148,6 +148,7 @@ static func analyze(doc: Dictionary, mode: int) -> Dictionary: "gebaeude": {"pairs": [], "singles_low": [], "singles_high": [], "diff": 0}, "spawns": {"pairs": [], "singles_low": [], "singles_high": [], "diff": 0}, "terrain_cells": [], + "material_cells": [], # Material-Raster (fein, Karte 24.10.) — eigenes Gitter "deviations": [], "links": 0, # positionierende Paare ohne sym-Kopplung (werden verknüpft) "conflicts": 0, # widersprüchliche Paare + Boden-Zellen @@ -163,7 +164,8 @@ static func analyze(doc: Dictionary, mode: int) -> Dictionary: _analyze_spawns(doc, mode, an) _analyze_terrain(doc, mode, an) an["conflicts"] = int(an["objects"]["diff"]) + int(an["gebaeude"]["diff"]) \ - + int(an["spawns"]["diff"]) + (an["terrain_cells"] as Array).size() + + int(an["spawns"]["diff"]) + (an["terrain_cells"] as Array).size() \ + + (an["material_cells"] as Array).size() an["adds"] = (an["objects"]["singles_low"] as Array).size() \ + (an["objects"]["singles_high"] as Array).size() \ + (an["gebaeude"]["singles_low"] as Array).size() \ @@ -455,9 +457,8 @@ static func _analyze_terrain(doc: Dictionary, mode: int, an: Dictionary) -> void var heights := SvmapIO.decode_heights(terrain) if heights.size() < res * res: return - var mats := SvmapIO.decode_materials(terrain) var cells: Array = an["terrain_cells"] - var eps := HEIGHT_EPS_M # absolut in Metern — Material als Byte exakt vergleichen + var eps := HEIGHT_EPS_M # absolut in Metern for j in res: for i in res: var mc := SymmetryCore.mirror_cell(mode, i, j, res) @@ -470,11 +471,25 @@ static func _analyze_terrain(doc: Dictionary, mode: int, an: Dictionary) -> void var ia := j * res + i var ib := mc.y * res + mc.x var dh := absf(heights[ia] - heights[ib]) - var dm: bool = mats[ia] != mats[ib] - if dh > eps or dm: + if dh > eps: cells.append(Vector2i(i, j)) if cells.size() <= MAX_EXAMPLES: _push_dev(an, "Boden (%d,%d)" % [i, j]) + # Material auf seinem eigenen Raster (Byte exakt vergleichen). + var mres := SvmapIO.material_resolution(terrain) + var mats := SvmapIO.decode_materials(terrain) + if mres < 2 or mats.size() < mres * mres: + return + var mcells: Array = an["material_cells"] + for j in mres: + for i in mres: + var mc := SymmetryCore.mirror_cell(mode, i, j, mres) + if not (i < mc.x or (i == mc.x and j < mc.y)): + continue + if mats[j * mres + i] != mats[mc.y * mres + mc.x]: + mcells.append(Vector2i(i, j)) + if mcells.size() <= MAX_EXAMPLES: + _push_dev(an, "Boden-Material (%d,%d)" % [i, j]) # ----------------------------------------------------------------- check() -- @@ -636,6 +651,7 @@ static func _apply_gebaeude(doc: Dictionary, mode: int, an: Dictionary, static func _apply_terrain(doc: Dictionary, mode: int, an: Dictionary, quelle_hoch: bool, c: Dictionary) -> void: + _apply_materials(doc, mode, an, quelle_hoch, c) var cells: Array = an["terrain_cells"] if (cells as Array).is_empty(): return @@ -644,7 +660,6 @@ static func _apply_terrain(doc: Dictionary, mode: int, an: Dictionary, if res < 2: return var heights := SvmapIO.decode_heights(terrain) - var mats := SvmapIO.decode_materials(terrain) var hmin := float(terrain.get("min_height_m", 0.0)) var hmax := float(terrain.get("max_height_m", 1.0)) for cell in cells: @@ -655,9 +670,28 @@ static func _apply_terrain(doc: Dictionary, mode: int, an: Dictionary, var si: int = src.y * res + src.x var di: int = dst.y * res + dst.x heights[di] = heights[si] - mats[di] = mats[si] c["terrain_cells"] = int(c["terrain_cells"]) + 1 terrain["heightmap_r16_b64"] = TerrainField.encode(heights, hmin, hmax) + + +## Material-Schicht angleichen (eigenes feines Raster, Karte 24.10.). +static func _apply_materials(doc: Dictionary, mode: int, an: Dictionary, + quelle_hoch: bool, c: Dictionary) -> void: + var cells: Array = an.get("material_cells", []) + if cells.is_empty(): + return + var terrain: Dictionary = doc.get("terrain", {}) + var mres := SvmapIO.material_resolution(terrain) + var mats := SvmapIO.decode_materials(terrain) + if mres < 2 or mats.size() < mres * mres: + return + for cell in cells: + var cc: Vector2i = cell + var mc := SymmetryCore.mirror_cell(mode, cc.x, cc.y, mres) + var src := cc if not quelle_hoch else mc + var dst := mc if not quelle_hoch else cc + mats[dst.y * mres + dst.x] = mats[src.y * mres + src.x] + c["terrain_cells"] = int(c["terrain_cells"]) + 1 terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) diff --git a/src/editor/terrain_tool.gd b/src/editor/terrain_tool.gd index 8b318be..40012e6 100644 --- a/src/editor/terrain_tool.gd +++ b/src/editor/terrain_tool.gd @@ -42,6 +42,7 @@ var _mats := PackedByteArray() # Material-Schicht (res²) — seit _refresh_ # _from_doc immer dekodiert (t_55ed041a), # Mal-Strich schreibt live hinein. var _res := 0 +var _mres := 0 # Raster der Material-Schicht (feines Raster, Karte 24.10.; alt = _res) var _sx := 0.0 var _sz := 0.0 var _hmin := 0.0 @@ -122,6 +123,7 @@ func _refresh_from_doc() -> bool: # 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) + _mres = SvmapIO.material_resolution(terrain) return _heights.size() >= _res * _res @@ -222,6 +224,7 @@ func _begin_stroke(mode: int, material_id: int = -1) -> void: # (dekodiert — Legacy-Maps ohne Schicht starten mit alles-Gras). if mode == TerrainField.Mode.PAINT: _mats = SvmapIO.decode_materials(main.doc.get("terrain", {})) + _mres = SvmapIO.material_resolution(main.doc.get("terrain", {})) _refresh_ring_mesh() main.call("_set_status", "%s läuft — Maustaste gedrückt halten. Beim Loslassen wird der ganze Strich EIN Rückgängig-Schritt (Strg+Z)." % _stroke_label()) @@ -384,21 +387,31 @@ func _sym_active() -> bool: ## Welt-X/Z → Raster-Koordinaten (gleiche Konvention wie TerrainField.height_at: ## Index 0 = −X/−Z-Ecke, x-Index läuft MIT +WeltX). func _world_to_grid(x: float, z: float) -> Vector2: - var last := float(_res - 1) + return _world_to_grid_r(x, z, _res) + + +## Wie _world_to_grid, aber auf einem beliebigen Raster (Material: _mres). +func _world_to_grid_r(x: float, z: float, r: int) -> Vector2: + var last := float(r - 1) return Vector2((x / _sx + 0.5) * last, (z / _sz + 0.5) * last) ## Grobes Rechteck um die Raster-Position (g), das den Pinsel-Fußabdruck ## jedes Modus garantiert abdeckt (Radius in Zellen + Margin). func _brush_region(g: Vector2) -> Rect2i: - var cell_x := _sx / float(_res - 1) - var cell_z := _sz / float(_res - 1) + return _brush_region_r(g, _res) + + +## Wie _brush_region, auf dem Raster r (Material-Schicht: _mres). +func _brush_region_r(g: Vector2, r: int) -> Rect2i: + var cell_x := _sx / float(r - 1) + var cell_z := _sz / float(r - 1) var rx := brush_radius / cell_x + float(SYM_REGION_MARGIN) var rz := brush_radius / cell_z + float(SYM_REGION_MARGIN) var x0 := maxi(int(floorf(g.x - rx)), 0) - var x1 := mini(int(ceilf(g.x + rx)), _res - 1) + var x1 := mini(int(ceilf(g.x + rx)), r - 1) var z0 := maxi(int(floorf(g.y - rz)), 0) - var z1 := mini(int(ceilf(g.y + rz)), _res - 1) + var z1 := mini(int(ceilf(g.y + rz)), r - 1) return Rect2i(x0, z0, x1 - x0 + 1, z1 - z0 + 1) @@ -427,14 +440,15 @@ func _height_step(x: float, z: float, amount: float) -> bool: ## Ein Pinsel-Schritt MATERIAL (Malen) inkl. Spiegel-Kopie. func _paint_step(x: float, z: float) -> bool: - var g := _world_to_grid(x, z) + # Material lebt auf seinem eigenen (feinen) Raster _mres (Karte 24.10.). + var g := _world_to_grid_r(x, z, _mres) # VOR dem Pinsel: Zellrect des Fußabdrucks merken (deckt Spiegelzellen mit) # — der Live-Splat-Refresh (t_7a0a6b79) leitet genau diese Region neu ab. _splat_rect = _splat_region(g) var snap := {} - if _sym_active() and _mats.size() >= _res * _res: - snap = _snap_mats(_brush_region(g)) - var changed := TerrainField.apply_material_brush(_mats, _res, _sx, _sz, + if _sym_active() and _mats.size() >= _mres * _mres: + snap = _snap_mats(_brush_region_r(g, _mres)) + var changed := TerrainField.apply_material_brush(_mats, _mres, _sx, _sz, x, z, brush_radius, _stroke_material) if changed and not snap.is_empty(): _mirror_mats(snap, g) @@ -490,28 +504,28 @@ func _snap_mats(rect: Rect2i) -> Dictionary: var snap := {} for z: int in range(rect.position.y, rect.end.y): for x: int in range(rect.position.x, rect.end.x): - snap[Vector2i(x, z)] = int(_mats[z * _res + x]) + snap[Vector2i(x, z)] = int(_mats[z * _mres + x]) return snap ## Spiegel-Pass für die Material-Schicht (identische Paar-Logik wie _mirror_heights). func _mirror_mats(snap: Dictionary, g: Vector2) -> void: var sym := _sym_mode() - var cell_x := _sx / float(_res - 1) - var cell_z := _sz / float(_res - 1) + var cell_x := _sx / float(_mres - 1) + var cell_z := _sz / float(_mres - 1) var done := {} var positions: Array = snap.keys() positions.sort_custom(func(a: Vector2i, b: Vector2i) -> bool: - return (a.y * _res + a.x) < (b.y * _res + b.x)) + return (a.y * _mres + a.x) < (b.y * _mres + b.x)) for pos: Vector2i in positions: if done.has(pos): continue - var idx: int = pos.y * _res + pos.x - var mirror: Vector2i = SymmetryCore.mirror_cell(sym, pos.x, pos.y, _res) + var idx: int = pos.y * _mres + pos.x + var mirror: Vector2i = SymmetryCore.mirror_cell(sym, pos.x, pos.y, _mres) done[pos] = true if mirror == pos: continue - var midx: int = mirror.y * _res + mirror.x + var midx: int = mirror.y * _mres + mirror.x var value: int = int(_mats[idx]) if snap.has(mirror): if _cell_dist(mirror, g, cell_x, cell_z) < _cell_dist(pos, g, cell_x, cell_z): @@ -580,12 +594,13 @@ var _splat_rect := Rect2i() ## Zellrect des Pinsel-Fußabdrucks (∪ Spiegelzellen): dieselben Region- ## Grenzen wie apply_material_brush, mit dem SYM_REGION_MARGIN-Rand — der ## deckt auch die Kernel-Nachbarschaft der Splat-Ableitung mit ab. +## g und Ergebnis im Material-Raster (_mres) — so rechnet auch die Splat. func _splat_region(g: Vector2) -> Rect2i: - var rect := _brush_region(g) + var rect := _brush_region_r(g, _mres) var sym := _sym_mode() if sym == SymmetryCore.Mode.OFF: return rect - var a := SymmetryCore.mirror_cell(sym, rect.position.x, rect.position.y, _res) - var b := SymmetryCore.mirror_cell(sym, rect.end.x - 1, rect.end.y - 1, _res) + var a := SymmetryCore.mirror_cell(sym, rect.position.x, rect.position.y, _mres) + var b := SymmetryCore.mirror_cell(sym, rect.end.x - 1, rect.end.y - 1, _mres) return rect.merge(Rect2i(mini(a.x, b.x), mini(a.y, b.y), absi(a.x - b.x) + 1, absi(a.y - b.y) + 1)) diff --git a/src/svmap/map_document.gd b/src/svmap/map_document.gd index 4d9091a..22f1146 100644 --- a/src/svmap/map_document.gd +++ b/src/svmap/map_document.gd @@ -24,8 +24,10 @@ static func create_new(map_name: String, size_m: float, weather: String) -> Dict var res := DEFAULT_RESOLUTION var zero_heights := PackedByteArray() zero_heights.resize(res * res * 2) # alles 0 → flaches Terrain auf min_height + # Feines Material-Raster (Karte 24.10.): ~1 m pro Punkt statt Höhenraster. + var mres := SvmapIO.material_resolution_for(res, size, size) var zero_materials := PackedByteArray() - zero_materials.resize(res * res) # alles 0 → komplette Map Gras + zero_materials.resize(mres * mres) # alles 0 → komplette Map Gras var half := size / 2.0 var data: Dictionary = { "format_version": SvmapIO.FORMAT_VERSION, @@ -46,6 +48,7 @@ static func create_new(map_name: String, size_m: float, weather: String) -> Dict "resolution": res, "heightmap_r16_b64": Marshalls.raw_to_base64(zero_heights), # v2 (t_603ec1a8): Material-Schicht — neue Maps starten komplett Gras. + "material_resolution": mres, "materials_r8_b64": Marshalls.raw_to_base64(zero_materials), }, "objects": [], @@ -199,14 +202,15 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float, var heights_old := SvmapIO.decode_heights(old_terrain) var mats_old := SvmapIO.decode_materials(old_terrain) + var old_mres := SvmapIO.material_resolution(old_terrain) + var new_mres := SvmapIO.material_resolution_for(new_res, new_sx, new_sz) var heights_new := PackedFloat32Array() heights_new.resize(new_res * new_res) var mats_new := PackedByteArray() - mats_new.resize(new_res * new_res) + mats_new.resize(new_mres * new_mres) var last_new := float(new_res - 1) - var last_old := float(old_res - 1) var half_x_old := old_sx * 0.5 var half_z_old := old_sz * 0.5 var eps := 0.0001 @@ -222,10 +226,20 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float, # Bestehendes Terrain: Weltposition halten, bilinear sampeln. heights_new[j * new_res + i] = TerrainField.height_at( heights_old, old_res, old_sx, old_sz, x, z) - # Material-Schicht auflösungstreu (nächster alter Gridpunkt). - var fx := clampf(roundf((x / old_sx + 0.5) * last_old), 0.0, last_old) - var fz := clampf(roundf((z / old_sz + 0.5) * last_old), 0.0, last_old) - mats_new[j * new_res + i] = mats_old[int(fz) * old_res + int(fx)] + # Material-Schicht auf ihrem eigenen (feinen) Raster, gleiche Regel: + # Weltposition halten, nächster alter Punkt; neue Randzone = Gras (0). + var last_mnew := float(new_mres - 1) + var last_mold := float(old_mres - 1) + for j in new_mres: + var z := (float(j) / last_mnew - 0.5) * new_sz + for i in new_mres: + var x := (float(i) / last_mnew - 0.5) * new_sx + if x < -half_x_old - eps or x > half_x_old + eps \ + or z < -half_z_old - eps or z > half_z_old + eps: + continue + var fx := clampf(roundf((x / old_sx + 0.5) * last_mold), 0.0, last_mold) + var fz := clampf(roundf((z / old_sz + 0.5) * last_mold), 0.0, last_mold) + mats_new[j * new_mres + i] = mats_old[int(fz) * old_mres + int(fx)] var terrain := { "type": "heightmap", @@ -235,6 +249,7 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float, "max_height_m": hmax, "resolution": new_res, "heightmap_r16_b64": TerrainField.encode(heights_new, hmin, hmax), + "material_resolution": new_mres, "materials_r8_b64": SvmapIO.encode_materials(mats_new), } diff --git a/src/svmap/svmap_io.gd b/src/svmap/svmap_io.gd index f01b8dc..cfed04c 100644 --- a/src/svmap/svmap_io.gd +++ b/src/svmap/svmap_io.gd @@ -72,6 +72,13 @@ const MAX_BOUNDS_EXTENT_M := 8192.0 # V14 const BOUNDS_TOLERANCE_M := 1.0 # V9 const MIN_SPAWN_POINTS_PER_TEAM := 4 # V11 (Entscheidung 11) const SYM_KEY_MAX_LEN := 64 # V16 (Symmetrie-Partner-Kennung) +## Feines Material-Raster (Karte 24.10., Sly: „feiner malen“): optionales +## terrain.material_resolution — eigenes Raster für materials_r8_b64, feiner als +## das Höhenfeld. Fehlt es, gilt resolution (alte Maps unverändert). Muss +## ≥ resolution sein und auf dem Höhenraster liegen ((m−1) Vielfaches von +## (res−1)), damit jeder Höhenpunkt genau einen Materialpunkt hat. +const MAX_MATERIAL_RESOLUTION := 1025 # V15 (1025² = 1 MB) +const MATERIAL_TARGET_M := 1.0 # Ziel: 1 m pro Materialpunkt const MANIFEST_PATH := "res://library/objects.json" const JSON_INDENT := " " @@ -261,12 +268,21 @@ static func _validate_terrain(terrain: Dictionary) -> PackedStringArray: errors.append("V5: heightmap_r16_b64 dekodiert zu %d Bytes, erwartet %d (= resolution² × 2)." % [ raw.size(), res_i * res_i * 2]) # V15: Material-Schicht (v2, optional — fehlt sie, gilt alles Gras). + var mres_v = terrain.get("material_resolution", res_i) + var mres_i := res_i + if not (mres_v is int or mres_v is float) or float(mres_v) != floorf(float(mres_v)) \ + or int(mres_v) < res_i or int(mres_v) > MAX_MATERIAL_RESOLUTION \ + or (res_i > 1 and (int(mres_v) - 1) % (res_i - 1) != 0): + errors.append("V15: terrain.material_resolution muss eine ganze Zahl %d–%d sein, deren (Wert−1) ein Vielfaches von (resolution−1) ist (ist %s)." % [ + res_i, MAX_MATERIAL_RESOLUTION, str(mres_v)]) + else: + mres_i = int(mres_v) var mats = terrain.get("materials_r8_b64", "") if mats is String and (mats as String) != "": var mraw := Marshalls.base64_to_raw(mats as String) - if mraw.size() != res_i * res_i: - errors.append("V15: materials_r8_b64 dekodiert zu %d Bytes, erwartet %d (= resolution²)." % [ - mraw.size(), res_i * res_i]) + if mraw.size() != mres_i * mres_i: + errors.append("V15: materials_r8_b64 dekodiert zu %d Bytes, erwartet %d (= material_resolution²)." % [ + mraw.size(), mres_i * mres_i]) else: for v in mraw: if int(v) >= MATERIAL_IDS.size(): @@ -579,7 +595,7 @@ static func decode_heights(terrain: Dictionary) -> PackedFloat32Array: ## Fehlt das Feld (Legacy-v1) → alles Gras (Default). Identische Indexierung ## wie das Höhenfeld — TerrainField.apply_material_brush arbeitet darauf. static func decode_materials(terrain: Dictionary) -> PackedByteArray: - var res := int(terrain.get("resolution", 0)) + var res := material_resolution(terrain) var out := PackedByteArray() out.resize(maxi(res * res, 0)) # PackedByteArray füllt mit 0 = Gras var mats = terrain.get("materials_r8_b64", "") @@ -592,6 +608,70 @@ static func decode_materials(terrain: Dictionary) -> PackedByteArray: return out +## Raster der Material-Schicht (feines Raster, Karte 24.10.) — ohne Feld +## gilt das Höhenraster (alte Maps). +static func material_resolution(terrain: Dictionary) -> int: + var v = terrain.get("material_resolution", null) + if v is int or v is float: + return int(v) + return int(terrain.get("resolution", 0)) + + +## Ziel-Raster für eine Karte: ~MATERIAL_TARGET_M pro Punkt, auf dem +## Höhenraster ausgerichtet, gedeckelt auf MAX_MATERIAL_RESOLUTION. +## 1024 m / res 129 → 1025 (1 m); 256 m / 129 → 257 (1 m); 2048 m → 1025 (2 m). +static func material_resolution_for(res: int, size_x: float, size_z: float) -> int: + if res < 2: + return res + var cell := maxf(size_x, size_z) / float(res - 1) + var k := maxi(1, int(ceilf(cell / MATERIAL_TARGET_M - 0.001))) + while k > 1 and (res - 1) * k + 1 > MAX_MATERIAL_RESOLUTION: + k -= 1 + return (res - 1) * k + 1 + + +## Material-Schicht auf ein anderes Raster umrechnen (nächster Punkt über die +## normierte Position — beide Raster spannen dieselbe Kartenfläche auf). +static func resample_materials(mats: PackedByteArray, from_res: int, to_res: int) -> PackedByteArray: + var out := PackedByteArray() + out.resize(maxi(to_res * to_res, 0)) + if from_res < 2 or to_res < 2 or mats.size() < from_res * from_res: + return out + var f := float(from_res - 1) / float(to_res - 1) + for j in to_res: + var sj := mini(int(roundf(float(j) * f)), from_res - 1) * from_res + var row := j * to_res + for i in to_res: + out[row + i] = mats[sj + mini(int(roundf(float(i) * f)), from_res - 1)] + return out + + +## Alte Karte (Material-Raster = Höhenraster) auf das feine Raster heben. +## true = geändert. Optisch identisch (nächster Punkt), nur ab jetzt feiner malbar. +static func upgrade_material_layer(terrain: Dictionary) -> bool: + var res := int(terrain.get("resolution", 0)) + if res < 2: + return false + var target := material_resolution_for(res, float(terrain.get("size_x_m", 0.0)), + float(terrain.get("size_z_m", 0.0))) + var cur := material_resolution(terrain) + if cur >= target: + return false + var mats := resample_materials(decode_materials(terrain), cur, target) + terrain["material_resolution"] = target + terrain["materials_r8_b64"] = encode_materials(mats) + return true + + +## Material-ID am Höhenpunkt (x, z) des Höhenrasters — für Vertex-Farben. +static func material_at_height_point(mats: PackedByteArray, mres: int, res: int, + x: int, z: int) -> int: + if mres < 2 or res < 2 or mats.size() < mres * mres: + return 0 + var f := float(mres - 1) / float(res - 1) + return int(mats[mini(int(roundf(float(z) * f)), mres - 1) * mres + mini(int(roundf(float(x) * f)), mres - 1)]) + + ## Material-Schicht kodieren: uint8 je Sample (Werte 0..MATERIAL_IDS.size()-1). static func encode_materials(mats: PackedByteArray) -> String: var bytes := PackedByteArray() diff --git a/src/ui/main.gd b/src/ui/main.gd index 8561323..a8b0035 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -1211,6 +1211,10 @@ func _on_open_selected(path: String) -> void: _set_status("Laden fehlgeschlagen: %s — versuche eine .svmap-Datei aus dem Map-Editor oder vom Server." % path.get_file(), true) return doc = result["data"] + # Feines Material-Raster (Karte 24.10.): alte Karten beim Öffnen anheben — + # optisch gleich, ab jetzt ~1 m fein malbar. Die Datei ändert sich erst + # beim nächsten Speichern (deshalb kein dirty). + SvmapIO.upgrade_material_layer(doc.get("terrain", {})) current_path = path dirty = false history.begin(doc) @@ -2736,6 +2740,7 @@ func _on_resign_confirmed() -> void: _set_status("Laden fehlgeschlagen: %s" % _resign_pending_path.get_file(), true) return doc = result["data"] + SvmapIO.upgrade_material_layer(doc.get("terrain", {})) # feines Material-Raster current_path = _resign_pending_path dirty = true history.begin(doc) diff --git a/src/ui/panels/symmetry_panel.gd b/src/ui/panels/symmetry_panel.gd index 440eb81..1a00c2a 100644 --- a/src/ui/panels/symmetry_panel.gd +++ b/src/ui/panels/symmetry_panel.gd @@ -156,7 +156,7 @@ func _conflict_text(an: Dictionary) -> String: var obj_n := int(an["objects"]["diff"]) var teil_n := int(an["gebaeude"]["diff"]) var spawn_n := int(an["spawns"]["diff"]) - var boden_n := (an["terrain_cells"] as Array).size() + var boden_n := (an["terrain_cells"] as Array).size() + (an.get("material_cells", []) as Array).size() if obj_n > 0: parts.append("%d Objekt-Paare unterscheiden sich" % obj_n) if teil_n > 0: diff --git a/src/ui/panels/terrain_panel.gd b/src/ui/panels/terrain_panel.gd index 464ee16..437f944 100644 --- a/src/ui/panels/terrain_panel.gd +++ b/src/ui/panels/terrain_panel.gd @@ -260,14 +260,14 @@ func _on_terrain_mode_changed(_pressed: bool) -> void: ## Hinweistext Malen-Modus (pure, testbar): feinste Mal-Einheit = Abstand ## zweier Rasterpunkte der Karte (size / (resolution − 1)). static func paint_hint_text(terrain: Dictionary) -> String: - var res := int(terrain.get("resolution", 0)) + var res := SvmapIO.material_resolution(terrain) # feines Material-Raster var size := maxf(float(terrain.get("size_x_m", 0.0)), float(terrain.get("size_z_m", 0.0))) if res < 2 or size <= 0.0: return "Malen setzt das Material zellenweise — Stärke gibt es hier nicht." var m := size / float(res - 1) var m_text := str(int(roundf(m))) if absf(m - roundf(m)) < 0.05 \ else String.num(m, 1).replace(".", ",") - return "Malen setzt das Material zellenweise: feinste Einheit auf dieser Karte ≈ %s m. Stärke gibt es hier nicht — kleiner geht's nur mit einem feineren Kartenraster." % m_text + return "Malen setzt das Material zellenweise: feinste Einheit auf dieser Karte ≈ %s m. Stärke gibt es hier nicht." % m_text ## Material-Palette (Karte t_603ec1a8): gewähltes Material an den Pinsel. diff --git a/src/viewport/terrain_mesh.gd b/src/viewport/terrain_mesh.gd index 45bd3c9..c7a5f68 100644 --- a/src/viewport/terrain_mesh.gd +++ b/src/viewport/terrain_mesh.gd @@ -210,13 +210,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: + # Material-Schicht hat ihr eigenes (feines) Raster (Karte 24.10.) — mres + # aus der Länge (quadratisch); alte Karten: mres == res. + if mats.is_empty() and not _mats_cache.is_empty(): 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. + _mats_cache = mats + var mres := mat_res_of(mats, res) var last := float(res - 1) var positions := PackedVector3Array() var normals := PackedVector3Array() @@ -268,9 +268,8 @@ func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0 (hx_l - hx_r) * cell_z, 2.0 * cell_x * cell_z, (hz_d - hz_u) * cell_x).normalized() - var mat_id := 0 - if idx < mats.size(): - mat_id = clampi(mats[idx], 0, MATERIAL_COLORS.size() - 1) + var mat_id := clampi(SvmapIO.material_at_height_point(mats, mres, res, x, z), + 0, MATERIAL_COLORS.size() - 1) # Material-Farbe × Höhen-Kurve × Pseudo-Licht (Relief lesbar). var farbe := relief_color(mat_id, heights[idx], hmin, hmax, normals[idx]) # Ufer-Automatik (Karte t_28fdef03): Zellen knapp unter der Wasser- @@ -319,11 +318,20 @@ func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0 var shm := mat as ShaderMaterial shm.set_shader_parameter("map_size_x", size_x) shm.set_shader_parameter("map_size_z", size_z) - if splat_rect_cells.size.x > 0 and _splat_tex != null and _splat_res == res \ - and _splat_side == TerrainArrays.splat_side_for(res, maxf(size_x, size_z)): + if splat_rect_cells.size.x > 0 and _splat_tex != null and _splat_res == mres \ + and _splat_side == TerrainArrays.splat_side_for(mres, maxf(size_x, size_z)): _refresh_splat_region(splat_rect_cells) else: - _build_splat(shm, mats, res, size_x, size_z) + _build_splat(shm, mats, mres, size_x, size_z) + + +## Raster einer Material-Schicht aus ihrer Länge (quadratisch). Unpassend/leer +## → Höhenraster (alte Karten, Legacy ohne Schicht). +static func mat_res_of(mats: PackedByteArray, res: int) -> int: + if mats.is_empty(): + return res + var r := int(roundf(sqrt(float(mats.size())))) + return r if r >= 2 and r * r == mats.size() else res ## Höhenlinien-Uniform an die aktuelle Situation anpassen (Pref ∧ Relief ≥ 2 m). diff --git a/tests/probe_fein_export.gd b/tests/probe_fein_export.gd new file mode 100644 index 0000000..dd81f31 --- /dev/null +++ b/tests/probe_fein_export.gd @@ -0,0 +1,22 @@ +extends SceneTree +## Probe (Karte 24.10.): alte Karte öffnen → feines Raster → 1-m-Sandpfad malen +## → speichern. Ausgabe-Pfad = erstes User-Arg. Für die Client-E2E-Kette. +func _initialize() -> void: + var out := OS.get_cmdline_user_args()[0] + var ld := SvmapIO.load_from_file(ProjectSettings.globalize_path("res://FirstFight.svmap")) + if not bool(ld["ok"]): + print("PROBE_FAIL load ", ld["errors"]) + quit(1) + return + var doc: Dictionary = ld["data"] + var t: Dictionary = doc["terrain"] + SvmapIO.upgrade_material_layer(t) + var mres := SvmapIO.material_resolution(t) + var mats := SvmapIO.decode_materials(t) + for i in 40: + TerrainField.apply_material_brush(mats, mres, float(t["size_x_m"]), float(t["size_z_m"]), + -20.0 + float(i), 5.0, 0.6, 2) + t["materials_r8_b64"] = SvmapIO.encode_materials(mats) + var sv := SvmapIO.save_to_file(out, doc) + print("PROBE ", "OK" if bool(sv["ok"]) else "FAIL %s" % str(sv["errors"]), " mres=", mres) + quit(0 if bool(sv["ok"]) else 1) diff --git a/tests/probe_fein_export.gd.uid b/tests/probe_fein_export.gd.uid new file mode 100644 index 0000000..c2568c9 --- /dev/null +++ b/tests/probe_fein_export.gd.uid @@ -0,0 +1 @@ +uid://c2o3agxvqdool diff --git a/tests/test_klick_exakt.gd.uid b/tests/test_klick_exakt.gd.uid new file mode 100644 index 0000000..98abf38 --- /dev/null +++ b/tests/test_klick_exakt.gd.uid @@ -0,0 +1 @@ +uid://b6pfyfkje6v46 diff --git a/tests/test_mal_fein.gd b/tests/test_mal_fein.gd new file mode 100644 index 0000000..55cd84b --- /dev/null +++ b/tests/test_mal_fein.gd @@ -0,0 +1,128 @@ +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() + 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 diff --git a/tests/test_mal_fein.gd.uid b/tests/test_mal_fein.gd.uid new file mode 100644 index 0000000..08bbccf --- /dev/null +++ b/tests/test_mal_fein.gd.uid @@ -0,0 +1 @@ +uid://ckupi718fp51v diff --git a/tests/test_mal_hinweis.gd.uid b/tests/test_mal_hinweis.gd.uid new file mode 100644 index 0000000..6c42766 --- /dev/null +++ b/tests/test_mal_hinweis.gd.uid @@ -0,0 +1 @@ +uid://ceeiu5fnech7g diff --git a/tests/test_symmetry_onclick.gd b/tests/test_symmetry_onclick.gd index d82569b..7918b1a 100644 --- a/tests/test_symmetry_onclick.gd +++ b/tests/test_symmetry_onclick.gd @@ -71,9 +71,11 @@ func _asym_doc() -> Dictionary: terrain["heightmap_r16_b64"] = TerrainField.encode(heights, float(terrain["min_height_m"]), float(terrain["max_height_m"])) var mats := SvmapIO.decode_materials(terrain) - for j in range(60, 70): - for i in range(10, 20): - mats[j * res + i] = 1 # Sandfleck nur links + var mres := SvmapIO.material_resolution(terrain) # feines Raster (8 Punkte je Höhenzelle) + var k := (mres - 1) / (res - 1) + for j in range(60 * k, 70 * k): + for i in range(10 * k, 20 * k): + mats[j * mres + i] = 1 # Sandfleck nur links terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) (doc["objects"] as Array).append(_obj("crate_wood", [-100.0, 0.0, 40.0], 30.0)) # A (0) (doc["objects"] as Array).append(_obj("crate_wood", [100.0, 0.0, -40.0], 10.0)) # B (1) Widerspruch @@ -91,7 +93,6 @@ func _mirror_cell_equal(doc: Dictionary, mode: int) -> bool: if res < 2: return true var heights := SvmapIO.decode_heights(terrain) - var mats := SvmapIO.decode_materials(terrain) for j in res: for i in res: var mc := SymmetryCore.mirror_cell(mode, i, j, res) @@ -99,7 +100,13 @@ func _mirror_cell_equal(doc: Dictionary, mode: int) -> bool: continue if absf(heights[j * res + i] - heights[mc.y * res + mc.x]) > 0.0001: return false - if mats[j * res + i] != mats[mc.y * res + mc.x]: + # Material auf seinem eigenen (feinen) Raster (Karte 24.10.). + var mres := SvmapIO.material_resolution(terrain) + var mats := SvmapIO.decode_materials(terrain) + for j in mres: + for i in mres: + var mc := SymmetryCore.mirror_cell(mode, i, j, mres) + if mats[j * mres + i] != mats[mc.y * mres + mc.x]: return false return true @@ -129,7 +136,8 @@ func _test_analyze_asym_map() -> void: _check((spawns["singles_low"] as Array).size() == 2, "Spawns: 2 Solo-Punkte links (A4 + A9)") _check((an["terrain_cells"] as Array).size() > 0, "Boden: abweichende Zellen erkannt") _check(int(an["conflicts"]) == int(objects["diff"]) + int(spawns["diff"]) - + (an["terrain_cells"] as Array).size(), "Widerspruchs-Zahl = Objekt- + Spawn- + Boden-Widersprüche") + + (an["terrain_cells"] as Array).size() + (an["material_cells"] as Array).size(), + "Widerspruchs-Zahl = Objekt- + Spawn- + Boden-Widersprüche (Höhe + Material)") _check(int(an["adds"]) == 3, "3 fehlende Zwillinge (C, A4, A9)") _check(int(an["total_deviations"]) == int(an["_dev_total"]), "Abweichungs-Zähler konsistent") diff --git a/tests/test_terrain.gd b/tests/test_terrain.gd index 50fd68c..99a487f 100644 --- a/tests/test_terrain.gd +++ b/tests/test_terrain.gd @@ -278,17 +278,19 @@ func _test_paint_undo_redo() -> void: hist.begin(doc) var terrain: Dictionary = doc["terrain"] var mats := SvmapIO.decode_materials(terrain) - _check(mats.size() == 129 * 129, "Neue Map: Material-Schicht 129² (alles Gras)") + # Feines Material-Raster (Karte 24.10.): neue 1024-m-Maps = 1025² (1 m). + var mres := SvmapIO.material_resolution(terrain) + _check(mres == 1025 and mats.size() == mres * mres, "Neue Map: Material-Schicht 1025² (alles Gras)") var before: Dictionary = hist.snapshot(["terrain"]) for step in 6: # Streifen von links nach rechts, wie ein Zieh-Stroke - TerrainField.apply_material_brush(mats, 129, 1024.0, 1024.0, + TerrainField.apply_material_brush(mats, mres, 1024.0, 1024.0, -300.0 + float(step) * 60.0, 0.0, 24.0, 1) terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) hist.commit("Boden gemalt (sand)", before, hist.snapshot(["terrain"])) _check(hist.entries.size() == 1, "6 Mal-Schritte → EIN History-Eintrag") var painted := SvmapIO.decode_materials(terrain) - _check(painted[64 * 129 + 64] == 1, "Stroke-Mitte = Sand") - _check(painted[10 * 129 + 10] == 0, "Außerhalb des Streifens bleibt Gras") + _check(painted[512 * mres + 512] == 1, "Stroke-Mitte = Sand") + _check(painted[80 * mres + 80] == 0, "Außerhalb des Streifens bleibt Gras") var painted_b64 := str(terrain["materials_r8_b64"]) var desc := hist.undo() _check(desc == "Boden gemalt (sand)", "Undo-Desc nennt den Mal-Stroke") @@ -318,7 +320,8 @@ func _test_material_format() -> void: var load := SvmapIO.load_from_file(path) _check(load["ok"], "v2-Map lädt sauber zurück (inkl. Prüfsumme)") var mats := SvmapIO.decode_materials(load["data"]["terrain"]) - _check(mats.size() == 129 * 129, "v2: Material-Schicht komplett dekodiert") + var mres := SvmapIO.material_resolution(load["data"]["terrain"]) + _check(mats.size() == mres * mres and mres == 1025, "v2: Material-Schicht komplett dekodiert (1025²)") # V15: falsche Länge → Ablehnung. var bad: Dictionary = doc.duplicate(true) @@ -332,7 +335,7 @@ func _test_material_format() -> void: # 20 (erster Wert oberhalb der neuen Obergrenze). bad = doc.duplicate(true) var mraw := PackedByteArray() - mraw.resize(129 * 129) + mraw.resize(mres * mres) mraw[5000] = 20 (bad["terrain"] as Dictionary)["materials_r8_b64"] = Marshalls.raw_to_base64(mraw) _check(_has_err(SvmapIO.validate_map(bad, {}), "V15"), @@ -340,7 +343,7 @@ func _test_material_format() -> void: # Neu: GÜLTIGE neue ID (3 = gras_satt) muss DURCHLASSEN werden. var good_new := doc.duplicate(true) var gmraw := PackedByteArray() - gmraw.resize(129 * 129) + gmraw.resize(mres * mres) gmraw[5000] = 3 (good_new["terrain"] as Dictionary)["materials_r8_b64"] = Marshalls.raw_to_base64(gmraw) _check(not _has_err(SvmapIO.validate_map(good_new, {}), "V15"), @@ -350,6 +353,7 @@ func _test_material_format() -> void: var legacy: Dictionary = doc.duplicate(true) legacy["format_version"] = 1 (legacy["terrain"] as Dictionary).erase("materials_r8_b64") + (legacy["terrain"] as Dictionary).erase("material_resolution") # Alt-Karten kennen das Feld nicht SvmapIO.sign_map(legacy) var legacy_path := "/tmp/scopedvr_test_terrain_legacy_v1.svmap" var lsave := SvmapIO.save_to_file(legacy_path, legacy) @@ -459,6 +463,7 @@ func _test_live_material_colors() -> void: 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) + terrain.erase("material_resolution") # Test-Raster: Material = Höhenraster (Alt-Karte) var tmain := ToolMain.new() tmain.doc = doc var builder := ToolBuilder.new() @@ -941,6 +946,7 @@ func _test_resize_grow() -> void: terrain["max_height_m"] = ceilf(TerrainField._max_h(heights) + 1.0) terrain["heightmap_r16_b64"] = TerrainField.encode(heights, float(terrain["min_height_m"]), float(terrain["max_height_m"])) + terrain.erase("material_resolution") # Quelle = Alt-Karte (Material = Höhenraster) var mats := SvmapIO.decode_materials(terrain) TerrainField.apply_material_brush(mats, 129, 256.0, 256.0, 0.0, 0.0, 40.0, 1) terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) @@ -971,7 +977,9 @@ func _test_resize_grow() -> void: _check(grown_heights.size() == 257 * 257 and str(t_new["heightmap_r16_b64"]).length() > 0, "Heights-Bytes an neue res angepasst (257²)") - _check(grown_mats.size() == 257 * 257, "Material-Bytes an neue res angepasst (257²)") + # Feines Raster (24.10.): 512 m / res 257 → 2 m → Material-Raster 513 (1 m). + var gm := SvmapIO.material_resolution(t_new) + _check(gm == 513 and grown_mats.size() == gm * gm, "Material-Bytes auf feines Raster (513²)") var same_world := true var max_err := 0.0 for i in 129 * 129: @@ -1001,13 +1009,15 @@ func _test_resize_grow() -> void: var b: int = i * 257 + j if absf(grown_heights[a] - hmin) > 0.001 or absf(grown_heights[b] - hmin) > 0.001: border_flat = false - if int(grown_mats[a]) != 0 or int(grown_mats[b]) != 0: + for j in gm: + for i in [0, gm - 1]: + if int(grown_mats[j * gm + i]) != 0 or int(grown_mats[i * gm + j]) != 0: border_grass = false _check(border_flat, "Randzone: flach auf min_height (h=%s)" % str(hmin)) _check(border_grass, "Randzone: Material 0 (Gras)") # Material-Inhalt resampelt: gemalter Sand-Fleck bleibt Sand. - _check(int(grown_mats[128 * 257 + 128]) == 1, "Sand-Fleck bleibt Sand (Mitte)") + _check(int(grown_mats[256 * gm + 256]) == 1, "Sand-Fleck bleibt Sand (Mitte)") var old_center_mats := SvmapIO.decode_materials(doc["terrain"]) _check(int(old_center_mats[64 * 129 + 64]) == 1, "Gegenprobe: Quelle wirklich Sand") diff --git a/tests/test_terrain_mirror.gd b/tests/test_terrain_mirror.gd index c48a26d..f21b9d1 100644 --- a/tests/test_terrain_mirror.gd +++ b/tests/test_terrain_mirror.gd @@ -110,6 +110,7 @@ func _make_doc() -> Dictionary: var mats := PackedByteArray() mats.resize(RES * RES) terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) + terrain.erase("material_resolution") # Test-Raster: Material = Höhenraster (Alt-Karte) return doc