import Foundation
import Metal
import PhysarumEngine
// Renders every preset JSON in a directory to a thumbnail PNG (dir/thumbs/<id>.png),
// so presets can be designed and visually iterated headless. Web-JSON compatible.
// Usage: swift run PresetGallery <presets-dir> [frames] [size]
let args = CommandLine.arguments
let dir = args.count > 1 ? args[1] : "presets"
let frames = args.count > 2 ? Int(args[2]) ?? 400 : 400
let size = args.count > 3 ? Int(args[3]) ?? 512 : 512
guard let device = MTLCreateSystemDefaultDevice() else { fatalError("no Metal device") }
let lib = ShaderLibrary(device: device)
let renderer = try Renderer(device: device, library: lib, width: size, height: size)
let fm = FileManager.default
let dirURL = URL(fileURLWithPath: dir)
let thumbsURL = dirURL.appendingPathComponent("thumbs")
try? fm.createDirectory(at: thumbsURL, withIntermediateDirectories: true)
let files = ((try? fm.contentsOfDirectory(at: dirURL, includingPropertiesForKeys: nil)) ?? [])
.filter { $0.pathExtension == "json" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
print("rendering \(files.count) presets at \(size)×\(size), \(frames) frames")
for f in files {
do {
let preset = try JSONDecoder().decode(Preset.self, from: Data(contentsOf: f))
let sim = try SimEngine(device: device, library: lib, width: size, height: size, preset: preset.params)
// Render every frame (not just the last) so temporal feedback / echo
// accumulates the same way the live viewer + web bake do.
renderer.resetFeedback()
for _ in 0..<frames {
sim.step()
try renderer.renderOffscreen(sim: sim, preset: preset.params)
}
let out = thumbsURL.appendingPathComponent("\(preset.id).png").path
_ = try renderer.savePNG(to: out)
print(" ✓ \(preset.id) — \(preset.name)")
} catch {
print(" ✗ \(f.lastPathComponent): \(error)")
}
}
print("thumbs → \(thumbsURL.path)")