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:
editedgeht jeden Frame anrenderer.render(preset:)+sim.updateLiveParams(edited)(beides existiert). Post-FX + Sim-Live-Params sofort. - Respawn: Ändert sich ein
isRespawn-Param, startet derRenderCoordinatoreinen debounced Respawn (~250 ms): neueSimEngine(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 ausParamDescriptor):- 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;
SaveDialogfü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
rangevariieren, 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 beiisRespawn-Ä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 testgrü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.tsFeld-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.ymloder 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.