- 423 Objekte in 19 deutschen Kategorien (Panzer, Radfahrzeuge, Flugzeuge, Gebaeude, Zelte, Tuerme, Mauern, Sperren, Kisten, Technik, Felsen, Boden, Decals ...); Gruppen "Bauwerke" und "Kisten & Barrieren" samt Test-Asset-Namenslisten entfernt - LibraryBrowser: Gruppen/Namen aus dem Manifest (kategorie, name_de), unbekannte Kategorien bekommen automatisch eine eigene Gruppe - Thumbs neu, library/ synchronisiert (inkl. neuer Kollision) - Neue Identitaetsprobe: Kollision ALLER Objekte Editor = Client = Server - Tests auf Pack-Objekte umgestellt Co-Authored-By: Claude <noreply@anthropic.com>
382 lines
18 KiB
GDScript
382 lines
18 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): Karte t_9b50b231 — Echte GLB-Meshes im Editor-
|
||
## Viewport (GlbMeshStore), Platzhalter nur als Fallback.
|
||
## Ausführen: godot --headless --path . --script res://tests/test_glb_view.gd
|
||
## Exit 0 = alles grün. Prüft:
|
||
## 1. Vorhandene GLB → echtes Mesh geladen, deutlich mehr Vertices als der
|
||
## Kollisions-Platzhalter (Moosfelsen-Probe).
|
||
## 2. Normalisierung: Unterkante auf y = 0, XZ zentriert (Pack-GLBs haben
|
||
## ihren Ursprung oft in der Mitte — AABB-Unterkende wird GERECHNET).
|
||
## 3. Viewport-Rebuild: Marker-Fußpunkt sitzt auf der Terrainhöhe
|
||
## (Karte-C-Konvention b20dff2), Meta „glb“ = true, keine Grundfarbe
|
||
## über den Texturen.
|
||
## 4. Fehlende GLB → Platzhalter + Eintrag in fehlende_glbs (Statuszeile).
|
||
## 5. Cache pro OID: zweiter Abruf liefert DIESELBE Mesh-Instanz.
|
||
## 6. Pack-Texturen (Karte t_1e82f747, Stand d0d4fbd): die GLB-Flächen
|
||
## tragen ihre ORIGINAL-Texturen (GLTF löst die URIs selbst auf),
|
||
## Kante = Pack-Palette 1024²; Zuordnung pack_datei_fuer wie
|
||
## tools/PACK_TEXTURE_MAPPING.md (Client-Repo, verbindlich):
|
||
## military_base(+_00x) → Demonstration_Textures1.png (1024²),
|
||
## picture_1/2/3 → Demonstration_picture_N.png (2048² Multicam).
|
||
## Die alten Camo-Erwartungen (desert_multicam_1/2.png + geteilte
|
||
## Cache-Instanz) sind ENTFERNT — die Welt hat sich gedreht.
|
||
##
|
||
## Quelle-Fund (Karte t_1e82f747): die GLBs liegen in den NACHBAR-Repos.
|
||
## Gesucht wird wie in tools/sync_objects.sh: Env SCOPEDVR_MATCH_SERVER_DIR /
|
||
## SCOPEDVR_CLIENT_DIR zuerst, dann ../ und ../../ Siblings. Ohne Fund endet
|
||
## die Suite mit klaren SKIP-Zeilen (Exit 0) statt einer roten Zahl — die
|
||
## GLB-Prüfungen laufen, sobald ein Nachbar-Repo klonbar/abgelegt ist.
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
## Absolute Repo-Wurzel der gefundenen Nachbar-Repos (Ladereihenfolge).
|
||
var repos: Array = []
|
||
|
||
|
||
func _init() -> void:
|
||
repos = _finde_repos()
|
||
if repos.is_empty():
|
||
# Karte t_1e82f747: ohne Nachbar-Repo sind die GLB-Prüfungen hier
|
||
# nicht ausführbar — klare SKIP-Zeilen statt irreführender roter
|
||
# Zahlen. Die echten Checks laufen, sobald ein Nachbar da ist.
|
||
print("SKIP: test_glb_view — kein Nachbar-Repo mit assets/objekte gefunden.")
|
||
print("SKIP GRUND: gesucht: ../scopedvr-match-server, ../scopedvr-client, ../../… (relativ zum Editor-Repo)")
|
||
print("SKIP GRUND: sowie Env SCOPEDVR_MATCH_SERVER_DIR / SCOPEDVR_CLIENT_DIR (wie tools/sync_objects.sh).")
|
||
print("SKIP GRUND: die GLBs + Pack-Texturen liegen in den Sibling-Repos — erst klonen, dann Suite fahren.")
|
||
quit(0)
|
||
return
|
||
# Quellen konfigurierbar machen: die gefundenen Repos landen in
|
||
# ProjectSettings (GlbMeshStore.QUELLEN_SETTING) — glb_quellen sind
|
||
# Basis-relativ, deshalb ../-Pfade statt Absolutpfade.
|
||
var quellen: Array = []
|
||
for root: String in repos:
|
||
quellen.append(_relativ_zu(ProjectSettings.globalize_path("res://"),
|
||
root.path_join("assets/objekte")))
|
||
print("GLB-Quellen: %s" % ", ".join(PackedStringArray(quellen)))
|
||
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, quellen)
|
||
GlbMeshStore.cache_leeren()
|
||
_run_tests()
|
||
if failures == 0:
|
||
print("ALL OK: %d checks" % checks)
|
||
quit(0)
|
||
else:
|
||
print("FAILED: %d/%d checks" % [failures, checks])
|
||
quit(1)
|
||
|
||
|
||
## Nachbar-Repos finden wie tools/sync_objects.sh: Env-Variablen gewinnen,
|
||
## dann ../- und ../../-Siblings relativ zum Editor-Projekt. Ein Repo zählt
|
||
## nur, wenn assets/objekte existiert (Mesh-Lager).
|
||
## (Achtung, Probe 20.10.: der Repo-Name beginnt schon mit "scopedvr-" —
|
||
## die Env-Keys sind deshalb als feste Map notiert, nicht abgeleitet.)
|
||
func _finde_repos() -> Array:
|
||
var basis := ProjectSettings.globalize_path("res://")
|
||
var env_keys := {
|
||
"scopedvr-match-server": "SCOPEDVR_MATCH_SERVER_DIR",
|
||
"scopedvr-client": "SCOPEDVR_CLIENT_DIR",
|
||
}
|
||
var out: Array = []
|
||
for name: String in env_keys:
|
||
var env_pfad := OS.get_environment(env_keys[name]).simplify_path()
|
||
if not env_pfad.is_empty() and DirAccess.dir_exists_absolute(
|
||
env_pfad.path_join("assets/objekte")):
|
||
out.append(env_pfad)
|
||
for rel: String in ["../", "../../"]:
|
||
var kandidat := basis.path_join(rel + name).simplify_path()
|
||
if DirAccess.dir_exists_absolute(kandidat.path_join("assets/objekte")) \
|
||
and not out.has(kandidat):
|
||
out.append(kandidat)
|
||
return out
|
||
|
||
|
||
## glb_quellen sind Basis-RELATIVE Pfade (GlbMeshStore._kandidaten joinkt sie
|
||
## auf res:// — String.path_join KETTET auch bei absoluten Argumenten, ein
|
||
## Absolutpfad würde also kaputt gehen). Aus einem absoluten Env-Pfad wird
|
||
## deshalb ein ../-Pfad gerechnet.
|
||
func _relativ_zu(von: String, nach: String) -> String:
|
||
var v: PackedStringArray = von.simplify_path().split("/", false)
|
||
var n: PackedStringArray = nach.simplify_path().split("/", false)
|
||
var i := 0
|
||
while i < v.size() and i < n.size() and v[i] == n[i]:
|
||
i += 1
|
||
var teile := PackedStringArray()
|
||
for _j: int in range(i, v.size()):
|
||
teile.append("..")
|
||
for k: int in range(i, n.size()):
|
||
teile.append(n[k])
|
||
if teile.is_empty():
|
||
return "."
|
||
return "/".join(teile)
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
checks += 1
|
||
if cond:
|
||
print(" ok: %s" % label)
|
||
else:
|
||
failures += 1
|
||
print(" FAIL: %s" % label)
|
||
|
||
|
||
func _run_tests() -> void:
|
||
GlbMeshStore.cache_leeren()
|
||
_test_store_ladet_moosfelsen()
|
||
_test_store_normalisierung()
|
||
_test_store_cache()
|
||
_test_pack_tex()
|
||
_test_viewport_rebuild_echt()
|
||
_test_viewport_rebuild_fehlend()
|
||
|
||
|
||
func _load_manifest() -> Dictionary:
|
||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH))
|
||
return parsed if parsed is Dictionary else {}
|
||
|
||
|
||
func _vertex_zahl(mesh: Mesh) -> int:
|
||
# Universell (PrimitiveMesh UND ArrayMesh): Dreiecks-Suppe zählen.
|
||
return mesh.get_faces().size()
|
||
|
||
|
||
func _platzhalter(oid: String) -> Mesh:
|
||
var lib: Dictionary = _load_manifest().get("objects", {})
|
||
var col: Dictionary = (lib.get(oid, {}) as Dictionary).get("collision", {})
|
||
return MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
||
|
||
|
||
# ------------------------------------------------------------ GlbMeshStore --
|
||
|
||
## Der Moosfelsen-GLB ist absichtlich Low-Poly (Registry poly = 46) — für
|
||
## das „deutlich mehr Geometrie als der Platzhalter"-Kriterium dient
|
||
## stone_025 (poly = 342) als Hoch-Poly-Probe.
|
||
const MOOS_OID := "fels_moos_block_gross"
|
||
const PROBE_OID := "stone_025"
|
||
|
||
func _test_store_ladet_moosfelsen() -> void:
|
||
print("Testgruppe: GlbMeshStore — echtes GLB laden (Moosfelsen-Probe)")
|
||
_check(not GlbMeshStore.fehlt(MOOS_OID), "GLB-Datei für %s liegt im Mesh-Lager" % MOOS_OID)
|
||
var moos := GlbMeshStore.mesh_fuer(MOOS_OID)
|
||
_check(moos != null, "echtes Moosfelsen-Mesh geladen (nicht null)")
|
||
if moos != null:
|
||
var moos_platz := _platzhalter(MOOS_OID)
|
||
_check(_vertex_zahl(moos) > _vertex_zahl(moos_platz),
|
||
"Moosfelsen: echtes GLB hat mehr Vertices als der Platzhalter (%d > %d)" % [
|
||
_vertex_zahl(moos), _vertex_zahl(moos_platz)])
|
||
_check(not GlbMeshStore.fehlt(PROBE_OID), "GLB-Datei für %s liegt im Mesh-Lager" % PROBE_OID)
|
||
var mesh := GlbMeshStore.mesh_fuer(PROBE_OID)
|
||
_check(mesh != null, "echtes Mesh geladen (nicht null)")
|
||
if mesh == null:
|
||
return
|
||
var platzhalter := _platzhalter(PROBE_OID)
|
||
_check(_vertex_zahl(mesh) > _vertex_zahl(platzhalter) * 5,
|
||
"Vertexzahl echtes GLB (%d) >> Platzhalter (%d)" % [_vertex_zahl(mesh), _vertex_zahl(platzhalter)])
|
||
_check(mesh is ArrayMesh and (mesh as ArrayMesh).get_surface_count() >= 1,
|
||
"ArrayMesh mit mindestens einer Oberfläche")
|
||
|
||
|
||
func _test_store_normalisierung() -> void:
|
||
print("Testgruppe: Normalisierung — Fußpunkt-Konvention (wie scopedvr_export)")
|
||
var mesh := GlbMeshStore.mesh_fuer(PROBE_OID)
|
||
if mesh == null:
|
||
return
|
||
var aabb := mesh.get_aabb()
|
||
_check(absf(aabb.position.y) < 0.05,
|
||
"Unterkante auf y = 0 (ist %.4f m — Pack-Ursprung in der Mitte wurde herausgerechnet)" % aabb.position.y)
|
||
_check(absf(aabb.get_center().x) < 0.05 and absf(aabb.get_center().z) < 0.05,
|
||
"XZ um den Mittelpunkt zentriert (Δ %.4f / %.4f m)" % [aabb.get_center().x, aabb.get_center().z])
|
||
_check(absf(MeshShapes.half_height(mesh)) < 0.0001,
|
||
"half_height == 0 (schon in Fußpunkt-Koordinaten gebacken — kein Extra-Lift)")
|
||
|
||
|
||
func _test_store_cache() -> void:
|
||
print("Testgruppe: Cache pro OID (kein Re-Parse je Instanz)")
|
||
var a := GlbMeshStore.mesh_fuer(PROBE_OID)
|
||
var b := GlbMeshStore.mesh_fuer(PROBE_OID)
|
||
_check(a != null and a == b, "zweiter Abruf liefert DIESELBE Mesh-Instanz (Cache)")
|
||
GlbMeshStore.cache_leeren()
|
||
_check(GlbMeshStore.mesh_fuer(PROBE_OID) != null, "cache_leeren → wird neu geladen")
|
||
|
||
|
||
# --------------------------------------------------------- Pack-Texturen ----
|
||
|
||
## Karte t_1e82f747 (Stand d0d4fbd): die Camo-Erwartungen sind überholt —
|
||
## die Pack-GLBs tragen ihre ORIGINAL-Texturen (GLTFDocument löst die externen
|
||
## URIs inzwischen selbst auf, je Parse eine eigene ImageTexture-Instanz;
|
||
## der _camo_tex_cache bleibt deshalb leer). Diese Gruppe beschreibt, was IST:
|
||
## Flächen getextured, Textur-Kante = Pack-Palette 1024², Zuordnung
|
||
## pack_datei_fuer wie tools/PACK_TEXTURE_MAPPING.md (Client-Repo).
|
||
func _test_pack_tex() -> void:
|
||
print("Testgruppe: Pack-Texturen im Live-Pfad — WYSIWYG Editor = Spiel (d0d4fbd)")
|
||
var mesh_w := GlbMeshStore.mesh_fuer("wall_001")
|
||
_check(mesh_w != null, "wall_001 (walls, military_base) geladen")
|
||
if mesh_w != null:
|
||
_pack_flachen_pruefen(mesh_w, "wall_001", 1024)
|
||
var mesh_g := GlbMeshStore.mesh_fuer("ground_001")
|
||
_check(mesh_g != null, "ground_001 (nature/ground, military_base) geladen")
|
||
if mesh_g != null:
|
||
_pack_flachen_pruefen(mesh_g, "ground_001", 1024)
|
||
# Pack-Zuordnung (pure Funktion — Quelle: tools/PACK_TEXTURE_MAPPING.md):
|
||
_check(GlbMeshStore.pack_datei_fuer("military_base") == GlbMeshStore.PACK_TEX_PALETTE,
|
||
"military_base → 1024er Palette (Demonstration_Textures1.png)")
|
||
_check(GlbMeshStore.pack_datei_fuer("military_base_003") == GlbMeshStore.PACK_TEX_PALETTE,
|
||
"military_base_NNN → Palette (Präfix-Fallback)")
|
||
_check(GlbMeshStore.pack_datei_fuer("picture_1") == GlbMeshStore.PACK_TEX_PICTURE_1,
|
||
"picture_1 → 2048er Multicam (Demonstration_picture_1.png)")
|
||
_check(GlbMeshStore.pack_datei_fuer("picture_2") == GlbMeshStore.PACK_TEX_PICTURE_2,
|
||
"picture_2 → 2048er Multicam (Demonstration_picture_2.png)")
|
||
_check(GlbMeshStore.pack_datei_fuer("gibts_nicht_material") == GlbMeshStore.PACK_TEX_PALETTE,
|
||
"unbekanntes Material → Palette (Default-Fallback)")
|
||
_pack_dateien_pruefen()
|
||
|
||
|
||
## Jede Fläche trägt eine Textur und die Kante entspricht der Pack-Palette
|
||
## (1024² für military_base-Objekte) — bewiesener Live-Stand (Probe 20.10.).
|
||
func _pack_flachen_pruefen(mesh: Mesh, oid: String, erwartete_kante: int) -> void:
|
||
var count := mesh.get_surface_count()
|
||
_check(count > 0, "%s hat Flächen (%d)" % [oid, count])
|
||
var textured := 0
|
||
var kante_ok := true
|
||
for s in count:
|
||
var tex := _mat_tex(mesh, s)
|
||
if tex == null:
|
||
continue
|
||
textured += 1
|
||
var img := tex.get_image()
|
||
if img != null and (img.get_width() != erwartete_kante
|
||
or img.get_height() != erwartete_kante):
|
||
kante_ok = false
|
||
_check(textured == count,
|
||
"%s: ALLE %d Flächen haben albedo_texture != null (getextured: %d)" % [oid, count, textured])
|
||
_check(kante_ok, "%s: Textur-Kante %d² (Pack-Palette)" % [oid, erwartete_kante])
|
||
|
||
|
||
func _mat_tex(mesh: Mesh, s: int) -> Texture2D:
|
||
var mat := mesh.surface_get_material(s)
|
||
if mat is BaseMaterial3D:
|
||
return (mat as BaseMaterial3D).albedo_texture
|
||
return null
|
||
|
||
|
||
## Die Pack-Dateien liegen im gefundenen Nachbar-Repo: Palette 1024²,
|
||
## Multicam 2048². Fehlen sie dort, ist das ein echter Bruch (die GLBs
|
||
## kommen ja aus ebendiesem Repo) und wird laut gemeldet — kein SKIP-Fall.
|
||
func _pack_dateien_pruefen() -> void:
|
||
print("Testgruppe: Pack-Dateien im Nachbar-Repo (Palette 1024² / Multicam 2048²)")
|
||
var treffer := ""
|
||
for root: String in repos:
|
||
var dir := root.path_join("assets/textures/military_desert")
|
||
if FileAccess.file_exists(dir.path_join(GlbMeshStore.PACK_TEX_PALETTE)):
|
||
treffer = dir
|
||
break
|
||
_check(not treffer.is_empty(),
|
||
"textures/military_desert im Nachbar-Repo gefunden (%s)" % treffer)
|
||
if treffer.is_empty():
|
||
return
|
||
_kante_pruefen(treffer.path_join(GlbMeshStore.PACK_TEX_PALETTE), 1024, "Palette")
|
||
_kante_pruefen(treffer.path_join(GlbMeshStore.PACK_TEX_PICTURE_1), 2048, "picture_1")
|
||
_kante_pruefen(treffer.path_join(GlbMeshStore.PACK_TEX_PICTURE_2), 2048, "picture_2")
|
||
|
||
|
||
func _kante_pruefen(pfad: String, kante: int, label: String) -> void:
|
||
var img := Image.load_from_file(pfad)
|
||
_check(img != null, "%s ladbar (%s)" % [label, pfad.get_file()])
|
||
if img == null:
|
||
return
|
||
_check(img.get_width() == kante and img.get_height() == kante,
|
||
"%s ist %d² (ist %d×%d)" % [label, kante, img.get_width(), img.get_height()])
|
||
|
||
|
||
# --------------------------------------------------------- Viewport-Rebuild --
|
||
|
||
func _proben_doc(oid: String, scale: float) -> Dictionary:
|
||
var doc := SvmapDocument.create_new("GLB-Probe", 320.0, "mittag")
|
||
var terrain: Dictionary = doc["terrain"]
|
||
var res := 17
|
||
var size := 320.0
|
||
terrain["resolution"] = res
|
||
terrain["size_x_m"] = size
|
||
terrain["size_z_m"] = size
|
||
terrain["min_height_m"] = 0.0
|
||
terrain["max_height_m"] = 4.0
|
||
var heights := PackedFloat32Array()
|
||
heights.resize(res * res)
|
||
var cell := size / float(res - 1)
|
||
for zi in res:
|
||
for xi in res:
|
||
var x := -size * 0.5 + float(xi) * cell
|
||
heights[zi * res + xi] = 4.0 * (x / size + 0.5) # Rampe 0..4 m
|
||
terrain["heightmap_r16_b64"] = TerrainField.encode(heights, 0.0, 4.0)
|
||
doc["objects"] = [{"id": oid, "pos": [0.0, 2.0, 0.0], "rot_y_deg": 0.0, "scale": scale}]
|
||
return doc
|
||
|
||
|
||
func _test_viewport_rebuild_echt() -> void:
|
||
print("Testgruppe: Viewport-Rebuild — echtes GLB im Editor-Viewport")
|
||
var builder := Node3D.new()
|
||
builder.set_script(load("res://src/viewport/map_view_builder.gd"))
|
||
root.add_child(builder)
|
||
# pos.y bewusst FALSCH (7 statt Terrainhöhe): der Marker steht am Doc-
|
||
# Fußpunkt, die Mesh-UNTERKANTE muss trotzdem auf Terrainhöhe hängen.
|
||
builder.call("rebuild", _proben_doc(PROBE_OID, 1.0), _load_manifest())
|
||
var markers: Array = builder.get("object_markers")
|
||
_check(markers.size() == 1 and markers[0] != null, "Marker für das Objekt gebaut")
|
||
if markers.size() != 1 or markers[0] == null:
|
||
return
|
||
var marker: Node3D = markers[0]
|
||
_check(bool(marker.get_meta("glb", false)), "Meta glb = true (echtes Modell)")
|
||
var mi := marker.get_child(0) as MeshInstance3D
|
||
_check(mi.mesh != null and _vertex_zahl(mi.mesh) > _vertex_zahl(_platzhalter(PROBE_OID)) * 5,
|
||
"Viewport zeigt das ECHTE Mesh, nicht den Platzhalter")
|
||
# Fußpunkt-Konvention (Karte C, b20dff2): Unterkante des Meshes == Terrainhöhe.
|
||
var boden: float = SvmapDocument.terrain_height_at(builder.get("_doc_ref"), 0.0, 0.0)
|
||
var unten: float = marker.position.y + mi.position.y + (mi.mesh.get_aabb().position.y * mi.scale.y)
|
||
_check(absf(unten - boden) < 0.05,
|
||
"Mesh-Unterkante == Terrainhöhe (%.3f m, ist %.3f m — b20dff2-Konvention)" % [boden, unten])
|
||
_check(mi.material_override == null,
|
||
"keine Grundfarbe drüber — Texturen des echten Modells bleiben sichtbar")
|
||
# U6 (t_639135dd): Auswahl = Außenhüllen-Kontur um das Modell — das
|
||
# Grundmaterial (mit Textur) bleibt im Auswahl-Zustand KOMPLETT sichtbar,
|
||
# es gibt kein material_override mehr für die Markierung.
|
||
builder.call("set_selected_multi", [0])
|
||
_check(mi.material_override == null,
|
||
"Auswahl: Textur bleibt sichtbar (kein material_override mehr, U6)")
|
||
_check(marker.get_meta("outline", null) is MeshInstance3D,
|
||
"Auswahl: Kontur-Outline am echten GLB-Marker (U6)")
|
||
builder.call("set_selected_multi", [])
|
||
_check(mi.material_override == null, "Auswahl weg: Texturen wieder frei")
|
||
_check(not marker.has_meta("outline"), "Auswahl weg: kein Rest-Outline (U6)")
|
||
# Skalierung: scale 2.0 → AABB-Doppelte Höhe im Marker-Meta.
|
||
builder.call("rebuild", _proben_doc(PROBE_OID, 2.0), _load_manifest())
|
||
var m2: Node3D = (builder.get("object_markers") as Array)[0]
|
||
var mi2 := m2.get_child(0) as MeshInstance3D
|
||
_check(absf(mi2.scale.x - 2.0) < 0.001, "Objekt-Scale landet am Mesh (mi.scale = 2)")
|
||
_check(absf(float(m2.get_meta("half_height")) - 2.0 * MeshShapes.center_height(mi2.mesh)) < 0.01,
|
||
"pick/half_height-Metas berücksichtigen den Scale")
|
||
|
||
|
||
func _test_viewport_rebuild_fehlend() -> void:
|
||
print("Testgruppe: Fallback — fehlende GLB → Platzhalter + Hinweis")
|
||
# Seit der Military-Pack-Umstellung hat JEDES Katalog-Objekt ein Modell — der
|
||
# Fallback wird mit einem synthetischen Eintrag ohne Mesh im Lager geprüft.
|
||
var oid := "test_ohne_mesh"
|
||
var manifest_fb: Dictionary = _load_manifest()
|
||
(manifest_fb["objects"] as Dictionary)[oid] = {"poly": 12, "collision": {"shape": "box", "size": [2.0, 1.0, 1.0]}}
|
||
_check(GlbMeshStore.fehlt(oid), "fehlende OID erkannt (fehlt == true)")
|
||
var builder := Node3D.new()
|
||
builder.set_script(load("res://src/viewport/map_view_builder.gd"))
|
||
root.add_child(builder)
|
||
builder.call("rebuild", _proben_doc(oid, 1.0), manifest_fb)
|
||
var marker: Node3D = (builder.get("object_markers") as Array)[0]
|
||
_check(not bool(marker.get_meta("glb", true)), "Meta glb = false (Platzhalter)")
|
||
var mi := marker.get_child(0) as MeshInstance3D
|
||
var lib: Dictionary = manifest_fb.get("objects", {})
|
||
var col: Dictionary = (lib.get(oid, {}) as Dictionary).get("collision", {})
|
||
var platzhalter := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
||
_check(mi.mesh.get_aabb().size.distance_to(platzhalter.get_aabb().size) < 0.001,
|
||
"Platzhalter-Größe wie Registry-Kollision (Fallback aktiv)")
|
||
_check(mi.material_override != null, "Platzhalter trägt die Editor-Grundfarbe")
|
||
var fehlend: Array = builder.get("fehlende_glbs")
|
||
_check(fehlend.has(oid), "fehlende_glbs meldet die OID (Statuszeilen-Hinweis)")
|
||
_check(GlbMeshStore.fehlt("gibts_nicht_oid"), "unbekannte OID zählt als fehlend")
|
||
var fehlend_nicht: Array = builder.get("fehlende_glbs")
|
||
_check(not fehlend_nicht.has("gibts_nicht_oid"), "unbekannte OID steht NICHT in fehlende_glbs (nicht im Doc)")
|