Zum Inhalt

Physarum Native — Authoring Editor + Preset Management (Increment 3b) Design Spec

Status: Draft · 2026-06-03 · author: Johannes (brainstormed with Claude) Builds on: 2026-06-02-universal-app-macos-playback-design.md (the app scaffold + macOS playback that already ships). Turns the native app from a viewer into the full authoring tool — exploring the parameter space and generating emergent patterns is the product. All web-app authoring features come to native, plus exploration features the web app doesn't have.

1. Kontext & Ziel

Die native App (apple/PhysarumApp/) spielt heute Presets ab (macOS-Playback, 25 Presets, kuratierte Slider). Das ist zu wenig: Der eigentliche Reiz von Physarum ist das Erkunden des Parameterraums und das Erzeugen emergenter Muster. Diese Spec bringt den vollen Authoring-Editor (alle ~52 Parameter) + komplettes Preset-Management nach nativ, über macOS + iPad + iPhone (adaptiv), und ergänzt vier Erkundungs-Features, die die Web-App nicht hat (Randomize/Mutate, Parameter-Locks, Undo/Redo, Preset-Morph).

2. Festgezurrte Entscheidungen (Brainstorming 2026-06-02/03)

Achse Entscheidung
Scope Voller Editor + volles Preset-Management + 4 Erkundungs-Features, in einer Spec.
Plattformen macOS + iPad + iPhone (Authoring), adaptiv über ein SwiftUI-Codebase. tvOS bleibt Playback-only (kein Editor).
Editor-Layout Live-Player immer sichtbar; Editor adaptiv: .inspector (regular: macOS/iPad-landscape) ↔ Bottom-Sheet (compact: iPhone/iPad-portrait).
Kern-Architektur Ein Parameter-Registry (Metamodell aller editierbaren Params) speist Editor, Randomize, Locks, Undo, Morph aus einer Quelle.
Persistenz Custom-Presets als JSON-Files (App-Support-Dir), Web-Schema-kompatibel, später CloudKit-migrierbar.
Live vs Respawn ParamDescriptor.isRespawn entscheidet automatisch: Live-Params jeden Frame; Respawn-Params (agentCount/spawnPattern/populationShare) via debounced Respawn (~250 ms).
Erkundungs-Features Randomize, Mutate, Locks, Undo/Redo, Preset-Morph — alle übers Registry.

3. Kern-Architektur: Parameter-Registry (Metamodell)

Der DRY-Schlüssel. Jeder editierbare Parameter wird einmal beschrieben:

struct ParamDescriptor {
    let id: ParamID                       // stable identity (for locks/undo)
    let label: String
    let section: EditorSection            // global | species(i) | matrix | pheromone | dynamics | effects
    let range: ClosedRange<Double>
    let step: Double?
    let isRespawn: Bool                   // agentCount/spawn/populationShare = true
    let get: (PresetParams) -> Double
    let set: (inout PresetParams, Double) -> Void
    let tooltip: String                   // from the web paramHelp dictionary
}
enum ParamID: Hashable { /* one case per editable param incl. species index + matrix r,c */ }
let ParamRegistry: [ParamDescriptor]      // the single source of all editable params

Speist: - Editor — Slider/Controls generiert aus den Descriptors einer Sektion (kein 52× Boilerplate). - Randomize/Mutate — Zufalls-/Variationswert in range, gelockte id überspringen. - Locks — Set<ParamID>. - Undo/Redo — Snapshots des ganzen PresetParams (value type). - Morph — Interpolation jedes Params entlang range. - Doppeltap-Reset — descriptor.set(&edited, descriptor.get(baseline)) für einen Param.

Diskrete/nicht-skalare Params (spawnPattern, blendMode als Enum; species color als RGB; matrix cells) bekommen passende Descriptor-Varianten bzw. eigene Editoren (Color-Picker, Picker), sind aber über dieselbe ParamID-Identität in Locks/Undo/Morph eingebunden.

4. State-Architektur (AppState wächst)

@Observable final class AppState {
    var edited: PresetParams         // the live-edited params (replaces the old LiveParams)
    var baseline: PresetParams       // the selected preset's original — for dirty + per-param reset
    var undo: [PresetParams]; var redo: [PresetParams]
    var locked: Set<ParamID>
    // + existing: presets catalog, selectedIndex, autoCycle, custom-preset store
    var isDirty: Bool { edited != baseline }
}
  • Live-Anwendung: edited geht jeden Frame an renderer.render(preset:) + sim.updateLiveParams(edited) (beides existiert). Post-FX + Sim-Live-Params sofort.
  • Respawn: Ändert sich ein isRespawn-Param, startet der RenderCoordinator einen debounced Respawn (~250 ms): neue SimEngine(edited) + resetFeedback. (Port des Web-respawnCoordinator-Verhaltens.)
  • Undo-Granularität: ein Snapshot bei jeder committed Änderung — Slider-Release, Randomize, Mutate, Preset-Wechsel, Doppeltap-Reset — nicht pro Drag-Pixel.

5. Editor-UI (adaptiv)

  • Präsentation: @Environment(\.horizontalSizeClass). Regular → .inspector(isPresented:) neben dem Player, mit Ein-/Ausklapp-Toggle (behebt „rechte Spalte nicht einklappbar"). Compact → Bottom-Sheet (.presentationDetents([.medium, .large])), Player bleibt sichtbar.
  • Vollbild/Cinema (behebt „kein Vollbild"): Toggle (Hotkey C + Button) → Player formatfüllend, Chrome/Editor aus, Tap/Esc zurück; macOS zusätzlich natives Fullscreen.
  • Sektionen als ausklappbare DisclosureGroups, generiert aus dem Registry: Global · Species (3 Tabs + „Alle") · Interaction Matrix (3×3-Grid, farbcodiert) · Pheromone · Dynamics · Effects.
  • Editor-Toolbar: Randomize · Mutate · Undo · Redo · Reset · Save (+ Dirty-Indikator).
  • ParamSlider (ein generisches Control aus ParamDescriptor):
  • Slider + Label + Wert; Tap-auf-Wert → numerische Eingabe; Doppelklick/Doppeltap → Reset auf Preset-Default (baseline-Wert dieses Params).
  • macOS: Shift = fein (0.1×), Alt = grob (4×) bei Drag/Wheel/Pfeilen; Hover-Tooltip aus paramHelp.
  • Touch: Drag = grob, Tap-to-type = präzise (idiomatisches Shift/Alt-Äquivalent).
  • Lock-Icon (🔒) pro Param.
  • Species-Editor: 3 Tabs (1/2/3) + „Alle" (synchron), je 7 Params + Color-Picker.
  • Matrix-Editor: 3×3-Grid, Zeile = sensende Spezies, Spalte = Quelle, Zellen −2…+2.

6. Preset-Management

  • Persistenz: PresetStore — Custom-Presets als JSON-Files (eine pro Preset) im App-Support-Dir; Web-Schema-kompatibel; Reihenfolge persistiert (z.B. Index-Präfix oder Order-Manifest). Built-ins bleiben read-only aus dem Bundle.
  • Operationen: Save (edited → neues Custom-Preset; SaveDialog für Name/Tags/tagSignal), Rename, Delete, Reorder. Built-in dirty → „Save as new"; Custom dirty → „Save" (overwrite) oder „Save as new" (Web-Verhalten: overwrite erhält die Position).
  • Library erweitert: Built-in + „Meine Presets" getrennt; Tag-Filter (Multi-Select, AND) + Suche (Name/Tags/tagSignal); Selektion setzt baseline = edited = preset.params.

7. Erkundungs-Features

  • Randomize: jeder nicht-gelockte Param → Zufallswert in range (mit sinnvollen Sub-Range-Grenzen pro Param, damit das Ergebnis lebendig bleibt statt „tot"/NaN); löst bei Respawn-Params einen debounced Respawn aus. Pusht einen Undo-Snapshot.
  • Mutate: nicht-gelockte Params um ±~15 % der range variieren, ausgehend vom aktuellen Stand — „in der Nähe weitersuchen". Pusht Undo.
  • Locks: Set<ParamID> via 🔒 am Slider; Randomize/Mutate respektieren sie.
  • Undo/Redo: PresetParams-Snapshot-Stacks; Toolbar-Buttons + Hotkeys (⌘Z/⌘⇧Z).
  • Morph: zwei Presets (A = aktuell, B = aus Library gewählt) + Morph-Slider 0…1. Interpoliert über das Registry: skalare Params linear in range; RGB/Matrix komponentenweise; diskrete (spawnPattern/blendMode) per Schwelle bei 0.5; agentCount interpoliert → debounced Respawn. Ergebnis als neues Preset speicherbar.

8. Engine-Erweiterungen (PhysarumEngine)

  • SimEngine.updateLiveParams(_:) existiert (respawn-frei) — deckt die Live-Sim-Params.
  • Debounced Respawn lebt im App-RenderCoordinator (er erzeugt schon neue SimEngines bei Preset-Wechsel) — ein Timer-gated Re-Create bei isRespawn-Änderung. Keine neue Engine-API zwingend nötig; ggf. ein kleiner Helfer.
  • Param-Interpolation (Morph) lebt in der App-Schicht (PresetParams ist value type) übers Registry — keine Engine-Änderung.

9. Testing & Verifikation

  • Unit-testbar (Engine-Suite bzw. ein App-Test-Target):
  • ParamRegistry: get/set-Roundtrip über alle ~52 Params; jeder Descriptor liest+schreibt sein Feld korrekt; Ranges nicht-leer.
  • Randomize respektiert Locks (gelockte Params unverändert); Ergebnisse in Range.
  • Mutate bleibt in Range, ausgehend von Basis.
  • Morph: Endpunkte (0 → A, 1 → B) exakt; Mitte plausibel; diskrete schalten bei 0.5.
  • PresetStore: save/load/delete/reorder Roundtrip (JSON ↔ Modell).
  • Undo/Redo-Stack: push/pop-Verhalten, redo-Invalidierung bei neuer Änderung.
  • Build + visuell (Johannes auf macOS/iPad/iPhone): adaptives Layout, Slider-Interaktionen (tap-to-type, Doppeltap-Reset, Modifier), Vollbild, Live-Render beim Schrauben, Randomize/ Morph visuell, Preset speichern/laden.
  • Gate: swift test grün; xcodebuild (macOS) BUILD SUCCEEDED; App startet + Editor funktioniert.

10. Scope (in) / Out-of-scope

In: voller Editor (~52 Params) · adaptive Inspector/Sheet-UI (macOS/iPad/iPhone) · einklappbares Panel + Vollbild-Fix · ParamSlider (tap-to-type, Doppeltap-Reset, macOS-Modifier, Lock) · Species-Tabs + Matrix-Grid · Tooltips (paramHelp portiert) · volles Preset-Mgmt (save/rename/delete/reorder/tag-filter/suche, JSON-Persistenz) · Randomize/Mutate/Locks/Undo/ Redo/Morph · debounced Respawn.

Out-of-scope (spätere Increments): Capture/Export (PNG/Video) · Audio-Reaktivität · CloudKit-Sync · tvOS-Editor-UI · iOS-Device-Signing-Setup (Free-Provisioning braucht einen einmaligen Xcode-Login von Johannes für echte Geräte; Sim-Builds gehen ohne).

11. Offene Risiken / noch zu verifizieren

  • ParamID-Vollständigkeit: muss exakt die ~52 Params + species-Indizes + matrix-Zellen abdecken; im Plan gegen schema.ts/params.ts Feld-für-Feld verifizieren.
  • Randomize-Sub-Ranges: volle Zod-Ranges können „tote"/überzogene Felder erzeugen — pro Param sinnvolle Generierungs-Grenzen festlegen (im Plan, anhand der Preset-Wertebereiche).
  • App-Test-Target: ob ein eigenes XCTest-Target im XcodeGen-project.yml oder die Logik in die Engine-Suite gehoben wird — im Plan entscheiden (Registry/PresetStore sind App-Code).
  • Sheet + MTKView Performance auf iPhone (Live-Render hinter einem .medium-Sheet) — auf Gerät messen.
  • Debounce-Thread-Safety: Respawn-Timer + Draw-Loop auf demselben (Main-)Thread halten.