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)