#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ParamKind {
Numeric,
Enum,
Toggle,
Action,
CliReadonly,
Display,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ParamId {
SensorAngle,
SensorDistance,
TurnAngle,
StepSize,
Decay,
Deposit,
TimeScale,
DiffusionKernel,
DiffusionSigma,
Wind,
TerrainType,
TerrainStrength,
Attractor,
MouseMode,
MouseTimeout,
Theme,
Palette,
Charset,
ColorAa,
PaletteShift,
Invert,
Reverse,
StatusLine,
WindowFrame,
Chrome,
Dither,
IntensityMapping,
AutoNormalize,
MotionBlur,
Brightness,
TrailAge,
TrailDelta,
EdgeGlow,
FastMode,
Population,
SaveFrame,
Reset,
Randomize,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ParamDesc {
pub id: ParamId,
pub key_hint: &'static str,
pub label: &'static str,
pub kind: ParamKind,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct RegistryCtx {
pub diffusion_gaussian: bool,
pub mouse_enabled: bool,
}
pub const CATEGORY_NAMES: [&str; 6] = ["SIM", "ENV", "APP", "PST", "PRF", "SYS"];
pub fn visible_params(category: usize, ctx: &RegistryCtx) -> Vec<ParamDesc> {
match category {
0 => vec![
ParamDesc {
id: ParamId::SensorAngle,
key_hint: "A/a",
label: "Sensor Angle",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::SensorDistance,
key_hint: "J/j",
label: "Sensor Dist",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::TurnAngle,
key_hint: "T/t",
label: "Turn Angle",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::StepSize,
key_hint: "S/s",
label: "Step Size",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::Decay,
key_hint: "E/e",
label: "Decay Factor",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::Deposit,
key_hint: "I/i",
label: "Deposit Amt",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::TimeScale,
key_hint: "+/-",
label: "Time Scale",
kind: ParamKind::Numeric,
},
],
1 => {
let mut v = vec![ParamDesc {
id: ParamId::DiffusionKernel,
key_hint: "K",
label: "Diffusion",
kind: ParamKind::Enum,
}];
if ctx.diffusion_gaussian {
v.push(ParamDesc {
id: ParamId::DiffusionSigma,
key_hint: ";/:",
label: "Diff Sigma",
kind: ParamKind::Numeric,
});
}
v.push(ParamDesc {
id: ParamId::Wind,
key_hint: "W",
label: "Wind",
kind: ParamKind::Enum,
});
v.push(ParamDesc {
id: ParamId::TerrainType,
key_hint: "U",
label: "Terrain Type",
kind: ParamKind::Enum,
});
v.push(ParamDesc {
id: ParamId::TerrainStrength,
key_hint: "Y/y",
label: "Terrain Str",
kind: ParamKind::Numeric,
});
v.push(ParamDesc {
id: ParamId::Attractor,
key_hint: "L/l",
label: "Attractor",
kind: ParamKind::Numeric,
});
v.push(ParamDesc {
id: ParamId::MouseMode,
key_hint: ",",
label: "Mouse Mode",
kind: ParamKind::Enum,
});
if ctx.mouse_enabled {
v.push(ParamDesc {
id: ParamId::MouseTimeout,
key_hint: "─",
label: "Mouse Timeout",
kind: ParamKind::Display,
});
}
v
}
2 => vec![
ParamDesc {
id: ParamId::Theme,
key_hint: "9/*",
label: "Theme",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::Palette,
key_hint: "c/C",
label: "Palette",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::Charset,
key_hint: "`/~",
label: "Charset",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::ColorAa,
key_hint: "\"",
label: "Color AA",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::PaletteShift,
key_hint: "O",
label: "Palette Shift",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::Invert,
key_hint: "X",
label: "Invert",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::Reverse,
key_hint: "Z",
label: "Reverse",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::StatusLine,
key_hint: "←→",
label: "Status Line",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::WindowFrame,
key_hint: ")/(",
label: "Frame",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::Chrome,
key_hint: "F10",
label: "Chrome",
kind: ParamKind::Enum,
},
],
3 => vec![
ParamDesc {
id: ParamId::IntensityMapping,
key_hint: "m/M",
label: "Intensity",
kind: ParamKind::Enum,
},
ParamDesc {
id: ParamId::Dither,
key_hint: "d/D",
label: "Dither (dev)",
kind: ParamKind::Display,
},
ParamDesc {
id: ParamId::AutoNormalize,
key_hint: "B",
label: "Auto Normalize",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::MotionBlur,
key_hint: "V",
label: "Motion Blur",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::Brightness,
key_hint: "N/n",
label: "Brightness",
kind: ParamKind::Numeric,
},
ParamDesc {
id: ParamId::TrailAge,
key_hint: "'",
label: "Trail Age",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::TrailDelta,
key_hint: ".",
label: "Trail Delta",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::EdgeGlow,
key_hint: ">",
label: "Edge Glow",
kind: ParamKind::Toggle,
},
],
4 => vec![
ParamDesc {
id: ParamId::FastMode,
key_hint: "F",
label: "Fast Mode",
kind: ParamKind::Toggle,
},
ParamDesc {
id: ParamId::Population,
key_hint: "─",
label: "Population",
kind: ParamKind::CliReadonly,
},
],
5 => vec![
ParamDesc {
id: ParamId::SaveFrame,
key_hint: "G",
label: "Save Frame",
kind: ParamKind::Action,
},
ParamDesc {
id: ParamId::Reset,
key_hint: "0",
label: "Reset",
kind: ParamKind::Action,
},
ParamDesc {
id: ParamId::Randomize,
key_hint: "8",
label: "Randomize",
kind: ParamKind::Action,
},
],
_ => vec![],
}
}
pub fn action_for(id: ParamId, sign: f32) -> Option<crate::terminal::state::ControlAction> {
use crate::terminal::state::ControlAction as A;
let reverse = sign < 0.0;
Some(match id {
ParamId::SensorAngle => A::AdjustSensorAngle(1.0 * sign),
ParamId::SensorDistance => A::AdjustSensorDistance(1.0 * sign),
ParamId::TurnAngle => A::AdjustTurnAngle(1.0 * sign),
ParamId::StepSize => A::AdjustStepSize(0.1 * sign),
ParamId::Decay => A::AdjustDecay(0.01 * sign),
ParamId::Deposit => A::AdjustDeposit(1.0 * sign),
ParamId::TimeScale => A::AdjustTimeScale(0.5 * sign),
ParamId::DiffusionSigma => A::AdjustDiffusionSigma(0.1 * sign),
ParamId::TerrainStrength => A::AdjustTerrainStrength(0.5 * sign),
ParamId::Attractor => A::AdjustAttractorStrength(0.5 * sign),
ParamId::Brightness => A::AdjustMaxBrightness(-5.0 * sign),
ParamId::MotionBlur => A::CycleMotionBlur,
ParamId::DiffusionKernel => A::CycleDiffusionKernel,
ParamId::Wind => {
if reverse {
A::CycleWindDirectionReverse
} else {
A::CycleWindDirection
}
}
ParamId::TerrainType => A::CycleTerrainType,
ParamId::MouseMode => A::CycleMouseMode,
ParamId::Theme => {
if reverse {
A::CycleThemeReverse
} else {
A::CycleTheme
}
}
ParamId::Palette => {
if reverse {
A::CyclePaletteReverse
} else {
A::CyclePalette
}
}
ParamId::Charset => {
if reverse {
A::CycleCharsetReverse
} else {
A::CycleCharset
}
}
ParamId::ColorAa => A::CycleColorAa,
ParamId::PaletteShift => A::CyclePaletteShiftSpeed,
ParamId::IntensityMapping => {
if reverse {
A::CycleIntensityMappingReverse
} else {
A::CycleIntensityMapping
}
}
ParamId::WindowFrame => {
if reverse {
A::CycleWindowFrameReverse
} else {
A::CycleWindowFrame
}
}
ParamId::Chrome => A::CycleChrome,
ParamId::Invert => A::ToggleInvertPalette,
ParamId::Reverse => A::ToggleReversePalette,
ParamId::StatusLine => A::ToggleStatusBar,
ParamId::AutoNormalize => A::ToggleAutoNormalize,
ParamId::TrailAge => A::ToggleTrailAge,
ParamId::TrailDelta => A::ToggleTrailDelta,
ParamId::EdgeGlow => A::ToggleGradientMagnitude,
ParamId::FastMode => A::ToggleFastMode,
ParamId::MouseTimeout | ParamId::Population | ParamId::Dither => return None,
ParamId::SaveFrame | ParamId::Reset | ParamId::Randomize => return None,
})
}
pub fn activate_action_for(id: ParamId) -> Option<crate::terminal::state::ControlAction> {
use crate::terminal::state::ControlAction as A;
Some(match id {
ParamId::SaveFrame => A::SaveFrameToPng,
ParamId::Reset => A::ResetToDefaults,
ParamId::Randomize => A::RandomizeParams,
_ => return None,
})
}
pub fn param_id_for_action(action: &crate::terminal::state::ControlAction) -> Option<ParamId> {
use crate::terminal::state::ControlAction as A;
Some(match action {
A::AdjustSensorAngle(_) => ParamId::SensorAngle,
A::AdjustSensorDistance(_) => ParamId::SensorDistance,
A::AdjustTurnAngle(_) => ParamId::TurnAngle,
A::AdjustStepSize(_) => ParamId::StepSize,
A::AdjustDecay(_) => ParamId::Decay,
A::AdjustDeposit(_) => ParamId::Deposit,
A::AdjustTimeScale(_) => ParamId::TimeScale,
A::CycleDiffusionKernel => ParamId::DiffusionKernel,
A::AdjustDiffusionSigma(_) => ParamId::DiffusionSigma,
A::CycleWindDirection | A::CycleWindDirectionReverse => ParamId::Wind,
A::CycleTerrainType => ParamId::TerrainType,
A::AdjustTerrainStrength(_) => ParamId::TerrainStrength,
A::AdjustAttractorStrength(_) => ParamId::Attractor,
A::CycleMouseMode => ParamId::MouseMode,
A::CycleTheme | A::CycleThemeReverse => ParamId::Theme,
A::CyclePalette | A::CyclePaletteReverse => ParamId::Palette,
A::CycleCharset | A::CycleCharsetReverse => ParamId::Charset,
A::CycleColorAa => ParamId::ColorAa,
A::CyclePaletteShiftSpeed => ParamId::PaletteShift,
A::CycleIntensityMapping | A::CycleIntensityMappingReverse => ParamId::IntensityMapping,
A::CycleWindowFrame | A::CycleWindowFrameReverse => ParamId::WindowFrame,
A::CycleChrome => ParamId::Chrome,
A::ToggleInvertPalette => ParamId::Invert,
A::ToggleReversePalette => ParamId::Reverse,
A::ToggleStatusBar => ParamId::StatusLine,
A::ToggleAutoNormalize => ParamId::AutoNormalize,
A::CycleMotionBlur => ParamId::MotionBlur,
A::AdjustMaxBrightness(_) => ParamId::Brightness,
A::ToggleTrailAge => ParamId::TrailAge,
A::ToggleTrailDelta => ParamId::TrailDelta,
A::ToggleGradientMagnitude => ParamId::EdgeGlow,
A::ToggleFastMode => ParamId::FastMode,
A::SaveFrameToPng => ParamId::SaveFrame,
A::ResetToDefaults => ParamId::Reset,
A::RandomizeParams => ParamId::Randomize,
_ => return None,
})
}
pub fn locate(id: ParamId, ctx: &RegistryCtx) -> Option<(usize, usize)> {
for cat in 0..CATEGORY_NAMES.len() {
let params = visible_params(cat, ctx);
if let Some(idx) = params.iter().position(|p| p.id == id) {
return Some((cat, idx));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptor_carries_kind_and_label() {
let d = ParamDesc {
id: ParamId::SensorAngle,
key_hint: "A/a",
label: "Sensor Angle",
kind: ParamKind::Numeric,
};
assert_eq!(d.kind, ParamKind::Numeric);
assert_eq!(d.label, "Sensor Angle");
}
#[test]
fn mouse_timeout_is_display_kind() {
let d = ParamDesc {
id: ParamId::MouseTimeout,
key_hint: "─",
label: "Mouse Timeout",
kind: ParamKind::Display,
};
assert_eq!(d.kind, ParamKind::Display);
}
#[test]
fn sim_category_has_seven_params() {
let ctx = RegistryCtx {
diffusion_gaussian: false,
mouse_enabled: false,
};
let v = visible_params(0, &ctx);
assert_eq!(v.len(), 7);
assert_eq!(v[0].id, ParamId::SensorAngle);
assert!(v.iter().all(|p| matches!(p.kind, ParamKind::Numeric)));
}
#[test]
fn env_diff_sigma_only_when_gaussian() {
let off = RegistryCtx {
diffusion_gaussian: false,
mouse_enabled: false,
};
let on = RegistryCtx {
diffusion_gaussian: true,
mouse_enabled: false,
};
assert!(!visible_params(1, &off)
.iter()
.any(|p| p.id == ParamId::DiffusionSigma));
assert!(visible_params(1, &on)
.iter()
.any(|p| p.id == ParamId::DiffusionSigma));
}
#[test]
fn env_mouse_timeout_only_when_mouse_enabled() {
let off = RegistryCtx {
diffusion_gaussian: false,
mouse_enabled: false,
};
let on = RegistryCtx {
diffusion_gaussian: false,
mouse_enabled: true,
};
assert!(!visible_params(1, &off)
.iter()
.any(|p| p.id == ParamId::MouseTimeout));
assert!(visible_params(1, &on)
.iter()
.any(|p| p.id == ParamId::MouseTimeout));
}
#[test]
fn locate_returns_category_and_index() {
let ctx = RegistryCtx {
diffusion_gaussian: false,
mouse_enabled: false,
};
assert_eq!(locate(ParamId::StepSize, &ctx), Some((0, 3)));
assert_eq!(locate(ParamId::DiffusionSigma, &ctx), None);
}
#[test]
fn action_for_numeric_uses_sign() {
use crate::terminal::state::ControlAction;
assert_eq!(
action_for(ParamId::SensorAngle, -1.0),
Some(ControlAction::AdjustSensorAngle(-1.0))
);
assert_eq!(
action_for(ParamId::SensorAngle, 1.0),
Some(ControlAction::AdjustSensorAngle(1.0))
);
assert_eq!(action_for(ParamId::MouseTimeout, 1.0), None);
assert_eq!(action_for(ParamId::Population, 1.0), None);
assert_eq!(action_for(ParamId::Reset, 1.0), None);
}
#[test]
fn action_for_enum_uses_reverse_for_negative_sign() {
use crate::terminal::state::ControlAction;
assert_eq!(
action_for(ParamId::Palette, 1.0),
Some(ControlAction::CyclePalette)
);
assert_eq!(
action_for(ParamId::Palette, -1.0),
Some(ControlAction::CyclePaletteReverse)
);
assert_eq!(
action_for(ParamId::DiffusionKernel, 1.0),
Some(ControlAction::CycleDiffusionKernel)
);
assert_eq!(
action_for(ParamId::DiffusionKernel, -1.0),
Some(ControlAction::CycleDiffusionKernel)
);
}
#[test]
fn action_for_brightness_forward_brightens() {
use crate::terminal::state::ControlAction;
assert_eq!(
action_for(ParamId::Brightness, 1.0),
Some(ControlAction::AdjustMaxBrightness(-5.0))
);
assert_eq!(
action_for(ParamId::Brightness, -1.0),
Some(ControlAction::AdjustMaxBrightness(5.0))
);
}
#[test]
fn activate_action_for_maps_action_rows() {
use crate::terminal::state::ControlAction;
assert_eq!(
activate_action_for(ParamId::SaveFrame),
Some(ControlAction::SaveFrameToPng)
);
assert_eq!(
activate_action_for(ParamId::Reset),
Some(ControlAction::ResetToDefaults)
);
assert_eq!(
activate_action_for(ParamId::Randomize),
Some(ControlAction::RandomizeParams)
);
assert_eq!(activate_action_for(ParamId::SensorAngle), None);
}
#[test]
fn orphan_enum_params_are_now_tunable() {
use crate::terminal::state::ControlAction as A;
assert_eq!(
action_for(ParamId::IntensityMapping, 1.0),
Some(A::CycleIntensityMapping)
);
assert_eq!(
action_for(ParamId::IntensityMapping, -1.0),
Some(A::CycleIntensityMappingReverse)
);
assert_eq!(
action_for(ParamId::WindowFrame, 1.0),
Some(A::CycleWindowFrame)
);
assert_eq!(
action_for(ParamId::WindowFrame, -1.0),
Some(A::CycleWindowFrameReverse)
);
assert_eq!(action_for(ParamId::Chrome, 1.0), Some(A::CycleChrome));
assert_eq!(action_for(ParamId::Chrome, -1.0), Some(A::CycleChrome));
assert_eq!(
param_id_for_action(&A::CycleIntensityMapping),
Some(ParamId::IntensityMapping)
);
assert_eq!(
param_id_for_action(&A::CycleWindowFrame),
Some(ParamId::WindowFrame)
);
assert_eq!(param_id_for_action(&A::CycleChrome), Some(ParamId::Chrome));
}
#[test]
fn locate_diffusion_sigma_when_gaussian_true() {
let ctx_gaussian_true = RegistryCtx {
diffusion_gaussian: true,
mouse_enabled: false,
};
assert_eq!(
locate(ParamId::DiffusionSigma, &ctx_gaussian_true),
Some((1, 1))
);
}
}