import PhysarumEngine
import simd
enum EditorSection: String, CaseIterable {
case global, species, matrix, pheromone, dynamics, effects
}
/// Stable identity for every editable parameter (scalars + non-scalars), so the
/// editor, locks, undo and morph (later plans) can address each one.
enum ParamID: Hashable {
// global scalars
case agentCount, exposure, simSpeed
// global non-scalars (own controls, not in the scalar registry)
case spawnPattern, blendMode, backgroundColor
// per-species (index 0..2)
case speciesColor(Int), populationShare(Int), sensorAngle(Int), sensorDistance(Int)
case turnSpeed(Int), moveSpeed(Int), depositAmount(Int)
// matrix cell (row, col)
case matrix(Int, Int)
// pheromone
case decayRate, diffusionFreq, fadeStrength, trailSaturation
// dynamics
case chaosInterval, chaosStrength, rhythmAmplitude
// effects
case headIntensity, kaleidoscope, feedbackAmount, bloomThreshold, bloomIntensity
case vignette, saturation, contrast
}
/// A scalar, slider-editable parameter. Non-scalar params (enums/colors) are not
/// in the registry — they get bespoke controls but share ParamID identity.
struct ParamDescriptor: Identifiable {
let id: ParamID
let label: String
let section: EditorSection
let range: ClosedRange<Double>
let isInt: Bool
let isRespawn: Bool
let get: (PresetParams) -> Double
let set: (inout PresetParams, Double) -> Void
let tooltip: String
var idKey: String { "\(id)" }
}
enum ParamRegistry {
/// All scalar editable params with web-schema ranges (src/presets/schema.ts).
static let all: [ParamDescriptor] = {
var d: [ParamDescriptor] = []
func add(_ id: ParamID, _ label: String, _ s: EditorSection, _ r: ClosedRange<Double>,
int: Bool = false, respawn: Bool = false,
_ get: @escaping (PresetParams) -> Double,
_ set: @escaping (inout PresetParams, Double) -> Void) {
d.append(ParamDescriptor(id: id, label: label, section: s, range: r,
isInt: int, isRespawn: respawn, get: get, set: set,
tooltip: ParamHelp.text(for: label)))
}
// Global
add(.agentCount, "Agents", .global, 100...500_000, int: true, respawn: true,
{ Double($0.agentCount) }, { $0.agentCount = Int($1.rounded()) })
add(.exposure, "Exposure", .global, 0.1...10, { Double($0.exposure) }, { $0.exposure = Float($1) })
add(.simSpeed, "Sim speed", .global, 1...8, int: true,
{ Double($0.simSpeed) }, { $0.simSpeed = Int($1.rounded()) })
// Per-species (i = 0..2)
for i in 0..<3 {
add(.populationShare(i), "Population", .species, 0...3, respawn: true,
{ Double($0.species[i].populationShare) }, { $0.species[i].populationShare = Float($1) })
add(.sensorAngle(i), "Sensor angle", .species, 0...Double.pi,
{ Double($0.species[i].sensorAngle) }, { $0.species[i].sensorAngle = Float($1) })
add(.sensorDistance(i), "Sensor distance", .species, 1...80,
{ Double($0.species[i].sensorDistance) }, { $0.species[i].sensorDistance = Float($1) })
add(.turnSpeed(i), "Turn speed", .species, 0...2,
{ Double($0.species[i].turnSpeed) }, { $0.species[i].turnSpeed = Float($1) })
add(.moveSpeed(i), "Move speed", .species, 0...10,
{ Double($0.species[i].moveSpeed) }, { $0.species[i].moveSpeed = Float($1) })
add(.depositAmount(i), "Deposit", .species, 0...100,
{ Double($0.species[i].depositAmount) }, { $0.species[i].depositAmount = Float($1) })
}
// Matrix (r,c)
for r in 0..<3 {
for c in 0..<3 {
add(.matrix(r, c), "M[\(r),\(c)]", .matrix, -2...2,
{ Double($0.interactionMatrix[r][c]) }, { $0.interactionMatrix[r][c] = Float($1) })
}
}
// Pheromone
add(.decayRate, "Decay", .pheromone, 0.5...1, { Double($0.decayRate) }, { $0.decayRate = Float($1) })
add(.diffusionFreq, "Diffusion", .pheromone, 1...20, int: true,
{ Double($0.diffusionFreq) }, { $0.diffusionFreq = Int($1.rounded()) })
add(.fadeStrength, "Fade", .pheromone, 0...1, { Double($0.fadeStrength) }, { $0.fadeStrength = Float($1) })
add(.trailSaturation, "Trail cap", .pheromone, 1...500,
{ Double($0.trailSaturation) }, { $0.trailSaturation = Float($1) })
// Dynamics
add(.chaosInterval, "Chaos interval", .dynamics, 0...2000, int: true,
{ Double($0.chaosInterval) }, { $0.chaosInterval = Int($1.rounded()) })
add(.chaosStrength, "Chaos strength", .dynamics, 0...2,
{ Double($0.chaosStrength) }, { $0.chaosStrength = Float($1) })
add(.rhythmAmplitude, "Rhythm", .dynamics, 0...0.5,
{ Double($0.rhythmAmplitude) }, { $0.rhythmAmplitude = Float($1) })
// Effects
add(.headIntensity, "Head glow", .effects, 0...3, { Double($0.headIntensity) }, { $0.headIntensity = Float($1) })
add(.kaleidoscope, "Kaleidoscope", .effects, 0...6, int: true,
{ Double($0.kaleidoscope) }, { $0.kaleidoscope = Int($1.rounded()) })
add(.feedbackAmount, "Feedback", .effects, 0...0.97, { Double($0.feedbackAmount) }, { $0.feedbackAmount = Float($1) })
add(.bloomThreshold, "Bloom threshold", .effects, 0...2, { Double($0.bloomThreshold) }, { $0.bloomThreshold = Float($1) })
add(.bloomIntensity, "Bloom", .effects, 0...3, { Double($0.bloomIntensity) }, { $0.bloomIntensity = Float($1) })
add(.vignette, "Vignette", .effects, 0...1, { Double($0.vignette) }, { $0.vignette = Float($1) })
add(.saturation, "Saturation", .effects, 0...2, { Double($0.saturation) }, { $0.saturation = Float($1) })
add(.contrast, "Contrast", .effects, 0...2, { Double($0.contrast) }, { $0.contrast = Float($1) })
return d
}()
static func descriptors(in section: EditorSection) -> [ParamDescriptor] {
all.filter { $0.section == section }
}
static let respawnIDs: Set<String> = Set(all.filter { $0.isRespawn }.map { "\($0.id)" })
}