use super::common::*;
#[cfg(test)]
use crate::lighting::effects::*;
use crate::lighting::engine::EffectEngine;
use std::collections::HashMap;
use std::time::Duration;
#[test]
fn test_dimmer_multiplier_passes_through_locks_rgb_only() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"front_wash".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let mut static_blue = EffectInstance::new(
"static_blue".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("red".to_string(), 0.0);
p.insert("green".to_string(), 0.0);
p.insert("blue".to_string(), 1.0);
p
},
duration: Duration::from_secs(5),
},
vec!["front_wash".to_string()],
None,
None,
None,
);
static_blue.layer = EffectLayer::Background;
static_blue.blend_mode = BlendMode::Replace;
engine.start_effect(static_blue).unwrap();
engine.update(Duration::from_millis(100), None).unwrap();
let mut fade_out = EffectInstance::new(
"fade_out".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.0,
duration: Duration::from_secs(2),
curve: DimmerCurve::Linear,
},
vec!["front_wash".to_string()],
None,
None,
None,
);
fade_out.layer = EffectLayer::Foreground;
fade_out.blend_mode = BlendMode::Multiply;
engine.start_effect(fade_out).unwrap();
let cmds_1s = engine.update(Duration::from_secs(1), None).unwrap();
let blue_1s = cmds_1s
.iter()
.find(|c| c.universe == 1 && c.channel == 3)
.map(|c| c.value)
.unwrap_or(0);
assert!(
blue_1s > 100 && blue_1s < 155,
"blue should be mid-fade (~50%) at 1s, got {}",
blue_1s
);
let cmds_15s = engine.update(Duration::from_millis(500), None).unwrap();
let blue_15s = cmds_15s
.iter()
.find(|c| c.universe == 1 && c.channel == 3)
.map(|c| c.value)
.unwrap_or(0);
assert!(
blue_15s > 50 && blue_15s < 75,
"blue should be around 25% (faded 75% to black) at 1.5s, got {}",
blue_15s
);
let cmds_after = engine.update(Duration::from_millis(500), None).unwrap();
let blue_after = cmds_after
.iter()
.find(|c| c.universe == 1 && c.channel == 3)
.map(|c| c.value)
.unwrap_or(0);
assert_eq!(
blue_after, 255,
"blue should return to full after dimmer completes (dimmer no longer persists)"
);
}
#[test]
fn test_dedicated_dimmer_preserves_rgb() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
"front_wash".to_string(),
1,
1,
"RGB_Par_Dimmer".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let mut static_blue = EffectInstance::new(
"static_blue".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("red".to_string(), 0.0);
p.insert("green".to_string(), 0.0);
p.insert("blue".to_string(), 1.0);
p.insert("dimmer".to_string(), 1.0);
p
},
duration: Duration::from_secs(5),
},
vec!["front_wash".to_string()],
None,
None,
None,
);
static_blue.layer = EffectLayer::Background;
static_blue.blend_mode = BlendMode::Replace;
engine.start_effect(static_blue).unwrap();
engine.update(Duration::from_millis(50), None).unwrap();
let mut fade_out = EffectInstance::new(
"fade_out".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.0,
duration: Duration::from_secs(2), curve: DimmerCurve::Linear,
},
vec!["front_wash".to_string()],
None,
None,
None,
);
fade_out.layer = EffectLayer::Foreground;
fade_out.blend_mode = BlendMode::Replace;
engine.start_effect(fade_out).unwrap();
let cmds_1s = engine.update(Duration::from_secs(1), None).unwrap();
let dimmer_1s = cmds_1s
.iter()
.find(|c| c.universe == 1 && c.channel == 1)
.map(|c| c.value)
.unwrap_or(0);
let red_1s = cmds_1s
.iter()
.find(|c| c.universe == 1 && c.channel == 2)
.map(|c| c.value)
.unwrap_or(0);
let green_1s = cmds_1s
.iter()
.find(|c| c.universe == 1 && c.channel == 3)
.map(|c| c.value)
.unwrap_or(0);
let blue_1s = cmds_1s
.iter()
.find(|c| c.universe == 1 && c.channel == 4)
.map(|c| c.value)
.unwrap_or(0);
assert!(
dimmer_1s > 100 && dimmer_1s < 155,
"dimmer should be mid-fade at 1s"
);
assert_eq!(red_1s, 0, "red should remain 0 at 1s");
assert_eq!(green_1s, 0, "green should remain 0 at 1s");
assert_eq!(blue_1s, 255, "blue should remain 255 at 1s");
}
#[test]
fn test_effect_layering_static_blue_and_dimmer() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture.clone());
let mut blue_params = HashMap::new();
blue_params.insert("red".to_string(), 0.0);
blue_params.insert("green".to_string(), 0.0);
blue_params.insert("blue".to_string(), 1.0);
let blue_effect = create_effect_with_layering(
"static_blue".to_string(),
EffectType::Static {
parameters: blue_params,
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
let dimmer_effect = create_effect_with_layering(
"dimmer".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(2),
curve: DimmerCurve::Linear,
},
vec!["test_fixture".to_string()],
EffectLayer::Midground,
BlendMode::Multiply,
);
engine.start_effect(blue_effect).unwrap();
engine.start_effect(dimmer_effect).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
assert_eq!(commands.len(), 3);
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert!(blue_cmd.value >= 250);
engine.update(Duration::from_millis(500), None).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert!(blue_cmd.value >= 215 && blue_cmd.value <= 225);
engine.update(Duration::from_millis(500), None).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert!(blue_cmd.value >= 185 && blue_cmd.value <= 195);
}
#[test]
fn test_dimmer_without_dedicated_channel() {
use super::super::effects::*;
use super::super::engine::EffectEngine;
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"rgb_only_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0), );
let mut engine = EffectEngine::new();
engine.register_fixture(fixture.clone());
let mut blue_effect = EffectInstance::new(
"blue".to_string(),
EffectType::Static {
parameters: {
let mut params = HashMap::new();
params.insert("red".to_string(), 0.0);
params.insert("green".to_string(), 0.0);
params.insert("blue".to_string(), 1.0);
params
},
duration: Duration::from_secs(5),
},
vec!["rgb_only_fixture".to_string()],
None,
None,
None,
);
blue_effect.layer = EffectLayer::Background;
blue_effect.blend_mode = BlendMode::Replace;
let mut dimmer_effect = EffectInstance::new(
"dimmer".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["rgb_only_fixture".to_string()],
None,
None,
None,
);
dimmer_effect.layer = EffectLayer::Midground;
dimmer_effect.blend_mode = BlendMode::Multiply;
engine.start_effect(blue_effect).unwrap();
engine.start_effect(dimmer_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
assert_eq!(commands.len(), 3);
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert_eq!(blue_cmd.value, 255);
let commands = engine.update(Duration::from_millis(500), None).unwrap();
assert_eq!(commands.len(), 3);
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert_eq!(
blue_cmd.value, 191,
"Expected 191 (0.75 * 255), got {}",
blue_cmd.value
);
println!("Dimmer without dedicated channel test passed!");
println!("RGB-only fixture properly dims its color channels");
}
#[test]
fn test_dimmer_precedence_and_selective_dimming() {
use super::super::effects::*;
use super::super::engine::EffectEngine;
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0), );
let mut engine = EffectEngine::new();
engine.register_fixture(fixture.clone());
println!("\n1. Blue-only static effect:");
let mut static_params = HashMap::new();
static_params.insert("blue".to_string(), 1.0);
let blue_effect = create_effect_with_layering(
"blue_static".to_string(),
EffectType::Static {
parameters: static_params,
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(blue_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
println!("Commands: {:?}", commands);
for cmd in commands {
let channel_name = match cmd.channel {
1 => "Red",
2 => "Green",
3 => "Blue",
_ => "Unknown",
};
println!(
" {}: {} ({:.1}%)",
channel_name,
cmd.value,
cmd.value as f64 / 255.0 * 100.0
);
}
println!("\n2. Adding dimmer effect (1.0 -> 0.0):");
let mut dimmer_effect = create_effect_with_layering(
"dimmer".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.0,
duration: Duration::from_secs(2),
curve: DimmerCurve::Linear,
},
vec!["test_fixture".to_string()],
EffectLayer::Midground,
BlendMode::Replace,
);
dimmer_effect.up_time = Some(Duration::from_secs(2));
dimmer_effect.hold_time = Some(Duration::from_secs(0));
dimmer_effect.down_time = Some(Duration::from_secs(0));
engine.start_effect(dimmer_effect).unwrap();
let mut previous_time = 0;
for (time_ms, description) in [(0, "Start"), (500, "25%"), (1000, "50%"), (2000, "End")] {
let increment = time_ms - previous_time;
let commands = engine
.update(Duration::from_millis(increment), None)
.unwrap();
previous_time = time_ms;
println!("\n At {} ({}ms):", description, time_ms);
for cmd in commands {
let channel_name = match cmd.channel {
1 => "Red",
2 => "Green",
3 => "Blue",
_ => "Unknown",
};
println!(
" {}: {} ({:.1}%)",
channel_name,
cmd.value,
cmd.value as f64 / 255.0 * 100.0
);
}
}
println!("\nFixed behavior analysis:");
println!("- Red channel: Gets dimmer values multiplied with static red value (for layering)");
println!(
"- Green channel: Gets dimmer values multiplied with static green value (for layering)"
);
println!("- Blue channel: Gets dimmer values multiplied with static blue value (for layering)");
let final_commands = engine.update(Duration::from_millis(2000), None).unwrap();
assert_eq!(final_commands.len(), 1);
let blue_cmd = final_commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(
blue_cmd.value, 255,
"Blue should return to full after dimmer completes"
);
println!("✅ Dimmer precedence and selective dimming test passed!");
println!("✅ RGB channels are used for layering with Multiply mode");
println!("✅ No dedicated dimmer channel - RGB multiplication preserves color");
}
#[test]
fn test_dimmer_debug() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0), );
engine.register_fixture(fixture.clone());
let mut blue_params = HashMap::new();
blue_params.insert("red".to_string(), 0.0);
blue_params.insert("green".to_string(), 0.0);
blue_params.insert("blue".to_string(), 1.0);
let blue_effect = create_effect_with_layering(
"static_blue".to_string(),
EffectType::Static {
parameters: blue_params,
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
let dimmer_effect = create_effect_with_layering(
"dimmer".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["test_fixture".to_string()],
EffectLayer::Midground,
BlendMode::Multiply,
);
engine.start_effect(blue_effect).unwrap();
engine.start_effect(dimmer_effect).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
println!("Commands at start:");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
engine.update(Duration::from_millis(500), None).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
println!("Commands at middle (should be dimmed blue):");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
println!(
"Red: {}, Green: {}, Blue: {}",
red_cmd.value, green_cmd.value, blue_cmd.value
);
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert!(blue_cmd.value > 180 && blue_cmd.value < 200); }
#[test]
fn test_static_with_dimmer_parameter() {
use super::super::effects::*;
use super::super::engine::EffectEngine;
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
channels.insert("strobe".to_string(), 4);
let fixture = FixtureInfo::new(
"front_wash".to_string(),
1,
1,
"Astera-PixelBrick".to_string(),
channels,
Some(20.0), );
let mut engine = EffectEngine::new();
engine.register_fixture(fixture);
let mut back_channels = HashMap::new();
back_channels.insert("red".to_string(), 1);
back_channels.insert("green".to_string(), 2);
back_channels.insert("blue".to_string(), 3);
back_channels.insert("strobe".to_string(), 4);
let back_fixture = FixtureInfo::new(
"back_wash".to_string(),
1,
5, "Astera-PixelBrick".to_string(),
back_channels,
Some(20.0), );
engine.register_fixture(back_fixture);
let mut static_params = HashMap::new();
static_params.insert("red".to_string(), 0.0);
static_params.insert("green".to_string(), 0.0);
static_params.insert("blue".to_string(), 1.0);
static_params.insert("dimmer".to_string(), 1.0);
let static_effect = create_effect_with_layering(
"static_blue_with_dimmer".to_string(),
EffectType::Static {
parameters: static_params,
duration: Duration::from_secs(5),
},
vec!["front_wash".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(static_effect).unwrap();
let commands = engine.update(Duration::from_secs(0), None).unwrap();
println!("Static effect with dimmer parameter:");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let dimmer_effect = create_effect_with_layering(
"dimmer_multiply".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["front_wash".to_string()],
EffectLayer::Midground,
BlendMode::Multiply,
);
engine.start_effect(dimmer_effect).unwrap();
let commands = engine.update(Duration::from_secs(500), None).unwrap(); println!("\nWith dimmer effect (50% through):");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1);
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2);
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3);
if let (Some(red), Some(green), Some(blue)) = (red_cmd, green_cmd, blue_cmd) {
println!("\nAnalysis:");
println!(" Red: {} (should be 0)", red.value);
println!(" Green: {} (should be 0)", green.value);
println!(" Blue: {} (should be dimmed)", blue.value);
}
}
#[test]
fn test_dimmer_replace_vs_multiply() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0), );
engine.register_fixture(fixture.clone());
let mut blue_params = HashMap::new();
blue_params.insert("red".to_string(), 0.0);
blue_params.insert("green".to_string(), 0.0);
blue_params.insert("blue".to_string(), 1.0);
let blue_effect = create_effect_with_layering(
"static_blue".to_string(),
EffectType::Static {
parameters: blue_params,
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
let dimmer_replace = create_effect_with_layering(
"dimmer_replace".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["test_fixture".to_string()],
EffectLayer::Midground,
BlendMode::Replace,
);
engine.start_effect(blue_effect.clone()).unwrap();
engine.start_effect(dimmer_replace).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
println!("Commands with Replace blend mode:");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
println!(
"Replace - Red: {}, Green: {}, Blue: {}",
red_cmd.value, green_cmd.value, blue_cmd.value
);
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert!(blue_cmd.value > 0);
let mut engine2 = EffectEngine::new();
engine2.register_fixture(fixture.clone());
engine2.start_effect(blue_effect).unwrap();
let dimmer_multiply = create_effect_with_layering(
"dimmer_multiply".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["test_fixture".to_string()],
EffectLayer::Midground,
BlendMode::Multiply,
);
engine2.start_effect(dimmer_multiply).unwrap();
let commands = engine2.update(Duration::from_millis(16), None).unwrap();
println!("Commands with Multiply blend mode:");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
println!(
"Multiply - Red: {}, Green: {}, Blue: {}",
red_cmd.value, green_cmd.value, blue_cmd.value
);
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert!(blue_cmd.value > 0);
}
#[test]
fn test_astera_pixelblock_dimmer() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
channels.insert("strobe".to_string(), 4);
let fixture = FixtureInfo::new(
"astera_pixelblock".to_string(),
1,
1,
"Astera-PixelBrick".to_string(),
channels,
Some(20.0), );
engine.register_fixture(fixture.clone());
let mut blue_params = HashMap::new();
blue_params.insert("red".to_string(), 0.0);
blue_params.insert("green".to_string(), 0.0);
blue_params.insert("blue".to_string(), 1.0);
let blue_effect = create_effect_with_layering(
"static_blue".to_string(),
EffectType::Static {
parameters: blue_params,
duration: Duration::from_secs(5),
},
vec!["astera_pixelblock".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
let dimmer_effect = create_effect_with_layering(
"dimmer".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["astera_pixelblock".to_string()],
EffectLayer::Midground,
BlendMode::Multiply,
);
engine.start_effect(blue_effect.clone()).unwrap();
engine.start_effect(dimmer_effect).unwrap();
let commands = engine.update(Duration::from_millis(16), None).unwrap();
println!("Commands with Astera PixelBlock fixture:");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
println!(
"Astera PixelBlock - Red: {}, Green: {}, Blue: {}",
red_cmd.value, green_cmd.value, blue_cmd.value
);
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert!(blue_cmd.value > 0);
let mut engine2 = EffectEngine::new();
engine2.register_fixture(fixture);
engine2.start_effect(blue_effect).unwrap();
let dimmer_replace = create_effect_with_layering(
"dimmer_replace".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.5,
duration: Duration::from_secs(1),
curve: DimmerCurve::Linear,
},
vec!["astera_pixelblock".to_string()],
EffectLayer::Midground,
BlendMode::Replace,
);
engine2.start_effect(dimmer_replace).unwrap();
let commands = engine2.update(Duration::from_millis(16), None).unwrap();
println!("Commands with Replace blend mode:");
for cmd in commands {
println!(" Channel {}: {}", cmd.channel, cmd.value);
}
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
println!(
"Replace - Red: {}, Green: {}, Blue: {}",
red_cmd.value, green_cmd.value, blue_cmd.value
);
assert_eq!(red_cmd.value, 0);
assert_eq!(green_cmd.value, 0);
assert!(blue_cmd.value > 0);
}
#[test]
fn test_chase_effect_without_dimmer_channel() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"rgb_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
let chase_effect = create_effect_with_layering(
"chase_effect".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(1.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(5),
},
vec!["rgb_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
let result = engine.start_effect(chase_effect);
assert!(
result.is_ok(),
"Chase effect should work with RGB-only fixture"
);
let commands = engine.update(Duration::from_millis(100), None).unwrap();
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1);
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2);
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3);
assert!(red_cmd.is_some(), "Should have red channel command");
assert!(green_cmd.is_some(), "Should have green channel command");
assert!(blue_cmd.is_some(), "Should have blue channel command");
if let (Some(red), Some(green), Some(blue)) = (red_cmd, green_cmd, blue_cmd) {
assert_eq!(red.value, green.value);
assert_eq!(green.value, blue.value);
}
}
#[test]
fn test_chase_effect_with_dimmer_channel() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
channels.insert("dimmer".to_string(), 4);
let fixture = FixtureInfo::new(
"rgb_dimmer_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
let chase_effect = create_effect_with_layering(
"chase_effect".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(1.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(5),
},
vec!["rgb_dimmer_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
let result = engine.start_effect(chase_effect);
assert!(
result.is_ok(),
"Chase effect should work with dimmer fixture"
);
let commands = engine.update(Duration::from_millis(100), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 4);
assert!(dimmer_cmd.is_some(), "Should have dimmer channel command");
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1);
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2);
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3);
assert!(
red_cmd.is_none(),
"Should not have red channel command when dimmer is available"
);
assert!(
green_cmd.is_none(),
"Should not have green channel command when dimmer is available"
);
assert!(
blue_cmd.is_none(),
"Should not have blue channel command when dimmer is available"
);
}
#[test]
fn test_software_strobing_dimmer_only_fixture() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
let fixture = FixtureInfo::new(
"dimmer_only_fixture".to_string(),
1,
1,
"Dimmer".to_string(),
channels,
None, );
engine.register_fixture(fixture);
let strobe_effect = create_effect_with_layering(
"strobe_effect".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Fixed(4.0), duration: Duration::from_secs(5),
},
vec!["dimmer_only_fixture".to_string()],
EffectLayer::Foreground,
BlendMode::Overlay,
);
engine.start_effect(strobe_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 255);
let commands = engine.update(Duration::from_millis(62), None).unwrap();
let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 255);
let commands = engine.update(Duration::from_millis(63), None).unwrap(); let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 0);
let commands = engine.update(Duration::from_millis(62), None).unwrap(); let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 0);
let commands = engine.update(Duration::from_millis(63), None).unwrap(); let dimmer_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(dimmer_cmd.value, 255); }
#[test]
fn test_multiple_dimmer_fade_to_black() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let front_wash = FixtureInfo::new(
"front_wash".to_string(),
1,
1,
"Dimmer".to_string(),
channels.clone(),
None,
);
let back_wash = FixtureInfo::new(
"back_wash".to_string(),
1,
5,
"Dimmer".to_string(),
channels.clone(),
None,
);
engine.register_fixture(front_wash);
engine.register_fixture(back_wash);
let mut front_wash_fade = EffectInstance::new(
"front_wash_fade".to_string(),
EffectType::Dimmer {
start_level: 0.5,
end_level: 0.0,
duration: Duration::from_secs(2), curve: DimmerCurve::Linear,
},
vec!["front_wash".to_string()],
None,
None,
None,
);
front_wash_fade.layer = EffectLayer::Foreground;
front_wash_fade.blend_mode = BlendMode::Replace;
let mut back_wash_fade = EffectInstance::new(
"back_wash_fade".to_string(),
EffectType::Dimmer {
start_level: 0.3,
end_level: 0.0,
duration: Duration::from_secs(2), curve: DimmerCurve::Linear,
},
vec!["back_wash".to_string()],
None,
None,
None,
);
back_wash_fade.layer = EffectLayer::Foreground;
back_wash_fade.blend_mode = BlendMode::Replace;
engine.start_effect(front_wash_fade).unwrap();
engine.start_effect(back_wash_fade).unwrap();
println!("Testing fade-out effects from layering_show.light");
for (time_ms, description) in [
(0, "Start"),
(500, "25%"),
(1000, "50%"),
(1500, "75%"),
(2000, "End"),
] {
let commands = engine.update(Duration::from_millis(time_ms), None).unwrap();
println!("\nAt {} ({}ms):", description, time_ms);
let front_dimmer = commands.iter().find(|cmd| cmd.channel == 1);
let back_dimmer = commands.iter().find(|cmd| cmd.channel == 5);
if let Some(cmd) = front_dimmer {
println!(
" Front wash dimmer: {} ({:.1}%)",
cmd.value,
cmd.value as f64 / 255.0 * 100.0
);
} else {
println!(" Front wash dimmer: No command");
}
if let Some(cmd) = back_dimmer {
println!(
" Back wash dimmer: {} ({:.1}%)",
cmd.value,
cmd.value as f64 / 255.0 * 100.0
);
} else {
println!(" Back wash dimmer: No command");
}
}
let final_commands = engine.update(Duration::from_millis(2000), None).unwrap();
for cmd in final_commands {
assert_eq!(cmd.value, 0, "Dimmer should persist at 0 after completion");
}
println!("✅ Fade-out effects test completed");
}
#[test]
fn test_dimmer_effect_mid_level_start() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let mut dimmer_effect = EffectInstance::new(
"fade_out_test".to_string(),
EffectType::Dimmer {
start_level: 0.5,
end_level: 0.0,
duration: Duration::from_secs(2), curve: DimmerCurve::Linear,
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
dimmer_effect.layer = EffectLayer::Foreground;
dimmer_effect.blend_mode = BlendMode::Replace;
let static_effect = EffectInstance::new(
"static_blue".to_string(),
EffectType::Static {
parameters: {
let mut params = HashMap::new();
params.insert("red".to_string(), 0.0);
params.insert("green".to_string(), 0.0);
params.insert("blue".to_string(), 1.0);
params
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(static_effect).unwrap();
engine.start_effect(dimmer_effect.clone()).unwrap();
let commands = engine.update(Duration::from_secs(0), None).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(blue_cmd.value, 127, "Blue should be at 50% (127) at 0s");
let commands = engine.update(Duration::from_millis(500), None).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(blue_cmd.value, 95, "Blue should be at 37.5% (95) at 0.5s");
let commands = engine.update(Duration::from_millis(500), None).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(blue_cmd.value, 63, "Blue should be at 25% (63) at 1s");
let commands = engine.update(Duration::from_secs(1), None).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(
blue_cmd.value, 255,
"Blue should return to full after dimmer completes"
);
assert_eq!(
engine.active_effects_count(),
1,
"Only static effect should remain active"
);
}
#[test]
fn test_dimmer_curves() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let static_blue = EffectInstance::new(
"static_blue".to_string(),
EffectType::Static {
parameters: {
let mut params = HashMap::new();
params.insert("red".to_string(), 0.0);
params.insert("green".to_string(), 0.0);
params.insert("blue".to_string(), 1.0);
params
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(static_blue).unwrap();
let curves = vec![
(DimmerCurve::Linear, "Linear"),
(DimmerCurve::Exponential, "Exponential"),
(DimmerCurve::Logarithmic, "Logarithmic"),
(DimmerCurve::Sine, "Sine"),
(DimmerCurve::Cosine, "Cosine"),
];
for (curve, curve_name) in curves {
let mut test_engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"test_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
None,
);
test_engine.register_fixture(fixture);
let static_blue = EffectInstance::new(
"static_blue".to_string(),
EffectType::Static {
parameters: {
let mut params = HashMap::new();
params.insert("red".to_string(), 0.0);
params.insert("green".to_string(), 0.0);
params.insert("blue".to_string(), 1.0);
params
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
test_engine.start_effect(static_blue).unwrap();
let mut dimmer = EffectInstance::new(
"dimmer".to_string(),
EffectType::Dimmer {
start_level: 1.0,
end_level: 0.0,
duration: Duration::from_secs(2),
curve: curve.clone(),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
dimmer.layer = EffectLayer::Midground;
dimmer.blend_mode = BlendMode::Multiply;
test_engine.start_effect(dimmer).unwrap();
println!("\n{} curve:", curve_name);
let test_points = vec![
(0, "0%"),
(500, "25%"),
(1000, "50%"),
(1500, "75%"),
(2000, "100%"),
];
let mut values = Vec::new();
for (time_ms, label) in test_points {
let commands = test_engine
.update(Duration::from_millis(time_ms), None)
.unwrap();
let blue_value = commands
.iter()
.find(|c| c.channel == 3)
.map(|c| c.value)
.unwrap_or(255);
values.push(blue_value);
println!(" {} ({:4}ms): {}", label, time_ms, blue_value);
}
match curve {
DimmerCurve::Linear => {
assert_eq!(values[0], 255, "Linear start should be 255");
assert_eq!(
values[4], 255,
"After dimmer completes, blue returns to full"
);
}
DimmerCurve::Exponential => {
assert_eq!(values[0], 255, "Exponential start should be 255");
let early_drop = values[0] as i32 - values[1] as i32;
let mid_drop = values[1] as i32 - values[2] as i32;
assert!(
early_drop < mid_drop,
"Exponential: early fade should be slower (early: {}, mid: {})",
early_drop,
mid_drop
);
assert_eq!(
values[4], 255,
"After dimmer completes, blue returns to full"
);
}
DimmerCurve::Logarithmic => {
assert_eq!(values[0], 255, "Logarithmic start should be 255");
let early_drop = values[0] as i32 - values[1] as i32;
let mid_drop = values[1] as i32 - values[2] as i32;
assert!(
early_drop > mid_drop,
"Logarithmic: early fade should be faster (early: {}, mid: {})",
early_drop,
mid_drop
);
assert_eq!(
values[4], 255,
"After dimmer completes, blue returns to full"
);
}
DimmerCurve::Sine => {
assert_eq!(values[0], 255, "Sine start should be 255");
assert_eq!(
values[4], 255,
"After dimmer completes, blue returns to full"
);
}
DimmerCurve::Cosine => {
assert_eq!(values[0], 255, "Cosine start should be 255");
assert_eq!(
values[4], 255,
"After dimmer completes, blue returns to full"
);
}
}
}
println!("\n✅ All dimmer curves tested successfully");
}