GLB-View (t_9b50b231): echte Modelle im Editor-Viewport — Platzhalter nur als Fallback.
- GlbMeshStore (neu): laedt OID+.glb aus konfigurierbaren Quellen
(ProjectSettings scopedvr/editor/glb_quellen, Voreinstellung =
Sibling-Repos, wie gen_thumbs; plattformneutral, relative Pfade),
backt ALLE MeshInstance3D der Szene in EIN normalisiertes ArrayMesh
(Fusspunkt y=0, XZ zentriert — wie scopedvr_export.normalisiere),
Materialien/Texturen bleiben je Oberflaeche erhalten.
- Cache pro OID (Parse nur einmal; fehlende Datei ungcached, Parse-Fail
negativ gecached).
- map_view_builder: echtes GLB vor Platzhalter; kein half_height-Lift
(schon Fusspunkt-Koordinaten), Scale ueber Node-Scale + AABB-Metas
mal Scale; Auswahl-Orange nur als Override im Auswahl-Zustand
(Texturen bleiben sichtbar). fehlende_glbs-Meldung an die Stats-Zeile.
- interaction (Ghost-Preview): nutzt denselben echten Mesh-Pfad;
Platzier-Status haengt „(Platzhalter — echte GLB fehlt)“ an.
- tests/test_glb_view.gd (neu, in run_tests.sh): Moosfelsen-Probe
(echtes GLB > Platzhalter-Vertices), stein_mittel_1 1026>>36,
Normalisierung y=0/XZ-zentriert, Cache-Identitaet, Viewport-Rebuild
Fusspunkt == Terrainhoehe (b20dff2), Fallback + fehlende_glbs.
Suite: 773 checks, alle gruen.
This commit is contained in:
parent
afcbda8dd0
commit
cda6f48b01
9 changed files with 430 additions and 12 deletions
|
|
@ -24,3 +24,11 @@ enabled=PackedStringArray("res://addons/modular_builder/plugin.cfg")
|
|||
[rendering]
|
||||
|
||||
anti_aliasing/quality/msaa_3d=2
|
||||
|
||||
[scopedvr]
|
||||
|
||||
; Quellen für echte GLB-Modelle im Editor-Viewport (Karte t_9b50b231).
|
||||
; Relative Pfade vom Editor-Projektordner aus — plattformneutral
|
||||
; (PLAN.md Plattform-Regel): unter Windows einfach die Sibling-Repos
|
||||
; neben den Editor legen oder die Pfade hier anpassen.
|
||||
editor/glb_quellen=PackedStringArray("../scopedvr-match-server/assets/objekte", "../scopedvr-client/assets/objekte")
|
||||
|
|
|
|||
|
|
@ -189,7 +189,9 @@ func start_placement(oid: String) -> void:
|
|||
_ensure_ghost()
|
||||
_ensure_ghost_visible()
|
||||
var label: String = main.object_label(oid)
|
||||
main.call("_set_status", "%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, Rechtsklick/ESC bricht ab." % label)
|
||||
var hinweis := "" if not GlbMeshStore.fehlt(oid) \
|
||||
else " (Platzhalter — echte GLB fehlt für %s)" % oid
|
||||
main.call("_set_status", "%s%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, Rechtsklick/ESC bricht ab." % [label, hinweis])
|
||||
main.call("_highlight_library", oid)
|
||||
|
||||
|
||||
|
|
@ -226,7 +228,11 @@ func _ghost_refresh_mesh() -> void:
|
|||
var lib: Dictionary = main.manifest.get("objects", {})
|
||||
var entry: Dictionary = lib.get(place_oid, {})
|
||||
var col: Dictionary = entry.get("collision", {})
|
||||
var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
||||
# Echtes GLB vorziehen (Karte t_9b50b231) — die Vorschau zeigt das
|
||||
# Modell, das landet, nicht nur die Kollisionsform. Ohne GLB: Platzhalter.
|
||||
var mesh := GlbMeshStore.mesh_fuer(place_oid)
|
||||
if mesh == null:
|
||||
mesh = MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
||||
_ghost_mi.mesh = mesh
|
||||
# Gleicher Lift wie beim platzierten Objekt: Mesh-Unterseite = Fußpunkt.
|
||||
_ghost_mi.position.y = MeshShapes.half_height(mesh)
|
||||
|
|
|
|||
|
|
@ -2164,6 +2164,14 @@ func _refresh_stats() -> void:
|
|||
stats_objects.text = "Objekte: %d / 5000 · Poly-Budget: %s / %s (%.0f %%) · Bauteile: %d" % [
|
||||
count, _fmt_int(int(poly)), _fmt_int(budget), 100.0 * poly / budget,
|
||||
BuildOps.piece_count(doc)]
|
||||
# GLB-Fehlende Objekte (Karte t_9b50b231): Platzhalter laufen, aber Sly soll
|
||||
# sehen, WELCHE echten Modelle im Mesh-Lager nachgelegt werden müssen.
|
||||
if view_builder != null and not (view_builder.get("fehlende_glbs") as Array).is_empty():
|
||||
var fehlend: Array = view_builder.get("fehlende_glbs")
|
||||
var nennung := ", ".join(fehlend.slice(0, 3))
|
||||
if fehlend.size() > 3:
|
||||
nennung += " … (+%d weitere)" % (fehlend.size() - 3)
|
||||
stats_objects.text += " · ⚠ GLB fehlt: %s" % nennung
|
||||
# Terrain-Anteil am Quest-3-Budget (res 513 = 524.288 Dreiecke, map_format.md §4)
|
||||
var tris := 0
|
||||
var res := 0
|
||||
|
|
|
|||
169
src/viewport/glb_mesh_store.gd
Normal file
169
src/viewport/glb_mesh_store.gd
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
extends RefCounted
|
||||
class_name GlbMeshStore
|
||||
## Echte GLB-Renderings für Bibliotheks-Objekte im Editor-Viewport
|
||||
## (Karte t_9b50b231). Der Viewport-Bauer zeigt bisher nur den
|
||||
## Kollisions-Platzhalter (MeshShapes.make_placeholder) — diese statische
|
||||
## Loader-Schicht holt stattdessen das ECHTE Modell aus dem Mesh-Lager
|
||||
## (Sibling-Repos, gleiche Quellen wie tools/gen_thumbs.gd) und fällt ohne
|
||||
## GLB auf den Platzhalter zurück.
|
||||
##
|
||||
## Messkonvention wie addons/modular_builder/scopedvr_export.gd
|
||||
## `normalisiere`: das geladene Mesh wird in Fußpunkt-Koordinaten gebacken —
|
||||
## Unterkante auf y = 0, XZ um den Mittelpunkt zentriert. Pack-GLBs haben
|
||||
## ihren Mesh-Ursprung oft in der Mitte, deshalb wird die AABB-Unterkante
|
||||
## GERECHNET und nie blind platziert. Damit gilt wie beim Compound-Platzhalter:
|
||||
## MeshShapes.half_height(mesh) == 0 und marker.position == Fußpunkt.
|
||||
##
|
||||
## Plattform-Regel (PLAN.md): nur relative Kandidaten-Pfade, konfigurierbar
|
||||
## über ProjectSettings — läuft identisch auf Linux und Windows.
|
||||
##
|
||||
## Performance: EIN Parse pro OID, Ergebnis gecacht — der Rebuild bei jedem
|
||||
## Platzieren/Undo liest nur noch aus dem Cache.
|
||||
|
||||
## ProjectSettings-Schlüssel für die GLB-Quellen (relativ zum Editor-Projekt).
|
||||
const QUELLEN_SETTING := "scopedvr/editor/glb_quellen"
|
||||
## Voreinstellung = dieselben Quellen wie tools/gen_thumbs.gd (Sibling-Repos).
|
||||
const DEFAULT_QUELLEN := ["../scopedvr-match-server/assets/objekte", "../scopedvr-client/assets/objekte"]
|
||||
|
||||
# Cache pro OID: Mesh (normalisiert) oder `false` = GLB gefunden, aber nicht
|
||||
# ladbar (Parse-Fehler — kein endloser Retry pro Rebuild). Fehlende Datei
|
||||
# bleibt UNGECACHED (nur ein billiger Datei-Check pro Rebuild; legt man die
|
||||
# GLB später ab, erscheint sie ohne Editor-Neustart).
|
||||
static var _cache: Dictionary = {}
|
||||
|
||||
|
||||
## Kandidaten-Pfade der Quellen (ProjectSettings → Fallback-Konstante).
|
||||
static func quellen() -> Array:
|
||||
var setting: Variant = ProjectSettings.get_setting(QUELLEN_SETTING, DEFAULT_QUELLEN)
|
||||
if not (setting is Array or setting is PackedStringArray):
|
||||
return DEFAULT_QUELLEN.duplicate()
|
||||
var out: Array = []
|
||||
for q in setting:
|
||||
if q is String and not (q as String).strip_edges().is_empty():
|
||||
out.append((q as String).strip_edges())
|
||||
return out if not out.is_empty() else DEFAULT_QUELLEN.duplicate()
|
||||
|
||||
|
||||
## Echtes Mesh für eine Objekt-ID (normalisiert, Fußpunkt-Koordinaten) oder
|
||||
## null = keine brauchbare GLB → Aufrufer zeigt den Platzhalter.
|
||||
static func mesh_fuer(oid: String) -> Mesh:
|
||||
if oid.is_empty():
|
||||
return null
|
||||
if _cache.has(oid):
|
||||
return _cache[oid] if _cache[oid] is Mesh else null
|
||||
for quelle in quellen():
|
||||
var basis := ProjectSettings.globalize_path("res://")
|
||||
var pfad := basis.path_join(quelle).path_join(oid + ".glb")
|
||||
if FileAccess.file_exists(pfad):
|
||||
var m := _glb_mesh(pfad)
|
||||
if m != null:
|
||||
_cache[oid] = m
|
||||
return m
|
||||
# Datei da, aber kaputt → NEGATIV cachen (Parse-Fehler wiederholt
|
||||
# sich sonst bei jedem Rebuild und kostet Zeit).
|
||||
_cache[oid] = false
|
||||
return null
|
||||
return null
|
||||
|
||||
|
||||
## Liegt für die OID überhaupt eine GLB-Datei im Lager? (Billiger Check ohne
|
||||
## Parse — für Statuszeilen-Hinweise beim Platzieren.)
|
||||
static func fehlt(oid: String) -> bool:
|
||||
if oid.is_empty():
|
||||
return true
|
||||
if _cache.has(oid):
|
||||
return not (_cache[oid] is Mesh)
|
||||
var basis := ProjectSettings.globalize_path("res://")
|
||||
for quelle in quellen():
|
||||
if FileAccess.file_exists(basis.path_join(quelle).path_join(oid + ".glb")):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Cache leeren (Tests / nach manuellem Eingriff ins Mesh-Lager).
|
||||
static func cache_leeren() -> void:
|
||||
_cache.clear()
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Laden --------
|
||||
|
||||
## GLB OHNE Import-System laden (GLTFDocument, wie gen_thumbs) und ALLE
|
||||
## MeshInstance3D der Szene in EIN ArrayMesh zusammenbacken (Jedes Teil-Node
|
||||
## mit seiner Node-Transform; Materialien je Oberfläche bleiben erhalten —
|
||||
## echte Texturen statt Editor-Farbe). null = Load-Fehlschlag.
|
||||
static func _glb_mesh(pfad: String) -> Mesh:
|
||||
var gltf := GLTFDocument.new()
|
||||
var state := GLTFState.new()
|
||||
if gltf.append_from_file(pfad, state) != OK:
|
||||
push_warning("GlbMeshStore: GLB nicht ladbar: %s" % pfad)
|
||||
return null
|
||||
var szene: Node = gltf.generate_scene(state)
|
||||
if szene == null:
|
||||
return null
|
||||
var m := _zusammenbacken(szene)
|
||||
# Das Mesh ist eine Resource (RefCounted) — das Freigeben der Szene
|
||||
# berührt die Referenz im Rückgabewert nicht.
|
||||
szene.free()
|
||||
return m
|
||||
|
||||
|
||||
## Alle Mesh-Instanzen der Szene → ein ArrayMesh (Transforms angewandt,
|
||||
## Materialien übernommen), dann normalisiert (Fußpunkt-Konvention).
|
||||
static func _zusammenbacken(szene: Node) -> Mesh:
|
||||
var roh := ArrayMesh.new()
|
||||
_sammeln(szene, Transform3D.IDENTITY, roh)
|
||||
if roh.get_surface_count() == 0:
|
||||
return null
|
||||
return _normalisiert(roh)
|
||||
|
||||
|
||||
static func _sammeln(n: Node, xf: Transform3D, out: ArrayMesh) -> void:
|
||||
if n is Node3D:
|
||||
xf = xf * (n as Node3D).transform
|
||||
if n is MeshInstance3D and (n as MeshInstance3D).mesh != null:
|
||||
var m: Mesh = (n as MeshInstance3D).mesh
|
||||
for s in m.get_surface_count():
|
||||
var arrays: Array = m.surface_get_arrays(s)
|
||||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||||
continue
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for i in verts.size():
|
||||
verts[i] = xf * verts[i]
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
if arrays[Mesh.ARRAY_NORMAL] is PackedVector3Array:
|
||||
var norms: PackedVector3Array = arrays[Mesh.ARRAY_NORMAL]
|
||||
for i in norms.size():
|
||||
norms[i] = (xf.basis * norms[i]).normalized()
|
||||
arrays[Mesh.ARRAY_NORMAL] = norms
|
||||
# Statisch fürs Editor-Viewport: Skin-Daten fliegen raus (sonst
|
||||
# verlangt add_surface_from_arrays ein Skeleton).
|
||||
arrays[Mesh.ARRAY_BONES] = null
|
||||
arrays[Mesh.ARRAY_WEIGHTS] = null
|
||||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
var mat: Material = m.surface_get_material(s)
|
||||
if mat != null:
|
||||
out.surface_set_material(out.get_surface_count() - 1, mat)
|
||||
for c in n.get_children():
|
||||
_sammeln(c, xf, out)
|
||||
|
||||
|
||||
## Fußpunkt-Konvention (wie scopedvr_export.normalisiere): Unterkante y = 0,
|
||||
## XZ um den AABB-Mittelpunkt zentriert. Materialien bleiben an den Flächen.
|
||||
static func _normalisiert(mesh: ArrayMesh) -> ArrayMesh:
|
||||
var aabb := mesh.get_aabb()
|
||||
var offset := Vector3(-aabb.get_center().x, -aabb.position.y, -aabb.get_center().z)
|
||||
var out := ArrayMesh.new()
|
||||
out.resource_name = mesh.resource_name
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays: Array = mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||||
continue
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for i in verts.size():
|
||||
verts[i] = verts[i] + offset
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
var mat: Material = mesh.surface_get_material(s)
|
||||
if mat != null:
|
||||
out.surface_set_material(out.get_surface_count() - 1, mat)
|
||||
return out
|
||||
1
src/viewport/glb_mesh_store.gd.uid
Normal file
1
src/viewport/glb_mesh_store.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bdbx4rc3fx5ix
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
extends Node3D
|
||||
## Baut die visuelle Repräsentation der Map im Editor-Viewport:
|
||||
## Terrain-Höhenfeld-Mesh + Maßstabsgitter + Himmel/Licht
|
||||
## + Platzhalter-Meshes für Bibliotheks-Objekte + Spawn-Marker mit Beschriftung.
|
||||
## + Echte GLB-Meshes für Bibliotheks-Objekte (GlbMeshStore; ohne GLB =
|
||||
## Platzhalter-Mesh aus dem Kollisions-Eintrag) + Spawn-Marker mit Beschriftung.
|
||||
## ##
|
||||
## Objekt-Marker werden in object_markers gemerkt (Meta: obj_index,
|
||||
## pick_radius) — Auswahl/Werkzeuge (interaction.gd) greifen darauf zu,
|
||||
|
|
@ -24,6 +25,7 @@ const WEATHER_LIGHT := {
|
|||
var selected_index := -1 # aktuelles Auswahl-Objekt (orange gefärbt)
|
||||
var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl)
|
||||
var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects)
|
||||
var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis)
|
||||
var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius)
|
||||
var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant)
|
||||
var gebaeude_markers: Array = [] # MeshInstance3D je Bauteil (Karte t_97ea59c5)
|
||||
|
|
@ -195,10 +197,15 @@ func _add_line(mesh: ImmediateMesh, from: Vector3, to: Vector3, color: Color) ->
|
|||
|
||||
|
||||
## Platzhalter je Objekt: Form aus dem Bibliotheks-Manifest (wie der Server-
|
||||
## Kollider), Position = Objekt-Fußpunkt. Marker-Root bekommt Metas für
|
||||
## Auswahl/Anfassen: obj_index, pick_radius (Kugel-Approximation), half_height.
|
||||
## Kollider), Position = Objekt-Fußpunkt. ECHTES GLB-Mesh vorziehen
|
||||
## (Karte t_9b50b231, GlbMeshStore): vorhanden = echtes Modell mit Texturen,
|
||||
## fehlend = Platzhalter + Eintrag in fehlende_glbs (Statuszeilen-Hinweis).
|
||||
## Marker-Root bekommt Metas für Auswahl/Anfassen: obj_index, pick_radius
|
||||
## (Kugel-Approximation), half_height, glb (true = echtes Modell — keine
|
||||
## Grundfarbe drüberlegen, sonst sind die Texturen weg).
|
||||
func _build_objects(objects: Array, manifest: Dictionary) -> void:
|
||||
var lib: Dictionary = manifest.get("objects", {})
|
||||
fehlende_glbs = []
|
||||
for i in objects.size():
|
||||
var obj = objects[i]
|
||||
if not (obj is Dictionary):
|
||||
|
|
@ -211,7 +218,12 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void:
|
|||
var entry: Dictionary = lib[oid]
|
||||
var col: Dictionary = entry.get("collision", {})
|
||||
var scale := float(obj.get("scale", 1.0))
|
||||
var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, scale)
|
||||
var mesh := GlbMeshStore.mesh_fuer(oid)
|
||||
var echt := mesh != null
|
||||
if not echt:
|
||||
if GlbMeshStore.fehlt(oid):
|
||||
fehlende_glbs.append(oid)
|
||||
mesh = MeshShapes.make_placeholder(str(col.get("shape", "box")), col, scale)
|
||||
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
|
||||
var marker := Node3D.new()
|
||||
marker.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||||
|
|
@ -219,12 +231,21 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void:
|
|||
marker.set_meta("obj_index", i)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.position.y = MeshShapes.half_height(mesh)
|
||||
mi.material_override = _select_mat if _is_selected(i) else _base_mat
|
||||
if echt:
|
||||
# Normalisiertes GLB liegt schon in Fußpunkt-Koordinaten
|
||||
# (Unterkante y = 0) → KEIN half_height-Lift; Objekt-Maßstab
|
||||
# über Node-Scale. AABB-Metas mal scale (Node-Scale geht nicht
|
||||
# in mesh.get_aabb() ein).
|
||||
mi.scale = Vector3.ONE * scale
|
||||
else:
|
||||
mi.position.y = MeshShapes.half_height(mesh)
|
||||
mi.material_override = _select_mat if _is_selected(i) else (null if echt else _base_mat)
|
||||
marker.add_child(mi)
|
||||
var half := (mesh.get_aabb().size * 0.5) as Vector3
|
||||
var half: Vector3 = (mesh.get_aabb().size * (scale if echt else 1.0) * 0.5) as Vector3
|
||||
marker.set_meta("pick_radius", half.length() + maxf(0.6, half.y * 0.25))
|
||||
marker.set_meta("half_height", MeshShapes.center_height(mesh))
|
||||
marker.set_meta("half_height",
|
||||
MeshShapes.center_height(mesh) * (scale if echt else 1.0))
|
||||
marker.set_meta("glb", echt)
|
||||
object_markers.append(marker)
|
||||
add_child(marker)
|
||||
|
||||
|
|
@ -238,6 +259,9 @@ func set_selected(idx: int) -> void:
|
|||
|
||||
## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte färben sich
|
||||
## orange — Gruppen/Mehrfachauswahl sind auf einen Blick erkennbar.
|
||||
## Echte GLB-Objekte (Meta „glb") bekommen im ungewählten Zustand KEINE
|
||||
## Grundfarbe drübergelegt — material_override würde die Texturen verstecken
|
||||
## (Karte t_9b50b231); orange bleibt auch dort das Auswahl-Signal.
|
||||
func set_selected_multi(indices: Array) -> void:
|
||||
selected_indices = indices.duplicate()
|
||||
selected_index = indices.back() if not indices.is_empty() else -1
|
||||
|
|
@ -245,7 +269,8 @@ func set_selected_multi(indices: Array) -> void:
|
|||
if marker == null or not is_instance_valid(marker):
|
||||
continue
|
||||
var mi: MeshInstance3D = marker.get_child(0)
|
||||
mi.material_override = _select_mat if _is_selected(int(marker.get_meta("obj_index", -1))) else _base_mat
|
||||
var echt := bool(marker.get_meta("glb", false))
|
||||
mi.material_override = _select_mat if _is_selected(int(marker.get_meta("obj_index", -1))) else (null if echt else _base_mat)
|
||||
|
||||
|
||||
func _is_selected(idx: int) -> bool:
|
||||
|
|
|
|||
200
tests/test_glb_view.gd
Normal file
200
tests/test_glb_view.gd
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
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)")
|
||||
1
tests/test_glb_view.gd.uid
Normal file
1
tests/test_glb_view.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dme0ulusw80bp
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
cd "$(dirname "$0")/.."
|
||||
GODOT=~/bin/godot4
|
||||
rc=0
|
||||
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_spawns test_testrun test_bauen test_gui_smoke; do
|
||||
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_gui_smoke; do
|
||||
"$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
|
||||
code=$?
|
||||
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue