Zum Inhalt

apple/PhysarumEngine/Sources/PhysarumViewer/main.swift

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()