269 lines
12 KiB
GDScript
269 lines
12 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.
|
|
|
|
var failures := 0
|
|
var checks := 0
|
|
|
|
|
|
func _init() -> void:
|
|
_run_tests()
|
|
if failures == 0:
|
|
print("ALL OK: %d checks" % checks)
|
|
quit(0)
|
|
else:
|
|
print("FAILED: %d/%d checks" % [failures, checks])
|
|
quit(1)
|
|
|
|
|
|
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_camo_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
|
|
## stein_mittel_1 (poly = 342) als Hoch-Poly-Probe.
|
|
const MOOS_OID := "fels_moos_block_gross"
|
|
const PROBE_OID := "stein_mittel_1"
|
|
|
|
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")
|
|
|
|
|
|
# ------------------------------------------------------------ Camo-Textur ----
|
|
|
|
## Beweis (t_50a07d4b): das Live-GLB-Mesh trägt ECHTE Multicam-Texturen
|
|
## (albedo_texture != null je Fläche), Zuordnung je Kategorie wie
|
|
## tools/CAMO_TEXTURE_MAPPING.md (Client-Repo, verbindlich).
|
|
## Die ImageTexture wird zur Laufzeit von der Platte geladen → resource_path
|
|
## ist leer; der Pfad steht im Store-Cache (_camo_tex_cache) und wird hier
|
|
## ausgegeben (Zahlen/Pfade ins Handoff).
|
|
func _test_camo_tex() -> void:
|
|
print("Testgruppe: Camo-Textur im Live-Pfad — WYSIWYG Editor = Spiel (t_50a07d4b)")
|
|
var mesh_w := GlbMeshStore.mesh_fuer("wall_001")
|
|
_check(mesh_w != null, "wall_001 (walls → _1) geladen")
|
|
if mesh_w != null:
|
|
_camo_flachen_pruefen(mesh_w, "wall_001", "desert_multicam_1.png")
|
|
var mesh_g := GlbMeshStore.mesh_fuer("ground_001")
|
|
_check(mesh_g != null, "ground_001 (nature/ground → _2) geladen")
|
|
if mesh_g != null:
|
|
_camo_flachen_pruefen(mesh_g, "ground_001", "desert_multicam_2.png")
|
|
# Eine-Quelle-Cache: wall_002 teilt DIESELBE ImageTexture-Instanz wie
|
|
# wall_001 (291 GLBs teilen sich 2 Texturen statt je eine Kopie).
|
|
var mesh_w2 := GlbMeshStore.mesh_fuer("wall_002")
|
|
if mesh_w2 != null and mesh_w != null:
|
|
var t1 := _camo_mat_tex(mesh_w, 0)
|
|
var t2 := _camo_mat_tex(mesh_w2, 0)
|
|
_check(t1 != null and t1 == t2,
|
|
"wall_001 und wall_002 teilen DIESELBE Textur-Instanz (VRAM-Cache)")
|
|
_check(GlbMeshStore._camo_tex_cache.size() == 2,
|
|
"Camo-Textur-Cache: genau 2 Texturen (Objekt _1 + Boden _2), ist %d" % GlbMeshStore._camo_tex_cache.size())
|
|
|
|
|
|
func _camo_flachen_pruefen(mesh: Mesh, oid: String, erwartet_name: String) -> void:
|
|
var count := mesh.get_surface_count()
|
|
_check(count > 0, "%s hat Flächen (%d)" % [oid, count])
|
|
var textured := 0
|
|
for s in count:
|
|
if _camo_mat_tex(mesh, s) != null:
|
|
textured += 1
|
|
_check(textured == count,
|
|
"%s: ALLE %d Flächen haben albedo_texture != null (getextured: %d)" % [oid, count, textured])
|
|
# Pfad + Zuordnung: die Textur am Mesh ist DIESELBE Instanz wie der
|
|
# Cache-Eintrag mit dem erwarteten Multicam-Pfad.
|
|
var erwartet: ImageTexture = null
|
|
var pfad_text := ""
|
|
for pfad in GlbMeshStore._camo_tex_cache.keys():
|
|
var p := str(pfad)
|
|
if p.ends_with(erwartet_name):
|
|
var v: Variant = GlbMeshStore._camo_tex_cache[pfad]
|
|
if v is ImageTexture:
|
|
erwartet = v
|
|
pfad_text = p
|
|
_check(erwartet != null, "%s: Multicam-Textur %s im Cache geladen" % [oid, erwartet_name])
|
|
if erwartet != null:
|
|
print(" info: %s → Textur-Pfad: %s" % [oid, pfad_text])
|
|
var instanz_ok := false
|
|
for s in count:
|
|
if _camo_mat_tex(mesh, s) == erwartet:
|
|
instanz_ok = true
|
|
_check(instanz_ok,
|
|
"%s: Flächen tragen DIESELBE Textur-Instanz wie der Cache (%s)" % [oid, erwartet_name])
|
|
|
|
|
|
func _camo_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
|
|
|
|
|
|
# --------------------------------------------------------- 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")
|
|
# Auswahl färbt trotzdem orange (Material-Override NUR im Auswahl-Zustand).
|
|
builder.call("set_selected_multi", [0])
|
|
_check(mi.material_override != null, "Auswahl: orange Override an")
|
|
builder.call("set_selected_multi", [])
|
|
_check(mi.material_override == null, "Auswahl weg: Texturen wieder frei")
|
|
# 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")
|
|
var oid := "crate_wood" # Basis-Katalog ohne GLB im Mesh-Lager
|
|
_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), _load_manifest())
|
|
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 = _load_manifest().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)")
|