scopedvr-map-editor/addons/modular_builder/scopedvr_export_ui.gd
2026-11-01 19:34:05 +01:00

250 lines
9 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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 + "/core/thumbs")
img.save_png(editor_dir + "/core/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