@tool extends RefCounted ## UI-Ablauf des ScopedVR-Bibliothek-Exports (Karte t_82d2a8dd): Name-Dialog → ## Mesh normalisieren → GLB + Trimesh-Vertices → tools/objekt (Match-Server-Repo) ## → Thumbnail → Status. ## ## Bewusst getrennt von scopedvr_export.gd (Kern, statisch/testbar) — dieses ## Skript braucht die Editor-Umgebung (Dialoge, OS.execute, Rendering). ## Deutsch für Sly: alle Status-/Fehlermeldungen auf Deutsch. const KERN := preload("res://addons/modular_builder/scopedvr_export.gd") const BUDGET := 20000 # Export-Strukturen (match-server BUILDER_BUDGET) const THUMB_SIZE := 128 var _plugin: EditorPlugin var _status: Callable var _building: Node3D var _mesh: ArrayMesh var _dialog: AcceptDialog var _name_edit: LineEdit ## Startet den Flow. building = gewählte Struktur, mesh = gebackenes Mesh ## (aus modular_mesh_baker), status = Callable(label: String) für das Dock-Statusfeld. func starten(plugin: EditorPlugin, building: Node3D, mesh: ArrayMesh, status: Callable) -> void: _plugin = plugin _building = building _mesh = mesh _status = status if _dialog != null: _dialog.queue_free() _dialog = AcceptDialog.new() _dialog.title = "Struktur exportieren — ScopedVR-Bibliothek" _dialog.ok_button_text = "Exportieren" var vbox := VBoxContainer.new() vbox.custom_minimum_size = Vector2(380, 0) var hinweis := Label.new() hinweis.text = "Name der Struktur (wird zur Objekt-ID: kleine Buchstaben,\nZahlen, Unterstrich — z. B. „Bauernhaus Rot\" → haus_bauernhaus_rot... \nrichtig: „Bauernhaus rot\" → bauernhaus_rot).\n\nDer Export durchläuft die Objekt-Pipeline (Mesh, Registry,\nClient, Editor) — der Client-Import dauert einen Moment." hinweis.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART vbox.add_child(hinweis) _name_edit = LineEdit.new() _name_edit.text = str(building.name) vbox.add_child(_name_edit) _dialog.add_child(vbox) _dialog.confirmed.connect(_on_name_ok) plugin.add_child(_dialog) _name_edit.text_submitted.connect(func(_t: String) -> void: _dialog.hide() _on_name_ok()) _name_edit.call_deferred("grab_focus") _dialog.popup_centered() func _on_name_ok() -> void: var name_roh := _name_edit.text var oid := KERN.slugify(name_roh) if oid.is_empty(): _status.call("FEHLER: Aus dem Namen „%s\" wird keine gültige Objekt-ID — bitte Buchstaben verwenden." % name_roh) return _status.call("Export läuft: „%s\" → %s (Pipeline: Mesh → Registry → Client → Editor) ..." % [name_roh, oid]) # Einen Frame abwarten, damit der Status sichtbar wird, bevor OS.execute blockiert. await _plugin.get_tree().process_frame await _export(oid, name_roh) func _export(oid: String, name_roh: String) -> void: var server_dir := _match_server_dir() if server_dir.is_empty(): _status.call("FEHLER: Match-Server-Repo nicht gefunden — erwartet ../scopedvr-match-server neben dem Map-Editor (oder SCOPEDVR_MATCH_SERVER_DIR).") return var objekt_tool := server_dir + "/tools/objekt" # 1) Normalisieren (Fußpunkt y=0, XZ zentriert). var norm: Dictionary = KERN.normalisiere(_mesh) var mesh: ArrayMesh = norm.get("mesh") if mesh == null: _status.call("FEHLER: Das gebackene Mesh ist leer — nichts zu exportieren.") return var size: Vector3 = norm["size"] var faces := KERN.trimesh_vertices(mesh) if faces.is_empty(): _status.call("FEHLER: Keine Kollisions-Dreiecke im gebackenen Mesh.") return # 2) GLB + Vertices temporär schreiben. var tmp_dir := _tmp_dir() var glb_path := tmp_dir + "/scopedvr_export_%s.glb" % oid var verts_path := tmp_dir + "/scopedvr_export_%s.json" % oid var glb := KERN.glb_bytes(mesh, oid) if glb.is_empty() or not KERN.glb_gueltig(glb): _status.call("FEHLER: GLB-Export fehlgeschlagen (GLTFDocument).") return var gf := FileAccess.open(glb_path, FileAccess.WRITE) if gf == null: _status.call("FEHLER: Konnte %s nicht schreiben." % glb_path) return gf.store_buffer(glb) gf.close() var vertices_liste: Array = [] for v in faces: vertices_liste.append([_r(v.x), _r(v.y), _r(v.z)]) var verts_json := JSON.stringify({ "vertices": vertices_liste, "size": [_r(size.x), _r(size.y), _r(size.z)], }) var vf := FileAccess.open(verts_path, FileAccess.WRITE) if vf == null: _status.call("FEHLER: Konnte %s nicht schreiben." % verts_path) return vf.store_string(verts_json) vf.close() # 3) Pipeline-Schritte 1–2 (Mesh + Registry). Fehler = sofort raus, klare Meldung. var schritt1 := _lauf(objekt_tool, ["mesh", oid, glb_path, "--budget", str(BUDGET)]) if schritt1 != "": _status.call(schritt1) return var schritt2 := _lauf(objekt_tool, ["registry", oid, name_roh.strip_edges(), "trimesh", "--vertices", verts_path, "--budget", str(BUDGET)]) if schritt2 != "": _status.call(schritt2) return # 4) Client-Sync (kopiert GLB + Registry in den Client, Import-Probe). _status.call("Export läuft: Client-Import (dauert einen Moment) ...") var schritt3 := _lauf(objekt_tool, ["client", oid]) if schritt3 != "": _status.call(schritt3) return # 5) Thumbnail AUS DEM ECHTEN MESH rendern (bevor der Editor-Sync kommt — # gen_thumbs überspringt Trimesh-Objekte mit vorhandener Datei). var editor_dir := _editor_dir() if not editor_dir.is_empty(): var img := await _thumbnail_bilden(mesh) if img != null: DirAccess.make_dir_recursive_absolute(editor_dir + "/library/thumbs") img.save_png(editor_dir + "/library/thumbs/" + oid + ".png") # 6) Editor-Sync (Registry-Kopie; vorhandene Thumbnails bleiben stehen). var schritt4 := _lauf(objekt_tool, ["editor", oid]) if schritt4 != "": _status.call(schritt4) return var vis_tris := 0 var tris := int(faces.size() / 3) for s in mesh.get_surface_count(): var arrays := mesh.surface_get_arrays(s) if arrays.is_empty(): continue # ARRAY_INDEX ist null bei nicht-indizierten Oberflächen (kein Cast!). var idx := PackedInt32Array() if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array: idx = arrays[Mesh.ARRAY_INDEX] var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] vis_tris += (idx.size() / 3) if not idx.is_empty() else (verts.size() / 3) _status.call("OK: „%s\" als '%s' exportiert (poly %d, Trimesh %d Dreiecke, Budget %d). Objekt liegt in Bibliothek + Client — Einchecken nicht vergessen (git add in 3 Repos)." % [name_roh, oid, vis_tris, tris, BUDGET]) _aufraeumen([glb_path, verts_path]) func _aufraeumen(pfade: Array) -> void: for p in pfade: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) func _lauf(objekt_tool: String, args: PackedStringArray) -> String: var out := [] var code: int = OS.execute(objekt_tool, args, out, true) var text := "\n".join(out) if code == 0: return "" # Die FEHLER-Zeile des Werkzeugs durchreichen (letzte gewinnt). var fehler := "Pipeline fehlgeschlagen (Exit %d)." % code for zeile in text.split("\n"): if str(zeile).begins_with("FEHLER: "): fehler = str(zeile) elif str(zeile).begins_with("FEHLER:"): fehler = str(zeile) return fehler func _match_server_dir() -> String: var env := OS.get_environment("SCOPEDVR_MATCH_SERVER_DIR") if not env.is_empty() and DirAccess.dir_exists_absolute(env): return env.rstrip("/") var root := ProjectSettings.globalize_path("res://").rstrip("/") var cand := root.path_join("../scopedvr-match-server") if DirAccess.dir_exists_absolute(cand): return cand return "" func _editor_dir() -> String: var env := OS.get_environment("SCOPEDVR_EDITOR_DIR") if not env.is_empty() and DirAccess.dir_exists_absolute(env): return env.rstrip("/") return ProjectSettings.globalize_path("res://").rstrip("/") func _tmp_dir() -> String: var t := OS.get_environment("TMPDIR") if t.is_empty(): t = "/tmp" return t.rstrip("/") static func _r(v: float) -> float: return snappedf(v, 0.0001) ## Thumbnail aus dem Mesh rendern (Editor-Rendering vorausgesetzt) — ## gleicher Stil wie tools/gen_thumbs.gd (128 px, dunkler Hintergrund). func _thumbnail_bilden(mesh: ArrayMesh) -> Image: var vp := SubViewport.new() vp.size = Vector2i(THUMB_SIZE, THUMB_SIZE) vp.own_world_3d = true vp.render_target_update_mode = SubViewport.UPDATE_ONCE _plugin.add_child(vp) var env := Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.16, 0.18, 0.21) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(1, 1, 1) env.ambient_light_energy = 0.7 var we := WorldEnvironment.new() we.environment = env vp.add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0) sun.light_energy = 1.1 vp.add_child(sun) var mi := MeshInstance3D.new() mi.mesh = mesh vp.add_child(mi) var cam := Camera3D.new() cam.current = true vp.add_child(cam) var aabb := mesh.get_aabb() var center := aabb.get_center() var radius := aabb.size.length() * 0.5 var dir := Vector3(1.0, 0.65, 1.0).normalized() cam.position = center + dir * (radius * 2.3 + 0.3) cam.look_at(center, Vector3.UP) # Ein Frame Rendering abwarten, dann auslesen und aufräumen. await _plugin.get_tree().process_frame await _plugin.get_tree().process_frame var img := vp.get_texture().get_image() vp.queue_free() return img