saumwacht-godot/tools/model_contact_sheet.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

130 lines
5.6 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.

extends Node
## Real Godot renders. Use model_gallery.tscn -- --capture after GLB import.
const STAGE: Script = preload("res://tools/model_gallery_stage.gd")
const OUT_DIR: String = "res://docs/screenshots/phase4"
const GROUPS: Array[Dictionary] = [
{"key": "glutpakt", "title": "GLUTPAKT", "subtitle": "Basalt · Orangeglut · Messing"},
{"key": "nietenkonvent", "title": "NIETENKONVENT", "subtitle": "Petrol · Ocker · Stahl · Keramik"},
{"key": "nebelchor", "title": "NEBELCHOR", "subtitle": "Indigo · Violett · Mint · Nebel"},
{"key": "wurzelbund", "title": "WURZELBUND", "subtitle": "Borke · Moos · Blatt · Blüte"},
{"key": "neutral", "title": "SÖLDNER & ARBEITER", "subtitle": "Fünf Send-Einheiten und der Funkenleser"},
{"key": "rift", "title": "RISSKREATUREN", "subtitle": "Schalen · Knochen · Rissglut"},
{"key": "environment", "title": "SPIELFELD & SAUMKERNE", "subtitle": "Modulares Lane-Segment und zwei Teamkerne"}
]
var _assets: Array[Dictionary] = []
var _sheet: Control
var _views: Array[Dictionary] = []
func _ready() -> void:
var manifest: String = "res://assets/phase4_manifest.json"
if not FileAccess.file_exists(manifest):
_fail("Phase 4 manifest is missing; export all models first.")
return
var data: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest))
if not data is Dictionary:
_fail("Invalid Phase 4 manifest.")
return
for value: Variant in data.get("assets", []):
if value is Dictionary:
_assets.append(value)
if _assets.size() != 59:
_fail("Expected 59 exported models, found %d." % _assets.size())
return
for asset: Dictionary in _assets:
if not ResourceLoader.exists(str(asset.get("resource_path", ""))):
_fail("Model has not been imported: " + str(asset.get("id", "?")))
return
DirAccess.make_dir_recursive_absolute(OUT_DIR)
get_window().size = Vector2i(1600, 960)
get_tree().create_timer(90.0).timeout.connect(func() -> void: _fail("Capture timed out."))
for group: Dictionary in GROUPS:
if not _build_sheet(group):
return
await get_tree().process_frame
await get_tree().process_frame
await RenderingServer.frame_post_draw
await get_tree().create_timer(0.25).timeout
var image: Image = get_viewport().get_texture().get_image()
var output: String = OUT_DIR + "/models-" + str(group["key"]) + ".png"
var error: Error = image.save_png(output)
if error != OK:
_fail("Could not save " + output)
return
print("CAPTURED ", output, " (", image.get_width(), "×", image.get_height(), ")")
_sheet.free()
_views.clear()
print("PASS: captured all 59 models in seven real Godot contact sheets.")
get_tree().quit(0)
func _build_sheet(group: Dictionary) -> bool:
var selected: Array[Dictionary] = []
var key: String = str(group["key"])
for asset: Dictionary in _assets:
var matches: bool = str(asset.get("faction", "")) == key
if key == "environment":
matches = str(asset.get("role", "")) in ["core", "lane"]
if matches:
selected.append(asset)
_sheet = Control.new()
_sheet.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(_sheet)
var background: ColorRect = ColorRect.new()
background.color = Color("10141b")
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_sheet.add_child(background)
var margin: MarginContainer = MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for edge: String in ["left", "right"]:
margin.add_theme_constant_override("margin_" + edge, 28)
for edge: String in ["top", "bottom"]:
margin.add_theme_constant_override("margin_" + edge, 22)
_sheet.add_child(margin)
var layout: VBoxContainer = VBoxContainer.new()
layout.add_theme_constant_override("separation", 12)
margin.add_child(layout)
layout.add_child(_label("SAUMWACHT / " + str(group["title"]), 30, Color("ddc898")))
layout.add_child(_label("Phase 4 · " + str(group["subtitle"]) + " · %d Modelle" % selected.size(), 17, Color("abb5c5")))
var grid: GridContainer = GridContainer.new()
grid.columns = 3 if selected.size() <= 6 else 5
grid.size_flags_vertical = Control.SIZE_EXPAND_FILL
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid.add_theme_constant_override("h_separation", 12)
grid.add_theme_constant_override("v_separation", 16)
layout.add_child(grid)
for asset: Dictionary in selected:
var card: VBoxContainer = VBoxContainer.new()
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.size_flags_vertical = Control.SIZE_EXPAND_FILL
card.add_theme_constant_override("separation", 5)
grid.add_child(card)
var view: Dictionary = STAGE.create(card, Vector2(270, 230))
_views.append(view)
if not STAGE.show_asset(view, asset):
_fail("Capture failed to load " + str(asset["id"]))
return false
STAGE.add_id_tag(view, str(asset["id"]))
var title: Label = _label(str(asset["name"]), 19, Color("eee6d8"))
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
card.add_child(title)
var detail: Label = _label("%s · %d Dreiecke" % [str(asset["id"]).to_upper(), int(asset.get("triangles", 0))], 14, Color("a1acbd"))
detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
card.add_child(detail)
layout.add_child(_label("Originale parametrisierte Modelle · Einheitliche Studiobeleuchtung · Vorschau proportional auf gemeinsame Bühne eingepasst", 14, Color("8491a3")))
return true
func _label(text: String, font_size: int, color: Color) -> Label:
var result: Label = Label.new()
result.text = text
result.add_theme_font_size_override("font_size", font_size)
result.add_theme_color_override("font_color", color)
return result
func _fail(message: String) -> void:
printerr("FAIL: ", message)
get_tree().quit(1)