Map-Editor R2 (t_27ba6c37): Verkleinern + Out-of-Bounds-Liste + Filter — resize_preview() listet je Kategorie (Objekte nach Anker-Regel, Bauteile nach Fußabdruck-"punkte" mit Rotation, Spawn-Punkte nach Punkt-Regel; Kante exakt = drin, Rand-Überlapper bleiben unange tastet), resize_apply(doc, x, z, drop_outside) resampelt wie R1 und entfernt bei Bedarf genau die gelisteten Einträge (Eingabe-doc bleibt rein). Shrink 512→256: Terrain an Weltposition 1:1 (±0.05 m), res bleibt 129 (2 m/Punkt). Tests: 19 neue Checks in test_terrain (Preview/Filter/Roundtrip), 11 Suiten ALL OK (920 Checks).
This commit is contained in:
parent
2d8b9822f0
commit
116acd63ed
2 changed files with 348 additions and 4 deletions
|
|
@ -174,10 +174,13 @@ 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ß:
|
||||
## Terrain auf neue Kartenmaße resampeln (Vergrößern UND Verkleinern —
|
||||
## beides geht über dieselbe Anker-Mitte-Resample-Schleife: beim Shrink liegt
|
||||
## jeder neue Gridpunkt innerhalb der alten Fläche, die Rand-flach-Regel
|
||||
## greift dann nie). R1/R2-Schnitt (PLAN.md Kette): Spawn-Zonen, objects und
|
||||
## gebaeude werden unverändert ÜBERNOMMEN — auch wenn etwas außerhalb des
|
||||
## neuen Geländes liegt (Filter nur mit drop_outside via resize_apply,
|
||||
## Out-of-Bounds-Dialog-Liste via resize_preview). 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).
|
||||
|
|
@ -256,3 +259,172 @@ static func resize_terrain(doc: Dictionary, new_size_x_m: float,
|
|||
},
|
||||
"weather": doc.get("weather", ""),
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------- R2: Out-of-Bounds-Liste ----
|
||||
# Karte t_27ba6c37 (PLAN.md „Map-Größe ändern", Punkt 3): beim Verkleinern
|
||||
# bleiben Objekte/Bauteile/Spawns-Kategorien erhalten, ABER der Dialog listet
|
||||
# vorher, was komplett außerhalb des neuen Geländes läge. Semantik:
|
||||
# - Komplett außerhalb → gelistet (resize_apply(drop_outside=true) entfernt).
|
||||
# - Teils am Rand überlappend → gelten als DRIN, werden unangetastet
|
||||
# übernommen (kein Clampen-Magie — sie dürfen über die Kante ragen).
|
||||
# Fußabdruck-Quellen je Kategorie (doc-lokale Fakten, kein Manifest nötig):
|
||||
# - Objekte: Anker-Punkt pos (svmap speichert keinen Objekt-Fußabdruck; die
|
||||
# Bibliothek lebt außerhalb der Map-Datei → die Anker-Regel ist die
|
||||
# dokumentierte Wahrheit, R3 zeigt im Dialog was passiert).
|
||||
# - Bauteile: das Teil trägt seinen eigenen Fußabdruck ("punkte", lokal,
|
||||
# mit "rot" gedreht) — EIN Eckpunkt innerhalb der neuen Grenzen = drin.
|
||||
# - Spawn-Punkte: Punkte (der Zonen-Kreis ist Editor-Visualisierung, kein
|
||||
# Formatfeld — SpawnOps-Kopf) → Punkt-Regel.
|
||||
# Grenzfall eps wie resize_terrain: exakt auf der Kante zählt als drin.
|
||||
|
||||
|
||||
static func _xz_in_bounds(x: float, z: float, half_x: float, half_z: float,
|
||||
eps: float) -> bool:
|
||||
return x >= -half_x - eps and x <= half_x + eps \
|
||||
and z >= -half_z - eps and z <= half_z + eps
|
||||
|
||||
|
||||
## Out-of-Bounds-Vorschau für den R3-Dialog (keine Mutation, keine Terrain-
|
||||
## Arbeit). Rückgabe:
|
||||
## objects_out: Indizes in doc["objects"] (Reihenfolge aufsteigend)
|
||||
## parts_out: Indizes in doc["gebaeude"]
|
||||
## spawns_out: Liste {"team": 0|1, "index": int, "id": String} je Punkt
|
||||
## Die Größen werden wie in resize_terrain geclampt (256–4096 m), damit
|
||||
## Preview und Apply garantiert dasselbe Gebiet meinen.
|
||||
static func resize_preview(doc: Dictionary, new_size_x_m: float,
|
||||
new_size_z_m: float) -> Dictionary:
|
||||
var half_x := clampf(new_size_x_m, MIN_SIZE_M, MAX_SIZE_M) * 0.5
|
||||
var half_z := clampf(new_size_z_m, MIN_SIZE_M, MAX_SIZE_M) * 0.5
|
||||
var eps := 0.0001
|
||||
|
||||
var objects_out: Array = []
|
||||
var objects: Array = doc.get("objects", [])
|
||||
for i in objects.size():
|
||||
var obj = objects[i]
|
||||
if not (obj is Dictionary):
|
||||
continue
|
||||
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
|
||||
if (pos as Array).size() < 3:
|
||||
continue
|
||||
if not _xz_in_bounds(float(pos[0]), float(pos[2]), half_x, half_z, eps):
|
||||
objects_out.append(i)
|
||||
|
||||
var parts_out: Array = []
|
||||
var pieces: Array = doc.get("gebaeude", [])
|
||||
for i in pieces.size():
|
||||
var teil = pieces[i]
|
||||
if not (teil is Dictionary):
|
||||
continue
|
||||
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
|
||||
if (pos as Array).size() < 3:
|
||||
continue
|
||||
var punkte = teil.get("punkte", [])
|
||||
var inside := false
|
||||
if not (punkte is Array) or (punkte as Array).is_empty():
|
||||
# Kein Fußabdruck bekannt → Anker-Punkt entscheidet.
|
||||
inside = _xz_in_bounds(float(pos[0]), float(pos[2]),
|
||||
half_x, half_z, eps)
|
||||
else:
|
||||
var rot: float = deg_to_rad(float(teil.get("rot", 0.0)))
|
||||
var c: float = cos(rot)
|
||||
var s: float = sin(rot)
|
||||
for p in punkte:
|
||||
if not (p is Array) or (p as Array).size() < 3:
|
||||
continue
|
||||
# Lokaler Fußabdruck-Punkt → Welt (Rotation um +Y, Godot-Basis):
|
||||
# x' = x·cos + z·sin, z' = −x·sin + z·cos.
|
||||
var wx: float = float(pos[0]) + float(p[0]) * c + float(p[2]) * s
|
||||
var wz: float = float(pos[2]) - float(p[0]) * s + float(p[2]) * c
|
||||
if _xz_in_bounds(wx, wz, half_x, half_z, eps):
|
||||
inside = true
|
||||
break
|
||||
if not inside:
|
||||
parts_out.append(i)
|
||||
|
||||
var spawns_out: Array = []
|
||||
var zones = doc.get("spawn_zones", {})
|
||||
if zones is Dictionary:
|
||||
for team_key in (zones as Dictionary).keys():
|
||||
var zone = zones[team_key]
|
||||
if not (zone is Dictionary):
|
||||
continue
|
||||
var points = (zone as Dictionary).get("points", [])
|
||||
if not (points is Array):
|
||||
continue
|
||||
for pi in (points as Array).size():
|
||||
var pt = points[pi]
|
||||
if not (pt is Dictionary):
|
||||
continue
|
||||
var pos: Array = pt.get("pos", [])
|
||||
if (pos as Array).size() < 3:
|
||||
continue
|
||||
if not _xz_in_bounds(float(pos[0]), float(pos[2]),
|
||||
half_x, half_z, eps):
|
||||
spawns_out.append({
|
||||
"team": int(team_key),
|
||||
"index": pi,
|
||||
"id": str(pt.get("id", "")),
|
||||
})
|
||||
return {
|
||||
"objects_out": objects_out,
|
||||
"parts_out": parts_out,
|
||||
"spawns_out": spawns_out,
|
||||
}
|
||||
|
||||
|
||||
## Resize anwenden (Terrain resampeln via resize_terrain) und optional die
|
||||
## Out-of-Bounds-Einträge ENTFERNEN. drop_outside=false → Ergebnis identisch
|
||||
## zu resize_terrain (reine Preview-Nutzung im Dialog). drop_outside=true →
|
||||
## die in resize_preview gelisteten Objekte/Bauteile/Spawn-Punkte werden aus
|
||||
## dem ERGEBNIS entfernt (der Eingabe-doc bleibt auch dann unangetastet —
|
||||
## Undo-Snapshot-Sicherheit wie in R1).
|
||||
static func resize_apply(doc: Dictionary, new_size_x_m: float,
|
||||
new_size_z_m: float, drop_outside: bool) -> Dictionary:
|
||||
var result := resize_terrain(doc, new_size_x_m, new_size_z_m)
|
||||
if not drop_outside:
|
||||
return result
|
||||
var preview := resize_preview(doc, new_size_x_m, new_size_z_m)
|
||||
|
||||
# Absteigend löschen, damit die Indizes der Preview stabil bleiben.
|
||||
var out_idx: Array = (preview["objects_out"] as Array).duplicate()
|
||||
out_idx.sort()
|
||||
out_idx.reverse()
|
||||
var objects: Array = result.get("objects", [])
|
||||
for i in out_idx:
|
||||
if int(i) < objects.size():
|
||||
objects.remove_at(int(i))
|
||||
|
||||
var parts_idx: Array = (preview["parts_out"] as Array).duplicate()
|
||||
parts_idx.sort()
|
||||
parts_idx.reverse()
|
||||
var gebaeude: Array = result.get("gebaeude", [])
|
||||
for i in parts_idx:
|
||||
if int(i) < gebaeude.size():
|
||||
gebaeude.remove_at(int(i))
|
||||
|
||||
# Spawn-Punkte je Team rausfiltern (Struktur bleibt: Teams ohne Punkte
|
||||
# bleiben als Zone-Block erhalten — der Spawn-Editor füllt sie wieder).
|
||||
var spawns_out: Array = preview["spawns_out"]
|
||||
var per_team := {0: {}, 1: {}} # team → {punkt-index: true}
|
||||
for entry in spawns_out:
|
||||
if entry is Dictionary:
|
||||
per_team[int(entry.get("team", -1))][int(entry.get("index", -1))] = true
|
||||
var zones = result.get("spawn_zones", {})
|
||||
if zones is Dictionary:
|
||||
for team_key in (zones as Dictionary).keys():
|
||||
var zone = zones[team_key]
|
||||
if not (zone is Dictionary):
|
||||
continue
|
||||
var points = (zone as Dictionary).get("points", [])
|
||||
if not (points is Array):
|
||||
continue
|
||||
var drop: Dictionary = per_team.get(int(team_key), {})
|
||||
if drop.is_empty():
|
||||
continue
|
||||
var kept: Array = []
|
||||
for pi in (points as Array).size():
|
||||
if not drop.has(pi):
|
||||
kept.append(points[pi])
|
||||
zone["points"] = kept
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ func _run_tests() -> void:
|
|||
_test_relief_shading()
|
||||
_test_grid_toggle()
|
||||
_test_resize_grow()
|
||||
_test_resize_shrink()
|
||||
# _test_ray_terrain_mapping läuft in _finish() — braucht Viewport im Baum.
|
||||
|
||||
|
||||
|
|
@ -1029,3 +1030,174 @@ func _test_resize_grow() -> void:
|
|||
_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")
|
||||
|
||||
|
||||
## R2 (t_27ba6c37): Verkleinern 512→256 m + Out-of-Bounds-Liste + Filter.
|
||||
## Semantik „Anker Mitte": Terrain behält Weltposition, Heights/Material
|
||||
## außerhalb fliegen weg. Objekte/Bauteile/Spawns bleiben in resize_terrain
|
||||
## unangetastet — resize_preview listet, was komplett außerhalb läge,
|
||||
## resize_apply(drop_outside=true) entfernt genau diese Einträge.
|
||||
func _test_resize_shrink() -> void:
|
||||
print("Testgruppe: Resize Verkleinern + Out-of-Bounds-Liste + Filter (R2)")
|
||||
var doc := SvmapDocument.create_new("Shrink", 512.0, "mittag")
|
||||
var terrain: Dictionary = doc["terrain"]
|
||||
var heights := SvmapIO.decode_heights(terrain)
|
||||
# Geformtes Terrain: Hügel nahe der künftigen +X-Kante (bleibt drin),
|
||||
# Senke weit draußen bei (-200, 150) — ihr Inhalt fällt beim Shrink weg.
|
||||
TerrainField.apply_brush(heights, 129, 512.0, 512.0, -100000.0, 100000.0,
|
||||
100.0, 0.0, 40.0, TerrainField.Mode.RAISE, 18.0)
|
||||
TerrainField.apply_brush(heights, 129, 512.0, 512.0, -100000.0, 100000.0,
|
||||
-200.0, 150.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, 512.0, 512.0, 100.0, 0.0, 40.0, 1)
|
||||
terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats)
|
||||
|
||||
# Objekte drin/draußen gemischt (eins EXAKT auf der Kante = drin, eps).
|
||||
doc["objects"] = [
|
||||
{"id": "crate_wood", "pos": [50.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
{"id": "crate_wood", "pos": [128.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
{"id": "crate_wood", "pos": [200.0, 0.0, -80.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
{"id": "crate_wood", "pos": [-300.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||
]
|
||||
# Bauteile: Wand mit Fußabdruck ÜBER der Kante (Punkte 127→129 m) = drin,
|
||||
# Fundament komplett draußen, gedrehte Wand über der -Z-Kante = drin.
|
||||
doc["gebaeude"] = [
|
||||
BuildOps.make_piece("wand", Vector3(127.0, 0.0, 10.0), 0.0),
|
||||
BuildOps.make_piece("fundament", Vector3(150.0, 0.0, -50.0), 0.0),
|
||||
BuildOps.make_piece("wand", Vector3(0.0, 0.0, -127.0), 90.0),
|
||||
]
|
||||
# Spawn-Punkte drin/draußen gemischt (Default-Zonen von 512 m lägen alle
|
||||
# bei ±204,8 m draußen — hier bewusst gemischt, damit nach dem Filter
|
||||
# beide Teams ≥ 4 Punkte behalten und die Map speicherbar bleibt, V11).
|
||||
doc["spawn_zones"] = {
|
||||
"0": {"label": "Team Blau", "points": [
|
||||
{"id": "A1", "pos": [100.0, 0.2, 0.0]},
|
||||
{"id": "A2", "pos": [300.0, 0.2, 0.0]},
|
||||
{"id": "A3", "pos": [60.0, 0.2, 40.0]},
|
||||
{"id": "A4", "pos": [-100.0, 0.2, -60.0]},
|
||||
{"id": "A5", "pos": [20.0, 0.2, -80.0]},
|
||||
]},
|
||||
"1": {"label": "Team Rot", "points": [
|
||||
{"id": "B1", "pos": [-204.8, 0.2, 0.0]},
|
||||
{"id": "B2", "pos": [0.0, 0.2, -100.0]},
|
||||
{"id": "B3", "pos": [40.0, 0.2, 80.0]},
|
||||
{"id": "B4", "pos": [-50.0, 0.2, 100.0]},
|
||||
{"id": "B5", "pos": [-20.0, 0.2, -80.0]},
|
||||
]},
|
||||
}
|
||||
var doc_json_before := JSON.stringify(doc)
|
||||
|
||||
# --- resize_preview: Liste je Kategorie, doc bleibt unangetastet ---------
|
||||
var preview := SvmapDocument.resize_preview(doc, 256.0, 256.0)
|
||||
_check(str(doc_json_before) == JSON.stringify(doc),
|
||||
"Preview: reine Funktion, Eingabe-doc unangetastet")
|
||||
var objects_out: Array = preview["objects_out"]
|
||||
_check(objects_out.size() == 2 and int(objects_out[0]) == 2
|
||||
and int(objects_out[1]) == 3,
|
||||
"Preview objects_out: Indizes 2+3 draußen, Kante-Objekt (128 m) drin")
|
||||
var parts_out: Array = preview["parts_out"]
|
||||
_check(parts_out.size() == 1 and int(parts_out[0]) == 1,
|
||||
"Preview parts_out: nur Fundament draußen, Rand-überlappende Wände drin")
|
||||
var spawns_out: Array = preview["spawns_out"]
|
||||
var spawn_keys := []
|
||||
for entry in spawns_out:
|
||||
spawn_keys.append("%d:%s" % [int(entry["team"]), str(entry["id"])])
|
||||
spawn_keys.sort()
|
||||
_check(spawn_keys == ["0:A2", "1:B1"],
|
||||
"Preview spawns_out: A2 (300 m) + B1 (-204,8 m) draußen")
|
||||
var grow_preview := SvmapDocument.resize_preview(doc, 1024.0, 1024.0)
|
||||
_check((grow_preview["objects_out"] as Array).is_empty()
|
||||
and (grow_preview["parts_out"] as Array).is_empty()
|
||||
and (grow_preview["spawns_out"] as Array).is_empty(),
|
||||
"Preview beim Vergrößern: nichts draußen")
|
||||
var clamp_preview := SvmapDocument.resize_preview(doc, 100.0, 512.0)
|
||||
_check((clamp_preview["objects_out"] as Array).size() == 2,
|
||||
"Preview clamp: 100 m→256 m zählt wie 256 m")
|
||||
|
||||
# --- resize_apply ohne Filter: wie resize_terrain ------------------------
|
||||
var kept := SvmapDocument.resize_apply(doc, 256.0, 256.0, false)
|
||||
_check((kept["objects"] as Array).size() == 4
|
||||
and (kept["gebaeude"] as Array).size() == 3
|
||||
and (kept["spawn_zones"]["0"]["points"] as Array).size() == 5,
|
||||
"Apply ohne Filter: Objekte/Bauteile/Spawns unverändert übernommen")
|
||||
_check(absf(float(kept["terrain"]["size_x_m"]) - 256.0) < 0.0001
|
||||
and absf(float(kept["terrain"]["size_z_m"]) - 256.0) < 0.0001
|
||||
and int(kept["terrain"]["resolution"]) == 129,
|
||||
"Shrink 512→256 m: Größe ok, Dichte 2 m/Punkt hält res 129")
|
||||
|
||||
# Drin bleibt 1:1: alle ALTEN Gridpunkte innerhalb des neuen Gebiets
|
||||
# (Vierer-Raster = exakte Knoten) stimmen ±0.05 m (R16-Quantisierung).
|
||||
var old_res := 129
|
||||
var old_sx := 512.0
|
||||
var old_sz := 512.0
|
||||
var new_doc := SvmapDocument.resize_apply(doc, 256.0, 256.0, true)
|
||||
var max_err := 0.0
|
||||
var world_kept := true
|
||||
for zi in old_res:
|
||||
for xi in old_res:
|
||||
var x := (float(xi) / float(old_res - 1) - 0.5) * old_sx
|
||||
var z := (float(zi) / float(old_res - 1) - 0.5) * old_sz
|
||||
if absf(x) > 124.0 or absf(z) > 124.0:
|
||||
continue
|
||||
var expected := heights[zi * old_res + xi]
|
||||
var actual := SvmapDocument.terrain_height_at(new_doc, x, z)
|
||||
var err := absf(actual - expected)
|
||||
if err > max_err:
|
||||
max_err = err
|
||||
if err > 0.05:
|
||||
world_kept = false
|
||||
_check(world_kept,
|
||||
"Anker Mitte beim Shrink: alter Grid drin ±0.05 m (max %.4f m)" % max_err)
|
||||
_check(absf(SvmapDocument.terrain_height_at(new_doc, 100.0, 0.0)
|
||||
- SvmapDocument.terrain_height_at(doc, 100.0, 0.0)) < 0.05,
|
||||
"Hügel an der künftigen Kante (100 m) an Weltposition erhalten")
|
||||
# Terrain außerhalb fliegt weg: der Hügel ragt über die neue Kante hinaus
|
||||
# (RAISE bei 100 m, Radius 40) — der neue Rand trägt die ALTE Höhe an
|
||||
# derselben Weltposition (Anker Mitte), ist aber NICHT flach abgeschnitten.
|
||||
_check(absf(SvmapDocument.terrain_height_at(new_doc, 128.0, 0.0)
|
||||
- SvmapDocument.terrain_height_at(doc, 128.0, 0.0)) < 0.05
|
||||
and SvmapDocument.terrain_height_at(new_doc, 128.0, 0.0)
|
||||
> float(new_doc["terrain"]["min_height_m"]) + 0.5,
|
||||
"Kante beim Shrink: neuer Rand = alte Höhe an Weltposition (kein flacher Schnitt)")
|
||||
|
||||
# --- Filter: genau die gelisteten Einträge raus, Rest 1:1 ----------------
|
||||
var filtered_objects: Array = new_doc["objects"]
|
||||
_check(filtered_objects.size() == 2
|
||||
and str(filtered_objects[0]["pos"][0]) == "50.0"
|
||||
and absf(float(filtered_objects[1]["pos"][0]) - 128.0) < 0.0001,
|
||||
"Filter: 2 Objekte raus, drin bleibt 1:1 (Position unangetastet)")
|
||||
var filtered_parts: Array = new_doc["gebaeude"]
|
||||
_check(filtered_parts.size() == 2
|
||||
and str(filtered_parts[0]["id"]) == "wand"
|
||||
and absf(float(filtered_parts[0]["pos"][0]) - 127.0) < 0.0001
|
||||
and absf(float(filtered_parts[1]["rot"]) - 90.0) < 0.0001,
|
||||
"Filter: Fundament raus, Rand-Wände bleiben (kein Clampen)")
|
||||
var zones: Dictionary = new_doc["spawn_zones"]
|
||||
_check((zones["0"]["points"] as Array).size() == 4
|
||||
and (zones["1"]["points"] as Array).size() == 4
|
||||
and str(zones["0"]["label"]) == "Team Blau",
|
||||
"Filter: A2/B1 raus, Team-Struktur bleibt (je 4 Punkte, V11)")
|
||||
# Reihenfolge/IDs der verbleibenden Spawn-Punkte unangetastet.
|
||||
var ids0 := []
|
||||
for p in zones["0"]["points"]:
|
||||
ids0.append(str(p["id"]))
|
||||
_check(ids0 == ["A1", "A3", "A4", "A5"],
|
||||
"Filter: Spawn-Punkte ohne Umnummerierung entfernt (A2 fehlt)")
|
||||
|
||||
# --- Roundtrip: gefiltertes Shrink-Ergebnis speichert + lädt -------------
|
||||
var path := "/tmp/scopedvr_test_terrain_shrink.svmap"
|
||||
var save := SvmapIO.save_to_file(path, new_doc)
|
||||
_check(save["ok"], "Gefiltertes Shrink-Ergebnis speichert valide (V1–V14)")
|
||||
var load := SvmapIO.load_from_file(path)
|
||||
_check(load["ok"], "Gefiltertes Shrink-Ergebnis lädt mit gültiger Prüfsumme")
|
||||
if load["ok"]:
|
||||
_check(str(load["data"]["terrain"]["heightmap_r16_b64"])
|
||||
== str(new_doc["terrain"]["heightmap_r16_b64"]),
|
||||
"Shrink-Roundtrip: Höhenfeld byte-identisch")
|
||||
_check((load["data"]["objects"] as Array).size() == 2
|
||||
and (load["data"]["gebaeude"] as Array).size() == 2,
|
||||
"Shrink-Roundtrip: gefilterte Objekte/Bauteile bleiben gefiltert")
|
||||
|
|
|
|||
Loading…
Reference in a new issue