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_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") # --------------------------------------------------------- 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)")