Map-Editor R1 (t_0d436687): Resize-Kern + Vergrößern — SvmapDocument.resize_terrain(), Anker Mitte/Rand flach, Dichte-Leiter 129→257→513.
Semantik aus PLAN.md „Map-Größe ändern" (17.10., Sly-geankert): - Anker Mitte: Karte bleibt am Ursprung zentriert, bestehendes Terrain behält seine WELTPOSITION (bilinear resampeln, kein Stretch). - Neue Randzone: flach auf min_height + Material 0 (Gras), wie create_new. - Dichte-Regel: würde m/Punkt über ~2 sinken, res hoch 129→257→513 (Format-Limit 513 hart — darüber Warnband, Anzeige in R3). - size clamp 256–4096 m (MIN_SIZE_M/MAX_SIZE_M), bounds auf neue Größe (Y-Grenzen unverändert). - R1-Schnitt: spawn_zones/objects/gebaeude unverändert übernommen (Filter + Out-of-Bounds-Dialog kommen in R2). Reine Funktion — Eingabe-doc bleibt unangetastet (Undo-Snapshot), alte Prüfsumme verworfen (save signiert neu). Tests (test_terrain.gd, 22 neue Checks → Suite 134/134, 11 Suiten ALL OK): Grow 256→512 — alte Gridpunkte ±0.01 m an Weltposition, Rand flach/Gras, Material resampelt, res-Bump 129→257 (bei 2048 m gedeckelt auf 513), spawn/object-Erhalt, svmap-v2-Roundtrip, Clamp.
This commit is contained in:
parent
b85e3bed52
commit
2d8b9822f0
2 changed files with 247 additions and 0 deletions
|
|
@ -137,3 +137,122 @@ static func _height_sample(bytes: PackedByteArray, res: int, xi: int, zi: int,
|
||||||
var i := (zi * res + xi) * 2
|
var i := (zi * res + xi) * 2
|
||||||
var raw := float(bytes[i]) + float(bytes[i + 1]) * 256.0
|
var raw := float(bytes[i]) + float(bytes[i + 1]) * 256.0
|
||||||
return hmin + (raw / 65535.0) * (hmax - hmin)
|
return hmin + (raw / 65535.0) * (hmax - hmin)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------- Kartengröße ändern ----
|
||||||
|
# Karte t_0d436687 / PLAN.md „Map-Größe ändern" (17.10., Sly-geankert).
|
||||||
|
# Semantik „Anker Mitte, Rand flach, Resample": die Karte bleibt am Ursprung
|
||||||
|
# zentriert, bestehendes Terrain behält seine WELTPOSITION (bilinear
|
||||||
|
# resampeln — kein Stretch), die neue Randzone wird flach (min_height) +
|
||||||
|
# Material Gras. Reine Funktion (altes Dict → NEUES Dict), damit R3 sie
|
||||||
|
# atomar hinter ein Undo-Snapshot hängen kann.
|
||||||
|
|
||||||
|
## Dichte-Ziel: nach einem Resize nicht grober als ~2 m/Punkt sampeln.
|
||||||
|
const RESIZE_DENSITY_TARGET_M := 2.0
|
||||||
|
## Auflösungs-Leiter beim Hochdrehen (Format erlaubt 17–513, V6).
|
||||||
|
const RESIZE_RESOLUTION_LADDER: Array[int] = [129, 257, 513]
|
||||||
|
|
||||||
|
|
||||||
|
## Auflösung nach einem Resize: kleinstmögliche Leiter-Sprosse ≥ alter res,
|
||||||
|
## die die ~2 m/Punkt-Dichte auf der GRÖSSTEN neuen Kante hält; über 513
|
||||||
|
## (Format-Maximum) nicht mehr hochdrehen → ab ~1026 m grobt es (Warnband,
|
||||||
|
## Anzeige kommt in R3).
|
||||||
|
static func _resize_resolution(old_res: int, new_sx: float, new_sz: float) -> int:
|
||||||
|
var res := old_res
|
||||||
|
for step in RESIZE_RESOLUTION_LADDER:
|
||||||
|
if step < res:
|
||||||
|
continue
|
||||||
|
var m_per_point := maxf(new_sx, new_sz) / float(step - 1)
|
||||||
|
if m_per_point <= RESIZE_DENSITY_TARGET_M:
|
||||||
|
return step
|
||||||
|
return SvmapIO.MAX_TERRAIN_RESOLUTION
|
||||||
|
|
||||||
|
|
||||||
|
## m/Punkt einer res/size-Kombination — R3-Dialog zeigt das Warnband ab
|
||||||
|
## ~1024 m (Dichte grobt, Quest-Perf: Objektzahl/Draw-Calls).
|
||||||
|
static func resize_m_per_point(res: int, size_m: float) -> float:
|
||||||
|
return size_m / float(maxi(res - 1, 1))
|
||||||
|
|
||||||
|
|
||||||
|
## Terrain auf neue Kartenmaße resampeln (Vergrößern; Verkleinern folgt in R2).
|
||||||
|
## R1-Schnitt (PLAN.md Kette): Spawn-Zonen, objects und gebaeude werden
|
||||||
|
## unverändert ÜBERNOMMEN — auch wenn etwas außerhalb des neuen Geländes
|
||||||
|
## liegt (Filter + Out-of-Bounds-Dialog kommen in R2). Kein Mutationsspaß:
|
||||||
|
## der Eingabe-doc bleibt unangetastet, das Ergebnis ist ein frisches Dict
|
||||||
|
## ohne Prüfsumme (die alte wäre nach dem Resize ungültig — save_to_file
|
||||||
|
## signiert beim nächsten Speichern neu).
|
||||||
|
static func resize_terrain(doc: Dictionary, new_size_x_m: float,
|
||||||
|
new_size_z_m: float) -> Dictionary:
|
||||||
|
var old_terrain: Dictionary = doc.get("terrain", {})
|
||||||
|
var old_res := int(old_terrain.get("resolution", DEFAULT_RESOLUTION))
|
||||||
|
var old_sx := float(old_terrain.get("size_x_m", 0.0))
|
||||||
|
var old_sz := float(old_terrain.get("size_z_m", 0.0))
|
||||||
|
var hmin := float(old_terrain.get("min_height_m", DEFAULT_MIN_HEIGHT))
|
||||||
|
var hmax := float(old_terrain.get("max_height_m", DEFAULT_MAX_HEIGHT))
|
||||||
|
|
||||||
|
var new_sx := clampf(new_size_x_m, MIN_SIZE_M, MAX_SIZE_M)
|
||||||
|
var new_sz := clampf(new_size_z_m, MIN_SIZE_M, MAX_SIZE_M)
|
||||||
|
var new_res := _resize_resolution(old_res, new_sx, new_sz)
|
||||||
|
|
||||||
|
var heights_old := SvmapIO.decode_heights(old_terrain)
|
||||||
|
var mats_old := SvmapIO.decode_materials(old_terrain)
|
||||||
|
|
||||||
|
var heights_new := PackedFloat32Array()
|
||||||
|
heights_new.resize(new_res * new_res)
|
||||||
|
var mats_new := PackedByteArray()
|
||||||
|
mats_new.resize(new_res * new_res)
|
||||||
|
|
||||||
|
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
|
||||||
|
for j in new_res:
|
||||||
|
var z := (float(j) / last_new - 0.5) * new_sz
|
||||||
|
for i in new_res:
|
||||||
|
var x := (float(i) / last_new - 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:
|
||||||
|
# Neue Randzone: flach auf min_height (wie create_new) + Gras.
|
||||||
|
heights_new[j * new_res + i] = hmin
|
||||||
|
continue
|
||||||
|
# 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)]
|
||||||
|
|
||||||
|
var terrain := {
|
||||||
|
"type": "heightmap",
|
||||||
|
"size_x_m": new_sx,
|
||||||
|
"size_z_m": new_sz,
|
||||||
|
"min_height_m": hmin,
|
||||||
|
"max_height_m": hmax,
|
||||||
|
"resolution": new_res,
|
||||||
|
"heightmap_r16_b64": TerrainField.encode(heights_new, hmin, hmax),
|
||||||
|
"materials_r8_b64": SvmapIO.encode_materials(mats_new),
|
||||||
|
}
|
||||||
|
|
||||||
|
# bounds wie create_new auf die neue Größe — Y-Grenzen vom alten doc
|
||||||
|
# behalten (Resize ändert nur die Grundfläche, nicht die Luft).
|
||||||
|
var half_x := new_sx / 2.0
|
||||||
|
var half_z := new_sz / 2.0
|
||||||
|
var old_bounds: Dictionary = doc.get("bounds", {})
|
||||||
|
var old_min: Array = old_bounds.get("min", [-half_x, -10.0, -half_z])
|
||||||
|
var old_max: Array = old_bounds.get("max", [half_x, 80.0, half_z])
|
||||||
|
return {
|
||||||
|
"format_version": SvmapIO.FORMAT_VERSION,
|
||||||
|
"info": (doc.get("info", {}) as Dictionary).duplicate(),
|
||||||
|
"terrain": terrain,
|
||||||
|
# R1: Objekte/Bauteile/Spawns unverändert übernehmen (Filter in R2).
|
||||||
|
"objects": (doc.get("objects", []) as Array).duplicate(),
|
||||||
|
"gebaeude": (doc.get("gebaeude", []) as Array).duplicate(),
|
||||||
|
"spawn_zones": (doc.get("spawn_zones", {}) as Dictionary).duplicate(true),
|
||||||
|
"bounds": {
|
||||||
|
"min": [-half_x, float(old_min[1]), -half_z],
|
||||||
|
"max": [half_x, float(old_max[1]), half_z],
|
||||||
|
},
|
||||||
|
"weather": doc.get("weather", ""),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ func _run_tests() -> void:
|
||||||
_test_live_material_colors()
|
_test_live_material_colors()
|
||||||
_test_relief_shading()
|
_test_relief_shading()
|
||||||
_test_grid_toggle()
|
_test_grid_toggle()
|
||||||
|
_test_resize_grow()
|
||||||
# _test_ray_terrain_mapping läuft in _finish() — braucht Viewport im Baum.
|
# _test_ray_terrain_mapping läuft in _finish() — braucht Viewport im Baum.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -901,3 +902,130 @@ func _mean(a: PackedFloat32Array) -> float:
|
||||||
for v in a:
|
for v in a:
|
||||||
s += v
|
s += v
|
||||||
return s / float(a.size())
|
return s / float(a.size())
|
||||||
|
|
||||||
|
|
||||||
|
## Resize-Kern (Karte t_0d436687, Plan „Map-Größe ändern" 17.10.):
|
||||||
|
## Vergrößern mit „Anker Mitte, Rand flach" — altes Terrain behält seine
|
||||||
|
## Weltposition (Resample, kein Stretch), neue Randzone = min_height + Gras,
|
||||||
|
## Dichte-Leiter 129→257→513, Spawn-Zonen/Objekte unverändert übernommen,
|
||||||
|
## svmap-v2-Roundtrip nach dem Resize.
|
||||||
|
func _test_resize_grow() -> void:
|
||||||
|
print("Testgruppe: Resize Vergrößern (Anker Mitte, Rand flach)")
|
||||||
|
var doc := SvmapDocument.create_new("Resize", 256.0, "mittag")
|
||||||
|
var terrain: Dictionary = doc["terrain"]
|
||||||
|
var heights := SvmapIO.decode_heights(terrain)
|
||||||
|
# Geformtes Terrain: Hügel + Senke (wie im Roundtrip-Test), plus Sand-Fleck.
|
||||||
|
TerrainField.apply_brush(heights, 129, 256.0, 256.0, -100000.0, 100000.0,
|
||||||
|
-64.0, -32.0, 40.0, TerrainField.Mode.RAISE, 18.0)
|
||||||
|
TerrainField.apply_brush(heights, 129, 256.0, 256.0, -100000.0, 100000.0,
|
||||||
|
64.0, 48.0, 30.0, TerrainField.Mode.LOWER, 9.0)
|
||||||
|
terrain["min_height_m"] = floorf(TerrainField._min_h(heights) - 1.0)
|
||||||
|
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"]))
|
||||||
|
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)
|
||||||
|
doc["objects"] = [{"id": "baum_laub_1", "pos": [80.0, 0.0, 40.0], "scale": 1.0}]
|
||||||
|
|
||||||
|
var old_heights_b64 := str(terrain["heightmap_r16_b64"])
|
||||||
|
var spawn_before := JSON.stringify(doc["spawn_zones"])
|
||||||
|
var grown := SvmapDocument.resize_terrain(doc, 512.0, 512.0)
|
||||||
|
|
||||||
|
# Reine Funktion: Eingabe-doc unangetastet (Undo-Snapshot bleibt gültig).
|
||||||
|
_check(str(doc["terrain"]["heightmap_r16_b64"]) == old_heights_b64
|
||||||
|
and int(doc["terrain"]["resolution"]) == 129
|
||||||
|
and absf(float(doc["terrain"]["size_x_m"]) - 256.0) < 0.0001,
|
||||||
|
"Reine Funktion: Eingabe-doc bleibt unangetastet")
|
||||||
|
|
||||||
|
var t_new: Dictionary = grown["terrain"]
|
||||||
|
_check(absf(float(t_new["size_x_m"]) - 512.0) < 0.0001
|
||||||
|
and absf(float(t_new["size_z_m"]) - 512.0) < 0.0001, "Größe 256→512 m")
|
||||||
|
_check(int(t_new["resolution"]) == 257,
|
||||||
|
"Dichte-Leiter: 4 m/Punkt → res 129→257 hochgedreht")
|
||||||
|
_check(SvmapDocument.resize_m_per_point(int(t_new["resolution"]), 512.0) <= 2.0,
|
||||||
|
"Nach dem Grow wieder ≤ 2 m/Punkt")
|
||||||
|
|
||||||
|
# Anker Mitte: Höhen an ALTEN Grid-Punkten (Weltposition) identisch —
|
||||||
|
# res 257 über 512 m hat dieselbe Punktlage wie res 129 über 256 m.
|
||||||
|
var grown_heights := SvmapIO.decode_heights(t_new)
|
||||||
|
var grown_mats := SvmapIO.decode_materials(t_new)
|
||||||
|
_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²)")
|
||||||
|
var same_world := true
|
||||||
|
var max_err := 0.0
|
||||||
|
for i in 129 * 129:
|
||||||
|
var xi := i % 129
|
||||||
|
var zi := i / 129
|
||||||
|
var x := (float(xi) / 128.0 - 0.5) * 256.0
|
||||||
|
var z := (float(zi) / 128.0 - 0.5) * 256.0
|
||||||
|
var j := int(roundf((z / 512.0 + 0.5) * 256.0)) * 257 \
|
||||||
|
+ int(roundf((x / 512.0 + 0.5) * 256.0))
|
||||||
|
var err := absf(grown_heights[j] - heights[i])
|
||||||
|
if err > max_err:
|
||||||
|
max_err = err
|
||||||
|
if err > 0.01:
|
||||||
|
same_world = false
|
||||||
|
_check(same_world, "Anker Mitte: alle alten Gridpunkte ±0.01 m (max %.4f m)" % max_err)
|
||||||
|
_check(absf(SvmapDocument.terrain_height_at(grown, -64.0, -32.0)
|
||||||
|
- SvmapDocument.terrain_height_at(doc, -64.0, -32.0)) < 0.05,
|
||||||
|
"Hügel-Kuppe an Weltposition erhalten (kein Stretch)")
|
||||||
|
|
||||||
|
# Neue Randzone: flach auf min_height + Material Gras.
|
||||||
|
var hmin := float(t_new["min_height_m"])
|
||||||
|
var border_flat := true
|
||||||
|
var border_grass := true
|
||||||
|
for j in 257:
|
||||||
|
for i in [0, 256]:
|
||||||
|
var a: int = j * 257 + i
|
||||||
|
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:
|
||||||
|
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)")
|
||||||
|
var old_center_mats := SvmapIO.decode_materials(doc["terrain"])
|
||||||
|
_check(int(old_center_mats[64 * 129 + 64]) == 1, "Gegenprobe: Quelle wirklich Sand")
|
||||||
|
|
||||||
|
# Spawns/Objekte unverändert übernommen (Filter kommt in R2).
|
||||||
|
_check(JSON.stringify(grown["spawn_zones"]) == spawn_before,
|
||||||
|
"Spawn-Zonen unverändert übernommen")
|
||||||
|
_check((grown["objects"] as Array).size() == 1
|
||||||
|
and str((grown["objects"] as Array)[0]["id"]) == "baum_laub_1",
|
||||||
|
"Objekte unverändert übernommen")
|
||||||
|
_check((grown["gebaeude"] as Array).is_empty() and not grown.has("checksum"),
|
||||||
|
"Bauteile übernommen, alte Prüfsumme verworfen")
|
||||||
|
_check(absf(float(grown["bounds"]["max"][0]) - 256.0) < 0.0001
|
||||||
|
and absf(float(grown["bounds"]["min"][2]) + 256.0) < 0.0001
|
||||||
|
and absf(float(grown["bounds"]["max"][1]) - 80.0) < 0.0001,
|
||||||
|
"bounds auf neue Größe (Y-Grenzen bleiben)")
|
||||||
|
|
||||||
|
# svmap-v2-Roundtrip: speichern → laden → Prüfsumme + byte-identisches Feld.
|
||||||
|
var path := "/tmp/scopedvr_test_terrain_resize.svmap"
|
||||||
|
var save := SvmapIO.save_to_file(path, grown)
|
||||||
|
_check(save["ok"], "Resize-Ergebnis speichert valide (V1–V14)")
|
||||||
|
var load := SvmapIO.load_from_file(path)
|
||||||
|
_check(load["ok"], "Resize-Ergebnis lädt mit gültiger Prüfsumme")
|
||||||
|
if load["ok"]:
|
||||||
|
_check(str(load["data"]["terrain"]["heightmap_r16_b64"])
|
||||||
|
== str(t_new["heightmap_r16_b64"]),
|
||||||
|
"Roundtrip: Höhenfeld byte-identisch nach Speichern→Laden")
|
||||||
|
|
||||||
|
# Dichte-Deckel: über ~1026 m NICHT über 513 hochdrehen (Warnband, R3).
|
||||||
|
var huge := SvmapDocument.resize_terrain(doc, 2048.0, 2048.0)
|
||||||
|
_check(int(huge["terrain"]["resolution"]) == SvmapIO.MAX_TERRAIN_RESOLUTION,
|
||||||
|
"Grow über ~1026 m: res bleibt 513 (Format-Max), kein Bump darüber")
|
||||||
|
_check(SvmapDocument.resize_m_per_point(513, 2048.0) > 2.0,
|
||||||
|
"Warnband: 2048 m bei res 513 grober als 2 m/Punkt")
|
||||||
|
|
||||||
|
# Clamp auf MIN/MAX (256/4096).
|
||||||
|
var clamped := SvmapDocument.resize_terrain(doc, 10.0, 100000.0)
|
||||||
|
_check(absf(float(clamped["terrain"]["size_x_m"]) - SvmapDocument.MIN_SIZE_M) < 0.0001
|
||||||
|
and absf(float(clamped["terrain"]["size_z_m"]) - SvmapDocument.MAX_SIZE_M) < 0.0001,
|
||||||
|
"Clamp: 10 m→256 m, 100000 m→4096 m")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue