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))
|
return size_m / float(maxi(res - 1, 1))
|
||||||
|
|
||||||
|
|
||||||
## Terrain auf neue Kartenmaße resampeln (Vergrößern; Verkleinern folgt in R2).
|
## Terrain auf neue Kartenmaße resampeln (Vergrößern UND Verkleinern —
|
||||||
## R1-Schnitt (PLAN.md Kette): Spawn-Zonen, objects und gebaeude werden
|
## beides geht über dieselbe Anker-Mitte-Resample-Schleife: beim Shrink liegt
|
||||||
## unverändert ÜBERNOMMEN — auch wenn etwas außerhalb des neuen Geländes
|
## jeder neue Gridpunkt innerhalb der alten Fläche, die Rand-flach-Regel
|
||||||
## liegt (Filter + Out-of-Bounds-Dialog kommen in R2). Kein Mutationsspaß:
|
## 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
|
## 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
|
## ohne Prüfsumme (die alte wäre nach dem Resize ungültig — save_to_file
|
||||||
## signiert beim nächsten Speichern neu).
|
## 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", ""),
|
"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_relief_shading()
|
||||||
_test_grid_toggle()
|
_test_grid_toggle()
|
||||||
_test_resize_grow()
|
_test_resize_grow()
|
||||||
|
_test_resize_shrink()
|
||||||
# _test_ray_terrain_mapping läuft in _finish() — braucht Viewport im Baum.
|
# _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
|
_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,
|
and absf(float(clamped["terrain"]["size_z_m"]) - SvmapDocument.MAX_SIZE_M) < 0.0001,
|
||||||
"Clamp: 10 m→256 m, 100000 m→4096 m")
|
"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