use std::collections::HashMap;
use super::color::Color;
use super::state::ChannelState;
use super::types::{BlendMode, EffectLayer};
#[inline]
fn layer_suffix(layer: EffectLayer) -> &'static str {
match layer {
EffectLayer::Background => "_bg",
EffectLayer::Midground => "_mid",
EffectLayer::Foreground => "_fg",
}
}
#[inline]
pub fn multiplier_key(prefix: &str, layer: EffectLayer) -> String {
format!("_{}_mult{}", prefix, layer_suffix(layer))
}
fn insert_rgb(
result: &mut HashMap<String, ChannelState>,
value: f64,
layer: EffectLayer,
blend_mode: BlendMode,
) {
let state = ChannelState::new(value, layer, blend_mode);
result.insert("red".to_string(), state);
result.insert("green".to_string(), state);
result.insert("blue".to_string(), state);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FixtureCapabilities(u32);
impl FixtureCapabilities {
pub const NONE: FixtureCapabilities = FixtureCapabilities(0);
pub const RGB_COLOR: FixtureCapabilities = FixtureCapabilities(1 << 0);
pub const WHITE_COLOR: FixtureCapabilities = FixtureCapabilities(1 << 1);
pub const DIMMING: FixtureCapabilities = FixtureCapabilities(1 << 2);
pub const STROBING: FixtureCapabilities = FixtureCapabilities(1 << 3);
pub const PANNING: FixtureCapabilities = FixtureCapabilities(1 << 4);
pub const TILTING: FixtureCapabilities = FixtureCapabilities(1 << 5);
pub const ZOOMING: FixtureCapabilities = FixtureCapabilities(1 << 6);
pub const FOCUSING: FixtureCapabilities = FixtureCapabilities(1 << 7);
pub const GOBO: FixtureCapabilities = FixtureCapabilities(1 << 8);
pub const COLOR_TEMPERATURE: FixtureCapabilities = FixtureCapabilities(1 << 9);
pub const EFFECTS: FixtureCapabilities = FixtureCapabilities(1 << 10);
#[inline]
pub fn contains(&self, capability: FixtureCapabilities) -> bool {
(self.0 & capability.0) != 0
}
#[inline]
pub fn with(&self, capability: FixtureCapabilities) -> FixtureCapabilities {
FixtureCapabilities(self.0 | capability.0)
}
#[cfg(test)]
#[inline]
pub fn count(&self) -> u32 {
self.0.count_ones()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BrightnessStrategy {
DedicatedDimmer,
RgbMultiplication,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorStrategy {
Rgb,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StrobeStrategy {
DedicatedChannel,
RgbStrobing,
BrightnessStrobing,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PulseStrategy {
DedicatedDimmer,
RgbMultiplication,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ChaseStrategy {
DedicatedDimmer,
RgbChannels,
BrightnessControl,
}
#[derive(Debug, Clone)]
pub struct FixtureProfile {
pub brightness_strategy: BrightnessStrategy,
pub color_strategy: ColorStrategy,
pub strobe_strategy: StrobeStrategy,
pub pulse_strategy: PulseStrategy,
pub chase_strategy: ChaseStrategy,
}
impl FixtureProfile {
pub fn for_fixture(fixture: &FixtureInfo) -> &FixtureProfile {
fixture.profile()
}
pub(super) fn from_capabilities(capabilities: &FixtureCapabilities) -> Self {
let brightness_strategy = Self::determine_brightness_strategy(capabilities);
let color_strategy = Self::determine_color_strategy(capabilities);
let strobe_strategy = Self::determine_strobe_strategy(capabilities);
let pulse_strategy = Self::determine_pulse_strategy(capabilities);
let chase_strategy = Self::determine_chase_strategy(capabilities);
FixtureProfile {
brightness_strategy,
color_strategy,
strobe_strategy,
pulse_strategy,
chase_strategy,
}
}
fn determine_brightness_strategy(capabilities: &FixtureCapabilities) -> BrightnessStrategy {
if capabilities.contains(FixtureCapabilities::DIMMING) {
BrightnessStrategy::DedicatedDimmer
} else {
BrightnessStrategy::RgbMultiplication
}
}
fn determine_color_strategy(_capabilities: &FixtureCapabilities) -> ColorStrategy {
ColorStrategy::Rgb
}
fn determine_strobe_strategy(capabilities: &FixtureCapabilities) -> StrobeStrategy {
if capabilities.contains(FixtureCapabilities::STROBING) {
StrobeStrategy::DedicatedChannel
} else if capabilities.contains(FixtureCapabilities::DIMMING) {
StrobeStrategy::BrightnessStrobing
} else if capabilities.contains(FixtureCapabilities::RGB_COLOR) {
StrobeStrategy::RgbStrobing
} else {
StrobeStrategy::BrightnessStrobing
}
}
fn determine_pulse_strategy(capabilities: &FixtureCapabilities) -> PulseStrategy {
if capabilities.contains(FixtureCapabilities::DIMMING) {
PulseStrategy::DedicatedDimmer
} else {
PulseStrategy::RgbMultiplication
}
}
fn determine_chase_strategy(capabilities: &FixtureCapabilities) -> ChaseStrategy {
if capabilities.contains(FixtureCapabilities::DIMMING) {
ChaseStrategy::DedicatedDimmer
} else if capabilities.contains(FixtureCapabilities::RGB_COLOR) {
ChaseStrategy::RgbChannels
} else {
ChaseStrategy::BrightnessControl
}
}
fn apply_dimmer_or_multiplier(
strategy_uses_dimmer: bool,
multiplier_prefix: &str,
value: f64,
layer: EffectLayer,
blend_mode: BlendMode,
) -> HashMap<String, ChannelState> {
let mut result = HashMap::new();
if strategy_uses_dimmer {
result.insert(
"dimmer".to_string(),
ChannelState::new(value, layer, blend_mode),
);
} else {
result.insert(
multiplier_key(multiplier_prefix, layer),
ChannelState::new(value, layer, BlendMode::Multiply),
);
}
result
}
pub fn apply_brightness(
&self,
level: f64,
layer: EffectLayer,
blend_mode: BlendMode,
) -> HashMap<String, ChannelState> {
Self::apply_dimmer_or_multiplier(
self.brightness_strategy == BrightnessStrategy::DedicatedDimmer,
"dimmer",
level,
layer,
blend_mode,
)
}
pub fn apply_color(
&self,
color: Color,
layer: EffectLayer,
blend_mode: BlendMode,
) -> HashMap<String, ChannelState> {
let mut result = HashMap::new();
match self.color_strategy {
ColorStrategy::Rgb => {
let normalize = |v: u8| v as f64 / 255.0;
result.insert(
"red".to_string(),
ChannelState::new(normalize(color.r), layer, blend_mode),
);
result.insert(
"green".to_string(),
ChannelState::new(normalize(color.g), layer, blend_mode),
);
result.insert(
"blue".to_string(),
ChannelState::new(normalize(color.b), layer, blend_mode),
);
if let Some(w) = color.w {
result.insert(
"white".to_string(),
ChannelState::new(normalize(w), layer, blend_mode),
);
}
}
}
result
}
pub fn apply_strobe(
&self,
frequency: f64,
layer: EffectLayer,
blend_mode: BlendMode,
crossfade_multiplier: f64,
strobe_value: Option<f64>, ) -> HashMap<String, ChannelState> {
let mut result = HashMap::new();
if crossfade_multiplier <= 0.0 {
return result;
}
match self.strobe_strategy {
StrobeStrategy::DedicatedChannel => {
result.insert(
"strobe".to_string(),
ChannelState::new(frequency, layer, blend_mode),
);
}
StrobeStrategy::RgbStrobing => {
if let Some(value) = strobe_value {
let effective_blend_mode = if value == 0.0 {
BlendMode::Replace
} else {
blend_mode
};
insert_rgb(&mut result, value, layer, effective_blend_mode);
}
}
StrobeStrategy::BrightnessStrobing => {
if let Some(value) = strobe_value {
let effective_blend_mode = if value == 0.0 {
BlendMode::Replace
} else {
blend_mode
};
let channel_state = ChannelState::new(value, layer, effective_blend_mode);
result.insert("dimmer".to_string(), channel_state);
}
}
}
result
}
pub fn apply_pulse(
&self,
pulse_value: f64,
layer: EffectLayer,
blend_mode: BlendMode,
) -> HashMap<String, ChannelState> {
Self::apply_dimmer_or_multiplier(
self.pulse_strategy == PulseStrategy::DedicatedDimmer,
"pulse",
pulse_value,
layer,
blend_mode,
)
}
pub fn apply_chase(
&self,
chase_value: f64,
layer: EffectLayer,
blend_mode: BlendMode,
) -> HashMap<String, ChannelState> {
let mut result = HashMap::new();
match self.chase_strategy {
ChaseStrategy::DedicatedDimmer | ChaseStrategy::BrightnessControl => {
result.insert(
"dimmer".to_string(),
ChannelState::new(chase_value, layer, blend_mode),
);
}
ChaseStrategy::RgbChannels => {
insert_rgb(&mut result, chase_value, layer, blend_mode);
}
}
result
}
}
#[derive(Debug, Clone)]
pub struct FixtureInfo {
pub name: String,
pub universe: u16,
pub address: u16,
pub fixture_type: String,
pub channels: HashMap<String, u16>,
pub max_strobe_frequency: Option<f64>,
pub min_strobe_frequency: Option<f64>,
pub strobe_dmx_offset: Option<u8>,
cached_capabilities: FixtureCapabilities,
cached_profile: FixtureProfile,
}
impl FixtureInfo {
pub fn new(
name: String,
universe: u16,
address: u16,
fixture_type: String,
channels: HashMap<String, u16>,
max_strobe_frequency: Option<f64>,
) -> Self {
let capabilities = Self::derive_capabilities(&channels);
let profile = FixtureProfile::from_capabilities(&capabilities);
Self {
name,
universe,
address,
fixture_type,
channels,
max_strobe_frequency,
min_strobe_frequency: None,
strobe_dmx_offset: None,
cached_capabilities: capabilities,
cached_profile: profile,
}
}
fn derive_capabilities(channels: &HashMap<String, u16>) -> FixtureCapabilities {
let mut capabilities = FixtureCapabilities::NONE;
if channels.contains_key("red")
&& channels.contains_key("green")
&& channels.contains_key("blue")
{
capabilities = capabilities.with(FixtureCapabilities::RGB_COLOR);
}
if channels.contains_key("white") {
capabilities = capabilities.with(FixtureCapabilities::WHITE_COLOR);
}
if channels.contains_key("dimmer") {
capabilities = capabilities.with(FixtureCapabilities::DIMMING);
}
if channels.contains_key("strobe") {
capabilities = capabilities.with(FixtureCapabilities::STROBING);
}
if channels.contains_key("pan") {
capabilities = capabilities.with(FixtureCapabilities::PANNING);
}
if channels.contains_key("tilt") {
capabilities = capabilities.with(FixtureCapabilities::TILTING);
}
if channels.contains_key("zoom") {
capabilities = capabilities.with(FixtureCapabilities::ZOOMING);
}
if channels.contains_key("focus") {
capabilities = capabilities.with(FixtureCapabilities::FOCUSING);
}
if channels.contains_key("gobo") {
capabilities = capabilities.with(FixtureCapabilities::GOBO);
}
if channels.contains_key("ct") || channels.contains_key("color_temp") {
capabilities = capabilities.with(FixtureCapabilities::COLOR_TEMPERATURE);
}
if channels.contains_key("effects")
|| channels.contains_key("prism")
|| channels.contains_key("frost")
{
capabilities = capabilities.with(FixtureCapabilities::EFFECTS);
}
capabilities
}
#[cfg(test)]
#[inline]
pub fn capabilities(&self) -> FixtureCapabilities {
self.cached_capabilities
}
#[inline]
pub fn has_capability(&self, capability: FixtureCapabilities) -> bool {
self.cached_capabilities.contains(capability)
}
#[inline]
pub fn profile(&self) -> &FixtureProfile {
&self.cached_profile
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_channels(names: &[&str]) -> HashMap<String, u16> {
names
.iter()
.enumerate()
.map(|(i, name)| (name.to_string(), i as u16 + 1))
.collect()
}
fn rgb_fixture() -> FixtureInfo {
FixtureInfo::new(
"par".to_string(),
1,
1,
"generic_par".to_string(),
make_channels(&["red", "green", "blue"]),
None,
)
}
fn rgb_dimmer_fixture() -> FixtureInfo {
FixtureInfo::new(
"par_d".to_string(),
1,
1,
"par_with_dimmer".to_string(),
make_channels(&["red", "green", "blue", "dimmer"]),
None,
)
}
fn full_fixture() -> FixtureInfo {
FixtureInfo::new(
"mover".to_string(),
1,
1,
"moving_head".to_string(),
make_channels(&[
"red", "green", "blue", "white", "dimmer", "strobe", "pan", "tilt",
]),
None,
)
}
#[test]
fn capabilities_none() {
assert_eq!(FixtureCapabilities::NONE.count(), 0);
}
#[test]
fn capabilities_single() {
let caps = FixtureCapabilities::NONE.with(FixtureCapabilities::RGB_COLOR);
assert!(caps.contains(FixtureCapabilities::RGB_COLOR));
assert!(!caps.contains(FixtureCapabilities::DIMMING));
assert_eq!(caps.count(), 1);
}
#[test]
fn capabilities_multiple() {
let caps = FixtureCapabilities::NONE
.with(FixtureCapabilities::RGB_COLOR)
.with(FixtureCapabilities::DIMMING)
.with(FixtureCapabilities::STROBING);
assert!(caps.contains(FixtureCapabilities::RGB_COLOR));
assert!(caps.contains(FixtureCapabilities::DIMMING));
assert!(caps.contains(FixtureCapabilities::STROBING));
assert!(!caps.contains(FixtureCapabilities::PANNING));
assert_eq!(caps.count(), 3);
}
#[test]
fn capabilities_idempotent() {
let caps = FixtureCapabilities::NONE
.with(FixtureCapabilities::RGB_COLOR)
.with(FixtureCapabilities::RGB_COLOR);
assert_eq!(caps.count(), 1);
}
#[test]
fn multiplier_key_background() {
assert_eq!(
multiplier_key("dimmer", EffectLayer::Background),
"_dimmer_mult_bg"
);
}
#[test]
fn multiplier_key_midground() {
assert_eq!(
multiplier_key("pulse", EffectLayer::Midground),
"_pulse_mult_mid"
);
}
#[test]
fn multiplier_key_foreground() {
assert_eq!(
multiplier_key("chase", EffectLayer::Foreground),
"_chase_mult_fg"
);
}
#[test]
fn derive_capabilities_rgb_only() {
let f = rgb_fixture();
assert!(f.has_capability(FixtureCapabilities::RGB_COLOR));
assert!(!f.has_capability(FixtureCapabilities::DIMMING));
assert!(!f.has_capability(FixtureCapabilities::STROBING));
}
#[test]
fn derive_capabilities_rgb_plus_dimmer() {
let f = rgb_dimmer_fixture();
assert!(f.has_capability(FixtureCapabilities::RGB_COLOR));
assert!(f.has_capability(FixtureCapabilities::DIMMING));
assert!(!f.has_capability(FixtureCapabilities::STROBING));
}
#[test]
fn derive_capabilities_full() {
let f = full_fixture();
assert!(f.has_capability(FixtureCapabilities::RGB_COLOR));
assert!(f.has_capability(FixtureCapabilities::WHITE_COLOR));
assert!(f.has_capability(FixtureCapabilities::DIMMING));
assert!(f.has_capability(FixtureCapabilities::STROBING));
assert!(f.has_capability(FixtureCapabilities::PANNING));
assert!(f.has_capability(FixtureCapabilities::TILTING));
}
#[test]
fn derive_capabilities_requires_all_rgb() {
let f = FixtureInfo::new(
"partial".to_string(),
1,
1,
"type".to_string(),
make_channels(&["red", "green"]),
None,
);
assert!(!f.has_capability(FixtureCapabilities::RGB_COLOR));
}
#[test]
fn derive_capabilities_color_temp() {
let f = FixtureInfo::new(
"ct_fixture".to_string(),
1,
1,
"type".to_string(),
make_channels(&["ct"]),
None,
);
assert!(f.has_capability(FixtureCapabilities::COLOR_TEMPERATURE));
}
#[test]
fn derive_capabilities_effects_channels() {
for channel in &["effects", "prism", "frost"] {
let f = FixtureInfo::new(
"fx".to_string(),
1,
1,
"type".to_string(),
make_channels(&[channel]),
None,
);
assert!(
f.has_capability(FixtureCapabilities::EFFECTS),
"failed for {}",
channel
);
}
}
#[test]
fn profile_rgb_only_strategies() {
let p = rgb_fixture().profile().clone();
assert_eq!(p.brightness_strategy, BrightnessStrategy::RgbMultiplication);
assert_eq!(p.strobe_strategy, StrobeStrategy::RgbStrobing);
assert_eq!(p.pulse_strategy, PulseStrategy::RgbMultiplication);
assert_eq!(p.chase_strategy, ChaseStrategy::RgbChannels);
}
#[test]
fn profile_rgb_dimmer_strategies() {
let p = rgb_dimmer_fixture().profile().clone();
assert_eq!(p.brightness_strategy, BrightnessStrategy::DedicatedDimmer);
assert_eq!(p.strobe_strategy, StrobeStrategy::BrightnessStrobing);
assert_eq!(p.pulse_strategy, PulseStrategy::DedicatedDimmer);
assert_eq!(p.chase_strategy, ChaseStrategy::DedicatedDimmer);
}
#[test]
fn profile_full_fixture_strategies() {
let p = full_fixture().profile().clone();
assert_eq!(p.brightness_strategy, BrightnessStrategy::DedicatedDimmer);
assert_eq!(p.strobe_strategy, StrobeStrategy::DedicatedChannel);
assert_eq!(p.pulse_strategy, PulseStrategy::DedicatedDimmer);
assert_eq!(p.chase_strategy, ChaseStrategy::DedicatedDimmer);
}
#[test]
fn profile_no_capabilities_fallbacks() {
let p = FixtureProfile::from_capabilities(&FixtureCapabilities::NONE);
assert_eq!(p.brightness_strategy, BrightnessStrategy::RgbMultiplication);
assert_eq!(p.strobe_strategy, StrobeStrategy::BrightnessStrobing);
assert_eq!(p.pulse_strategy, PulseStrategy::RgbMultiplication);
assert_eq!(p.chase_strategy, ChaseStrategy::BrightnessControl);
}
#[test]
fn apply_brightness_dedicated_dimmer() {
let p = rgb_dimmer_fixture().profile().clone();
let result = p.apply_brightness(0.75, EffectLayer::Background, BlendMode::Replace);
assert!(result.contains_key("dimmer"));
assert!(!result.contains_key("red"));
assert!((result["dimmer"].value - 0.75).abs() < f64::EPSILON);
}
#[test]
fn apply_brightness_rgb_multiplication() {
let p = rgb_fixture().profile().clone();
let result = p.apply_brightness(0.5, EffectLayer::Background, BlendMode::Replace);
assert!(!result.contains_key("dimmer"));
let key = multiplier_key("dimmer", EffectLayer::Background);
assert!(result.contains_key(&key));
}
#[test]
fn apply_color_rgb() {
let p = rgb_fixture().profile().clone();
let color = Color::new(255, 128, 0);
let result = p.apply_color(color, EffectLayer::Background, BlendMode::Replace);
assert!((result["red"].value - 1.0).abs() < f64::EPSILON);
assert!((result["green"].value - 128.0 / 255.0).abs() < 0.01);
assert!((result["blue"].value - 0.0).abs() < f64::EPSILON);
}
#[test]
fn apply_color_with_white_channel() {
let p = rgb_fixture().profile().clone();
let color = Color {
r: 255,
g: 255,
b: 255,
w: Some(200),
};
let result = p.apply_color(color, EffectLayer::Background, BlendMode::Replace);
assert!(result.contains_key("white"));
assert!((result["white"].value - 200.0 / 255.0).abs() < 0.01);
}
#[test]
fn apply_strobe_dedicated_channel() {
let p = full_fixture().profile().clone();
let result = p.apply_strobe(0.5, EffectLayer::Foreground, BlendMode::Replace, 1.0, None);
assert!(result.contains_key("strobe"));
}
#[test]
fn apply_strobe_zero_crossfade_returns_empty() {
let p = full_fixture().profile().clone();
let result = p.apply_strobe(0.5, EffectLayer::Foreground, BlendMode::Replace, 0.0, None);
assert!(result.is_empty());
}
#[test]
fn apply_strobe_rgb_strobing() {
let p = rgb_fixture().profile().clone();
let result = p.apply_strobe(
0.0,
EffectLayer::Foreground,
BlendMode::Replace,
1.0,
Some(1.0),
);
assert!(result.contains_key("red"));
assert!(result.contains_key("green"));
assert!(result.contains_key("blue"));
}
#[test]
fn apply_pulse_dedicated_dimmer() {
let p = rgb_dimmer_fixture().profile().clone();
let result = p.apply_pulse(0.8, EffectLayer::Midground, BlendMode::Multiply);
assert!(result.contains_key("dimmer"));
}
#[test]
fn apply_pulse_rgb_multiplication() {
let p = rgb_fixture().profile().clone();
let result = p.apply_pulse(0.8, EffectLayer::Midground, BlendMode::Multiply);
let key = multiplier_key("pulse", EffectLayer::Midground);
assert!(result.contains_key(&key));
}
#[test]
fn apply_chase_dedicated_dimmer() {
let p = rgb_dimmer_fixture().profile().clone();
let result = p.apply_chase(0.5, EffectLayer::Foreground, BlendMode::Replace);
assert!(result.contains_key("dimmer"));
}
#[test]
fn apply_chase_rgb_channels() {
let p = rgb_fixture().profile().clone();
let result = p.apply_chase(0.5, EffectLayer::Foreground, BlendMode::Replace);
assert!(result.contains_key("red"));
assert!(result.contains_key("green"));
assert!(result.contains_key("blue"));
}
#[test]
fn fixture_info_basic_accessors() {
let f = FixtureInfo::new(
"spot1".to_string(),
2,
10,
"generic_spot".to_string(),
make_channels(&["red", "green", "blue", "dimmer"]),
Some(25.0),
);
assert_eq!(f.name, "spot1");
assert_eq!(f.universe, 2);
assert_eq!(f.address, 10);
assert_eq!(f.fixture_type, "generic_spot");
assert_eq!(f.max_strobe_frequency, Some(25.0));
assert_eq!(f.channels.len(), 4);
}
}