import AppKit
import Metal
import MetalKit
import PhysarumEngine
// Native macOS Ambiance Viewer — a live, lean-back window that runs the native
// PhysarumEngine and cycles through bundled presets. Foundation for the new DMG
// and shares the renderer with the future tvOS target.
// space = next preset · f = toggle fullscreen · q / esc = quit
let FIELD = 720
let SWITCH_FRAMES = 60 * 25 // auto-advance every ~25 s at 60 fps
final class Coordinator: NSObject, MTKViewDelegate {
let device: MTLDevice
let lib: ShaderLibrary
let renderer: Renderer
let presets: [Preset]
var idx = 0
var sim: SimEngine
var frameInPreset = 0
weak var window: NSWindow?
init(device: MTLDevice) throws {
self.device = device
self.lib = ShaderLibrary(device: device)
self.renderer = try Renderer(device: device, library: lib, width: FIELD, height: FIELD)
self.presets = Coordinator.loadPresets()
guard let first = presets.first else { fatalError("no presets bundled") }
self.sim = try SimEngine(device: device, library: lib, width: FIELD, height: FIELD, preset: first.params)
super.init()
}
static func loadPresets() -> [Preset] {
guard let dir = Bundle.module.url(forResource: "Presets", withExtension: nil) else { return [] }
let urls = (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)) ?? []
return urls
.filter { $0.pathExtension == "json" }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
.compactMap { try? JSONDecoder().decode(Preset.self, from: Data(contentsOf: $0)) }
}
var currentName: String { presets.isEmpty ? "" : presets[idx].name }
func advance(by step: Int) {
guard !presets.isEmpty else { return }
idx = (idx + step + presets.count) % presets.count
if let s = try? SimEngine(device: device, library: lib, width: FIELD, height: FIELD, preset: presets[idx].params) {
sim = s
}
renderer.resetFeedback() // blank echo canvas for the new preset
frameInPreset = 0
window?.title = "Physarum Ambiance — \(currentName)"
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
func draw(in view: MTKView) {
sim.step()
frameInPreset += 1
if frameInPreset >= SWITCH_FRAMES { advance(by: 1) }
guard let desc = view.currentRenderPassDescriptor, let drawable = view.currentDrawable else { return }
renderer.render(sim: sim, preset: presets[idx].params, into: desc, drawable: drawable)
}
}
final class ViewerView: MTKView {
weak var coord: Coordinator?
override var acceptsFirstResponder: Bool { true }
override func keyDown(with e: NSEvent) {
switch e.charactersIgnoringModifiers {
case " ": coord?.advance(by: 1)
case "p", "P": coord?.advance(by: -1)
case "f", "F": window?.toggleFullScreen(nil)
case "q", "Q", "\u{1b}": NSApp.terminate(nil)
default: super.keyDown(with: e)
}
}
}
// ── App bootstrap ───────────────────────────────────────────────────────────
guard let device = MTLCreateSystemDefaultDevice() else { fatalError("no Metal device") }
// Self-test mode: build everything + render a few frames headless, then exit.
// Lets CI/verification confirm the viewer wiring without a window.
if CommandLine.arguments.contains("--selftest") {
let coord = try Coordinator(device: device)
for _ in 0..<30 { coord.sim.step() }
print("selftest ok: \(coord.presets.count) presets, sim frame \(coord.sim.frame)")
exit(0)
}
let app = NSApplication.shared
app.setActivationPolicy(.regular)
let coord = try Coordinator(device: device)
let frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
let view = ViewerView(frame: frame, device: device)
view.colorPixelFormat = .bgra8Unorm
view.preferredFramesPerSecond = 60
view.delegate = coord
view.coord = coord
let window = NSWindow(
contentRect: frame,
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false)
window.title = "Physarum Ambiance — \(coord.currentName)"
window.contentView = view
window.center()
window.makeKeyAndOrderFront(nil)
window.makeFirstResponder(view)
coord.window = window
// Smoke mode: run the real window briefly, then self-terminate (verification).
if CommandLine.arguments.contains("--smoke") {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
print("smoke ok: live-rendered to frame \(coord.sim.frame), preset \(coord.currentName)")
NSApp.terminate(nil)
}
}
app.activate(ignoringOtherApps: true)
app.run()