feat: video/state_machine unit tests and on_video_completed logic fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
pub mod video;
|
||||
pub mod http;
|
||||
pub mod ble;
|
||||
pub mod http;
|
||||
pub mod screen;
|
||||
pub mod video;
|
||||
pub mod wifi;
|
||||
|
||||
@@ -28,7 +28,8 @@ impl ScreenPlugin {
|
||||
}
|
||||
|
||||
match Command::new("systemd-inhibit")
|
||||
.arg("--what=idle")
|
||||
.arg("--what=idle:sleep")
|
||||
.arg("--mode=block")
|
||||
.arg("--who=ShowenV2")
|
||||
.arg("--why=Prevent screen lock during playback")
|
||||
.arg("sleep")
|
||||
@@ -66,6 +67,13 @@ impl ScreenPlugin {
|
||||
}
|
||||
|
||||
if hidden {
|
||||
let _ = Command::new("pkill")
|
||||
.args(["-f", "unclutter"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
|
||||
match Command::new("unclutter")
|
||||
.arg("-idle")
|
||||
.arg("0")
|
||||
@@ -80,7 +88,7 @@ impl ScreenPlugin {
|
||||
}
|
||||
} else {
|
||||
match Command::new("pkill")
|
||||
.arg("unclutter")
|
||||
.args(["-f", "unclutter"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -118,6 +126,10 @@ impl Plugin for ScreenPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<&'static str> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn init(&mut self, ctx: PluginContext) -> Result<()> {
|
||||
self.ctx = Some(ctx);
|
||||
Ok(())
|
||||
|
||||
@@ -102,6 +102,10 @@ impl Plugin for VideoPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<&'static str> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn init(&mut self, ctx: PluginContext) -> Result<()> {
|
||||
self.ctx = Some(ctx);
|
||||
Ok(())
|
||||
|
||||
@@ -1350,3 +1350,298 @@ impl Drop for VideoProcessor {
|
||||
Self::show_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_video_loop_count, TransitionEffect, VideoProcessor, VideoTransformer};
|
||||
use crate::core::config::{
|
||||
AppConfig, BleConfig, DisplayConfig, PerspectiveCorrectionConfig, PlaybackConfig,
|
||||
RemoteControlConfig, ScaleMode, ScenesConfig, TransitionConfig, TransitionType, VideoItem,
|
||||
};
|
||||
use opencv::{
|
||||
core::{Scalar, Size, Vec3b, CV_8UC3},
|
||||
prelude::*,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn transformer_applies_rotation_flip_and_fit_padding() {
|
||||
let mut display = sample_display();
|
||||
display.render_width = 4;
|
||||
display.render_height = 4;
|
||||
display.rotation = 90;
|
||||
display.flip_horizontal = true;
|
||||
|
||||
let transformer = VideoTransformer::new(&display);
|
||||
let mut frame = Mat::new_rows_cols_with_default(2, 1, CV_8UC3, Scalar::all(0.0))
|
||||
.expect("frame should build");
|
||||
*frame
|
||||
.at_2d_mut::<Vec3b>(0, 0)
|
||||
.expect("top pixel should exist") = Vec3b::from([10, 0, 0]);
|
||||
*frame
|
||||
.at_2d_mut::<Vec3b>(1, 0)
|
||||
.expect("bottom pixel should exist") = Vec3b::from([20, 0, 0]);
|
||||
|
||||
let output = transformer
|
||||
.transform(&frame)
|
||||
.expect("transform should succeed");
|
||||
|
||||
assert_eq!(output.size().expect("size should exist"), Size::new(4, 4));
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(0, 0)
|
||||
.expect("top-left pixel should exist"),
|
||||
Vec3b::from([0, 0, 0])
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(2, 0)
|
||||
.expect("bottom-left pixel should exist"),
|
||||
Vec3b::from([10, 0, 0])
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(2, 3)
|
||||
.expect("bottom-right pixel should exist"),
|
||||
Vec3b::from([20, 0, 0])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_effect_fades_between_frames_and_resizes_previous_frame() {
|
||||
let effect = TransitionEffect::new(0.5, TransitionType::Fade);
|
||||
let previous = Mat::new_rows_cols_with_default(1, 1, CV_8UC3, Scalar::all(0.0))
|
||||
.expect("previous frame should build");
|
||||
let current = Mat::new_rows_cols_with_default(2, 2, CV_8UC3, Scalar::all(200.0))
|
||||
.expect("current frame should build");
|
||||
|
||||
let output = effect
|
||||
.apply(Some(&previous), ¤t, 0.25)
|
||||
.expect("fade should succeed");
|
||||
|
||||
assert_eq!(output.size().expect("size should exist"), Size::new(2, 2));
|
||||
assert_eq!(
|
||||
*output.at_2d::<Vec3b>(0, 0).expect("pixel should exist"),
|
||||
Vec3b::from([50, 50, 50])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_effect_cut_returns_current_frame() {
|
||||
let effect = TransitionEffect::new(0.5, TransitionType::Cut);
|
||||
let previous = Mat::new_rows_cols_with_default(1, 1, CV_8UC3, Scalar::all(0.0))
|
||||
.expect("previous frame should build");
|
||||
let current = Mat::new_rows_cols_with_default(1, 1, CV_8UC3, Scalar::all(123.0))
|
||||
.expect("current frame should build");
|
||||
|
||||
let output = effect
|
||||
.apply(Some(&previous), ¤t, 0.5)
|
||||
.expect("cut should succeed");
|
||||
|
||||
assert_eq!(
|
||||
*output.at_2d::<Vec3b>(0, 0).expect("pixel should exist"),
|
||||
Vec3b::from([123, 123, 123])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_output_size_requires_both_dimensions() {
|
||||
let mut processor = sample_processor();
|
||||
processor.config.display.output_width = Some(1920);
|
||||
processor.config.display.output_height = Some(1080);
|
||||
assert_eq!(
|
||||
processor.configured_output_size(),
|
||||
Some(Size::new(1920, 1080))
|
||||
);
|
||||
|
||||
processor.config.display.output_height = None;
|
||||
assert_eq!(processor.configured_output_size(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_output_frame_fit_preserves_aspect_ratio_with_padding() {
|
||||
let mut processor = sample_processor();
|
||||
processor.output_size = Size::new(1920, 1080);
|
||||
processor.config.display.scale_mode = ScaleMode::Fit;
|
||||
|
||||
let frame = Mat::new_rows_cols_with_default(1024, 1024, CV_8UC3, Scalar::all(255.0))
|
||||
.expect("frame should build");
|
||||
let output = processor
|
||||
.prepare_output_frame(frame)
|
||||
.expect("fit output should build");
|
||||
|
||||
assert_eq!(
|
||||
output.size().expect("output size should exist"),
|
||||
Size::new(1920, 1080)
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 100)
|
||||
.expect("left padding pixel should exist"),
|
||||
Vec3b::from([0, 0, 0])
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 960)
|
||||
.expect("center pixel should exist"),
|
||||
Vec3b::from([255, 255, 255])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_output_frame_stretch_fills_target_without_padding() {
|
||||
let mut processor = sample_processor();
|
||||
processor.output_size = Size::new(1920, 1080);
|
||||
processor.config.display.scale_mode = ScaleMode::Stretch;
|
||||
|
||||
let frame = Mat::new_rows_cols_with_default(1024, 1024, CV_8UC3, Scalar::all(255.0))
|
||||
.expect("frame should build");
|
||||
let output = processor
|
||||
.prepare_output_frame(frame)
|
||||
.expect("stretch output should build");
|
||||
|
||||
assert_eq!(
|
||||
output.size().expect("output size should exist"),
|
||||
Size::new(1920, 1080)
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 100)
|
||||
.expect("edge pixel should exist"),
|
||||
Vec3b::from([255, 255, 255])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_output_frame_fit_applies_horizontal_offset_within_bounds() {
|
||||
let mut processor = sample_processor();
|
||||
processor.output_size = Size::new(1920, 1080);
|
||||
processor.config.display.scale_mode = ScaleMode::Fit;
|
||||
processor.config.display.offset_x = 50;
|
||||
|
||||
let frame = Mat::new_rows_cols_with_default(1024, 1024, CV_8UC3, Scalar::all(255.0))
|
||||
.expect("frame should build");
|
||||
let output = processor
|
||||
.prepare_output_frame(frame)
|
||||
.expect("fit output should build");
|
||||
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 450)
|
||||
.expect("shifted padding pixel should exist"),
|
||||
Vec3b::from([0, 0, 0])
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 530)
|
||||
.expect("shifted content pixel should exist"),
|
||||
Vec3b::from([255, 255, 255])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_output_frame_fit_clamps_offset_to_available_padding() {
|
||||
let mut processor = sample_processor();
|
||||
processor.output_size = Size::new(1920, 1080);
|
||||
processor.config.display.scale_mode = ScaleMode::Fit;
|
||||
processor.config.display.offset_x = 1000;
|
||||
|
||||
let frame = Mat::new_rows_cols_with_default(1024, 1024, CV_8UC3, Scalar::all(255.0))
|
||||
.expect("frame should build");
|
||||
let output = processor
|
||||
.prepare_output_frame(frame)
|
||||
.expect("fit output should build");
|
||||
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 0)
|
||||
.expect("left edge pixel should exist"),
|
||||
Vec3b::from([0, 0, 0])
|
||||
);
|
||||
assert_eq!(
|
||||
*output
|
||||
.at_2d::<Vec3b>(540, 1919)
|
||||
.expect("right edge pixel should exist"),
|
||||
Vec3b::from([255, 255, 255])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_video_loop_count_prefers_random_range() {
|
||||
let item = VideoItem {
|
||||
id: "clip".to_string(),
|
||||
path: "clip.mp4".to_string(),
|
||||
duration: None,
|
||||
loop_count: 99,
|
||||
random_loop_range: Some([2, 2]),
|
||||
};
|
||||
|
||||
assert_eq!(resolve_video_loop_count(&item), 2);
|
||||
}
|
||||
|
||||
fn sample_processor() -> VideoProcessor {
|
||||
VideoProcessor::new(sample_config()).expect("sample processor should build")
|
||||
}
|
||||
|
||||
fn sample_config() -> AppConfig {
|
||||
AppConfig {
|
||||
display: sample_display(),
|
||||
playlist: vec![VideoItem {
|
||||
id: "sample".to_string(),
|
||||
path: "sample.mp4".to_string(),
|
||||
duration: None,
|
||||
loop_count: 1,
|
||||
random_loop_range: None,
|
||||
}],
|
||||
transition: TransitionConfig {
|
||||
enabled: false,
|
||||
transition_type: TransitionType::None,
|
||||
duration: 0.0,
|
||||
},
|
||||
playback: PlaybackConfig {
|
||||
loop_playlist: false,
|
||||
auto_start: false,
|
||||
},
|
||||
scenes: ScenesConfig {
|
||||
rest: vec![],
|
||||
active: vec![],
|
||||
sleep: vec![],
|
||||
interact: vec![],
|
||||
state_machine: None,
|
||||
},
|
||||
remote_control: RemoteControlConfig {
|
||||
enabled: false,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
},
|
||||
ble: BleConfig::default(),
|
||||
source_path: PathBuf::from("/tmp/config.json"),
|
||||
source_dir: PathBuf::from("/tmp"),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_display() -> DisplayConfig {
|
||||
DisplayConfig {
|
||||
fullscreen: false,
|
||||
window_title: "test".to_string(),
|
||||
rotation: 0,
|
||||
flip_horizontal: false,
|
||||
flip_vertical: false,
|
||||
offset_x: 0,
|
||||
offset_y: 0,
|
||||
prevent_screen_lock: false,
|
||||
render_width: 1024,
|
||||
render_height: 1024,
|
||||
output_width: None,
|
||||
output_height: None,
|
||||
scale_mode: ScaleMode::Fit,
|
||||
allow_upscale: true,
|
||||
perspective_correction: PerspectiveCorrectionConfig {
|
||||
enabled: false,
|
||||
points: vec![],
|
||||
},
|
||||
chroma_key: Default::default(),
|
||||
brightness_adjust: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +43,11 @@ impl StateMachine {
|
||||
pub fn on_video_completed(&mut self) -> Result<bool> {
|
||||
self.ensure_current_state_valid()?;
|
||||
|
||||
let old_state = self.current_state.clone();
|
||||
|
||||
if self.current_loop_remaining > 1 {
|
||||
self.current_loop_remaining -= 1;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(target_state) = self.pending_trigger_target.take() {
|
||||
self.transition_to_state(&target_state)?;
|
||||
return Ok(true);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let sequence_len = self
|
||||
@@ -61,12 +58,17 @@ impl StateMachine {
|
||||
if self.current_sequence_index + 1 < sequence_len {
|
||||
self.current_sequence_index += 1;
|
||||
self.current_loop_remaining = self.resolve_current_loop_count()?;
|
||||
return Ok(true);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(target_state) = self.pending_trigger_target.take() {
|
||||
self.transition_to_state(&target_state)?;
|
||||
return Ok(self.current_state != old_state);
|
||||
}
|
||||
|
||||
let next_state = self.select_next_state()?;
|
||||
self.transition_to_state(&next_state)?;
|
||||
Ok(true)
|
||||
Ok(self.current_state != old_state)
|
||||
}
|
||||
|
||||
pub fn handle_trigger(&mut self, name: &str, value: &str) -> Result<bool> {
|
||||
@@ -78,6 +80,10 @@ impl StateMachine {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if self.pending_trigger_target.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let target_state = self
|
||||
.matching_transition(&state.transitions, name, value)
|
||||
.map(|transition| transition.target_state.clone());
|
||||
@@ -88,7 +94,10 @@ impl StateMachine {
|
||||
|
||||
if state.defer_triggers {
|
||||
self.pending_trigger_target = Some(target_state);
|
||||
return Ok(true);
|
||||
if self.current_loop_remaining > 1 {
|
||||
self.current_loop_remaining = 1;
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.transition_to_state(&target_state)?;
|
||||
@@ -178,6 +187,10 @@ impl StateMachine {
|
||||
}
|
||||
|
||||
fn resolve_current_loop_count(&self) -> Result<i32> {
|
||||
if self.pending_trigger_target.is_some() {
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
let state = self
|
||||
.current_state_config()
|
||||
.ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?;
|
||||
@@ -238,14 +251,14 @@ impl StateMachine {
|
||||
}
|
||||
|
||||
let mut cursor = rand::thread_rng().gen_range(0.0..total_weight);
|
||||
for entry in valid_entries {
|
||||
for entry in &valid_entries {
|
||||
cursor -= entry.weight;
|
||||
if cursor <= 0.0 {
|
||||
return Some(entry.state.clone());
|
||||
}
|
||||
}
|
||||
|
||||
entries.last().map(|entry| entry.state.clone())
|
||||
valid_entries.last().map(|entry| entry.state.clone())
|
||||
}
|
||||
|
||||
fn matching_transition<'a>(
|
||||
@@ -262,10 +275,220 @@ impl StateMachine {
|
||||
|
||||
fn trigger_matches(trigger: &TriggerType, name: &str, value: &str) -> bool {
|
||||
match trigger {
|
||||
TriggerType::Button { name: expected } => expected == name,
|
||||
TriggerType::Button { name: expected } => {
|
||||
(name == "button" && value == expected) || expected == name
|
||||
}
|
||||
TriggerType::Voice { keyword } => keyword == value || keyword == name,
|
||||
TriggerType::Sensor { name: expected } => expected == name,
|
||||
TriggerType::Sensor { name: expected } => {
|
||||
(name == "sensor" && value == expected) || expected == name
|
||||
}
|
||||
TriggerType::Timer { .. } | TriggerType::Random { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::StateMachine;
|
||||
use crate::core::config::{
|
||||
AnimationStep, NextStateEntry, StateConfig, StateMachineConfig, StateMode, StateTransition,
|
||||
TriggerType,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn button_and_sensor_triggers_match_legacy_message_shape() {
|
||||
let mut machine = StateMachine::new(config_with_states([
|
||||
state(
|
||||
"idle",
|
||||
vec![step("idle")],
|
||||
vec![
|
||||
transition(
|
||||
TriggerType::Button {
|
||||
name: "button1".to_string(),
|
||||
},
|
||||
"button_state",
|
||||
10,
|
||||
),
|
||||
transition(
|
||||
TriggerType::Sensor {
|
||||
name: "touch".to_string(),
|
||||
},
|
||||
"sensor_state",
|
||||
10,
|
||||
),
|
||||
],
|
||||
),
|
||||
state("button_state", vec![step("button")], vec![]),
|
||||
state("sensor_state", vec![step("sensor")], vec![]),
|
||||
]));
|
||||
machine.start().expect("state machine should start");
|
||||
|
||||
assert!(machine
|
||||
.handle_trigger("button", "button1")
|
||||
.expect("button trigger should work"));
|
||||
assert_eq!(machine.current_state, "button_state");
|
||||
|
||||
machine.transition_to_state("idle").expect("reset to idle");
|
||||
assert!(machine
|
||||
.handle_trigger("sensor", "touch")
|
||||
.expect("sensor trigger should work"));
|
||||
assert_eq!(machine.current_state, "sensor_state");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_trigger_waits_for_sequence_end_and_forces_single_remaining_loops() {
|
||||
let mut machine = StateMachine::new(config_with_states([
|
||||
StateConfig {
|
||||
name: "idle".to_string(),
|
||||
mode: StateMode::FreeMode,
|
||||
sequence: vec![
|
||||
AnimationStep {
|
||||
video_id: "looping".to_string(),
|
||||
loop_count: Some(3),
|
||||
random_loop_range: None,
|
||||
},
|
||||
AnimationStep {
|
||||
video_id: "tail".to_string(),
|
||||
loop_count: Some(5),
|
||||
random_loop_range: None,
|
||||
},
|
||||
],
|
||||
next_state: None,
|
||||
next_states: None,
|
||||
transitions: vec![transition(
|
||||
TriggerType::Voice {
|
||||
keyword: "name".to_string(),
|
||||
},
|
||||
"called",
|
||||
10,
|
||||
)],
|
||||
weight: 1.0,
|
||||
defer_triggers: true,
|
||||
ignore_triggers: false,
|
||||
},
|
||||
state("called", vec![step("called")], vec![]),
|
||||
]));
|
||||
machine.start().expect("state machine should start");
|
||||
|
||||
assert_eq!(machine.current_loop_remaining, 3);
|
||||
assert!(!machine
|
||||
.handle_trigger("voice", "name")
|
||||
.expect("deferred trigger should queue"));
|
||||
assert!(machine.has_pending_trigger());
|
||||
assert_eq!(machine.current_state, "idle");
|
||||
assert_eq!(machine.current_loop_remaining, 1);
|
||||
|
||||
assert!(!machine
|
||||
.on_video_completed()
|
||||
.expect("should advance within same state"));
|
||||
assert_eq!(machine.current_sequence_index, 1);
|
||||
assert_eq!(machine.current_loop_remaining, 1);
|
||||
assert_eq!(machine.current_video_id().as_deref(), Some("tail"));
|
||||
|
||||
assert!(machine
|
||||
.on_video_completed()
|
||||
.expect("pending trigger should fire at sequence end"));
|
||||
assert_eq!(machine.current_state, "called");
|
||||
assert_eq!(machine.current_video_id().as_deref(), Some("called"));
|
||||
assert!(!machine.has_pending_trigger());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_video_completed_returns_false_when_staying_in_same_state() {
|
||||
let mut machine = StateMachine::new(config_with_states([StateConfig {
|
||||
name: "idle".to_string(),
|
||||
mode: StateMode::FreeMode,
|
||||
sequence: vec![step("a"), step("b")],
|
||||
next_state: Some("idle".to_string()),
|
||||
next_states: None,
|
||||
transitions: vec![],
|
||||
weight: 1.0,
|
||||
defer_triggers: false,
|
||||
ignore_triggers: false,
|
||||
}]));
|
||||
machine.start().expect("state machine should start");
|
||||
|
||||
assert!(!machine
|
||||
.on_video_completed()
|
||||
.expect("step transition should stay in same state"));
|
||||
assert_eq!(machine.current_video_id().as_deref(), Some("b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_states_take_priority_over_next_state() {
|
||||
let mut machine = StateMachine::new(config_with_states([
|
||||
StateConfig {
|
||||
name: "idle".to_string(),
|
||||
mode: StateMode::FreeMode,
|
||||
sequence: vec![step("idle")],
|
||||
next_state: Some("fallback".to_string()),
|
||||
next_states: Some(vec![NextStateEntry {
|
||||
state: "weighted".to_string(),
|
||||
weight: 1.0,
|
||||
}]),
|
||||
transitions: vec![],
|
||||
weight: 1.0,
|
||||
defer_triggers: false,
|
||||
ignore_triggers: false,
|
||||
},
|
||||
state("weighted", vec![step("weighted")], vec![]),
|
||||
state("fallback", vec![step("fallback")], vec![]),
|
||||
]));
|
||||
machine.start().expect("state machine should start");
|
||||
|
||||
assert!(machine
|
||||
.on_video_completed()
|
||||
.expect("state should change via next_states"));
|
||||
assert_eq!(machine.current_state, "weighted");
|
||||
}
|
||||
|
||||
fn config_with_states<const N: usize>(states: [StateConfig; N]) -> StateMachineConfig {
|
||||
let states = Vec::from(states)
|
||||
.into_iter()
|
||||
.map(|state| {
|
||||
let name = state.name.clone();
|
||||
(name, state)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
StateMachineConfig {
|
||||
initial_state: "idle".to_string(),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(
|
||||
name: &str,
|
||||
sequence: Vec<AnimationStep>,
|
||||
transitions: Vec<StateTransition>,
|
||||
) -> StateConfig {
|
||||
StateConfig {
|
||||
name: name.to_string(),
|
||||
mode: StateMode::FreeMode,
|
||||
sequence,
|
||||
next_state: None,
|
||||
next_states: None,
|
||||
transitions,
|
||||
weight: 1.0,
|
||||
defer_triggers: false,
|
||||
ignore_triggers: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn step(video_id: &str) -> AnimationStep {
|
||||
AnimationStep {
|
||||
video_id: video_id.to_string(),
|
||||
loop_count: Some(1),
|
||||
random_loop_range: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition(trigger: TriggerType, target_state: &str, priority: i32) -> StateTransition {
|
||||
StateTransition {
|
||||
trigger,
|
||||
target_state: target_state.to_string(),
|
||||
priority,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user