scopedvr-map-editor/tools/gen_thumbs.gd
Hermes (godot-vr) 3a5c9d44bb Objekt-Bibliothek: Gruppen-Browser mit Thumbnails, Snap-to-Ground, Raster-Snap, Tests
- Bibliothek-Browser: 11 etablierte Objekte in deutschen Klapp-Gruppen
  (Kisten & Barrieren / Bauwerke / Natur, neue IDs wandern nach Sonstiges)
- 128-px-Thumbnails einmalig gerendert (tools/gen_thumbs.gd, xvfb) und
  committed; fehlendes PNG = sauberer Text-Fallback (LibraryBrowser)
- Snap to Ground: ein Klick setzt ALLE schwebenden Objekte auf Terrain-Höhe,
  EIN Undo-Eintrag (ObjectOps.snap_to_ground)
- Raster-Snap (1 m): Toggle — rastet Platzieren und Ziehen auf ganze Meter
- Deutsche Namen nach LibraryBrowser verschoben (headless testbar)
- tests/test_library.gd: 47 Checks (Bibliothek, Gruppen, Thumbnails, Snap,
  CRUD+Undo über alle Objekte, svmap-Roundtrip byte-identisch, Poly-Budget)
- Desktop-Smoke: --library-demo erzeugt Screenshots (lokal via xvfb verifiziert)
- UI-Layout: linke Spalte min 909→797 px, Statusleiste wieder vollständig sichtbar
2026-10-15 12:18:11 +00:00

100 lines
3.1 KiB
GDScript

extends SceneTree
## Thumbnail-Generator für die Objekt-Bibliothek (Karte t_3a998ae4).
## Rendert JEDESM Bibliotheks-Objekt einmalig als 128-px-PNG nach
## library/thumbs/<id>.png — die Bilder werden committed, der Editor lädt sie
## dann einfach aus der Datei (LibraryBrowser.load_thumb).
##
## Ausführen (braucht echtes Rendering, NICHT --headless):
## xvfb-run -a godot4 --rendering-driver opengl3 --path . --script res://tools/gen_thumbs.gd
## (auf Slys PC auch ohne xvfb: godot4 --rendering-driver opengl3 --path . --script ...)
##
## Exit 0 = alle Thumbnails geschrieben.
const SIZE := 128
const BG_COLOR := Color(0.16, 0.18, 0.21)
var manifest: Dictionary = {}
var ids: Array = []
var idx := -1
var settle_frames := 0
var vp: SubViewport
var mi: MeshInstance3D
var cam: Camera3D
var out_dir := ""
func _initialize() -> void:
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH))
if parsed is Dictionary:
manifest = parsed
ids = (manifest.get("objects", {}) as Dictionary).keys()
if ids.is_empty():
push_error("gen_thumbs: Bibliothek leer — nichts zu rendern.")
quit(1)
return
out_dir = ProjectSettings.globalize_path("res://library/thumbs")
DirAccess.make_dir_recursive_absolute(out_dir)
vp = SubViewport.new()
vp.size = Vector2i(SIZE, SIZE)
vp.own_world_3d = true
vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(vp)
var env := Environment.new()
env.background_mode = Environment.BG_COLOR
env.background_color = BG_COLOR
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)
mi = MeshInstance3D.new()
vp.add_child(mi)
cam = Camera3D.new()
cam.current = true
vp.add_child(cam)
idx = -1 # erst im ersten _process starten — look_at braucht eingetretene Knoten
func _next() -> void:
idx += 1
if idx >= ids.size():
print("GEN_THUMBS_OK: %d Thumbnails in %s" % [ids.size(), out_dir])
quit(0)
return
var oid := str(ids[idx])
var entry: Dictionary = (manifest["objects"] as Dictionary)[oid]
var col: Dictionary = entry.get("collision", {})
var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
mi.mesh = mesh
# Kamera diagonal aufs Objekt, passend zur Mesh-Größe (Kiste nah, Turm weit).
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)
settle_frames = 4
func _process(_delta: float) -> bool:
if idx < 0:
_next()
return false
if settle_frames > 0:
settle_frames -= 1
return false
var img := vp.get_texture().get_image()
var oid := str(ids[idx])
img.save_png(out_dir + "/" + oid + ".png")
print(" thumb: %s (%dx%d)" % [oid, img.get_width(), img.get_height()])
_next()
return false