use super::*;
use crate::mesh::{Clip, NoParts, Posing};
use crate::{AnimationStates, Animator, Motion, Progress, Transition};
const CORGI: &str = "examples/assets/corgi.glb";
const CHEST: &str = "examples/assets/chest.glb";
const TALL: u32 = 64;
const STEP: Duration = Duration::from_millis(100);
const ASIDE: Camera = Camera::new(
View::look_at(Vec3::new(700.0, 250.0, 900.0), Vec3::new(0.0, 0.0, 197.0)),
Projection::orthographic(1000.0),
);
const AHEAD: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 0.8, 2.0), Vec3::new(0.0, 0.3, 0.0)),
Projection::orthographic(1.5),
);
meshes! { enum CorgiSet { Corgi } }
meshes! { enum ChestSet { Chest } }
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Mood {
Idle,
Walk,
}
impl Clip for Mood {
fn from_name(name: &str) -> Option<Self> {
match name {
"idle" => Some(Self::Idle),
"walk" => Some(Self::Walk),
_ => None,
}
}
fn all() -> Vec<Self> {
vec![Self::Idle, Self::Walk]
}
fn index(&self) -> u32 {
self.clone() as u32
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Corgi;
impl Catalog for Corgi {
fn catalog() -> Vec<Self> {
vec![Self]
}
}
impl Mesh<NoParts, Mood> for Corgi {
fn build(&self, assets: &Assets) -> MeshData<NoParts, Mood> {
assets.model("RootNode")
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Lid {
Open,
}
impl Clip for Lid {
fn from_name(name: &str) -> Option<Self> {
match name {
"open" => Some(Self::Open),
_ => None,
}
}
fn all() -> Vec<Self> {
vec![Self::Open]
}
fn index(&self) -> u32 {
0
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Chest;
impl Catalog for Chest {
fn catalog() -> Vec<Self> {
vec![Self]
}
}
impl Mesh<NoParts, Lid> for Chest {
fn build(&self, assets: &Assets) -> MeshData<NoParts, Lid> {
assets.model("Chest")
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Walking {
On,
}
impl AnimationStates for Walking {
type Clip = Mood;
type Input = ();
fn entry() -> Self {
Self::On
}
fn motion(&self, _input: &()) -> Motion<Mood> {
Motion::looping(Mood::Walk)
}
fn next(&self, _input: &(), _at: Progress) -> Option<Transition<Self>> {
None
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Opening {
It,
}
impl AnimationStates for Opening {
type Clip = Lid;
type Input = f32;
fn entry() -> Self {
Self::It
}
fn motion(&self, openness: &f32) -> Motion<Lid> {
Motion::scrubbed(Lid::Open, *openness)
}
fn next(&self, _input: &f32, _at: Progress) -> Option<Transition<Self>> {
None
}
}
struct Trotting {
animator: Animator<Corgi, Walking>,
at: Option<f32>,
}
impl Game for Trotting {
type Meshes = CorgiSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
ctx.animate(Corgi, &mut self.animator, &());
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(ASIDE);
let corgi = Corgi.at(Vec3::ZERO).material(Material::color(Color::WHITE));
ctx.draw(match self.at {
Some(at) => corgi.posed_by(Posing::clip(Mood::Walk.index(), at)),
None => corgi.posed(&self.animator),
});
}
}
struct Opened {
animator: Animator<Chest, Opening>,
openness: Option<f32>,
at: Option<f32>,
}
impl Game for Opened {
type Meshes = ChestSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
let Some(openness) = self.openness else {
return;
};
ctx.animate(Chest, &mut self.animator, &openness);
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(AHEAD);
let chest = Chest.at(Vec3::ZERO).material(Material::color(Color::WHITE));
ctx.draw(match (self.at, self.openness) {
(Some(at), _) => chest.posed_by(Posing::clip(Lid::Open.index(), at)),
(None, Some(_)) => chest.posed(&self.animator),
(None, None) => chest,
});
}
}
fn session<G: Game>(title: &str, source: &str, game: G) -> Option<Session<G>> {
let config = Config::new(title)
.with_tonemap(Tonemap::Off)
.with_assets([source])
.with_tick_interval(STEP);
started(config, UVec2::splat(TALL), |_ctx| Ok(game))
}
fn after<G: Game>(session: &mut Session<G>, ticks: u32) -> Vec<u8> {
for _ in 0..ticks {
session.tick();
}
session.step();
session.pixels().expect("the target reads back")
}
fn drawn<G: Game>(title: &str, source: &str, game: G) -> Option<Vec<u8>> {
let mut session = session(title, source, game)?;
Some(after(&mut session, 1))
}
#[test]
fn a_held_machine_draws_one_pose_however_far_past_its_tick_the_frames_draw() {
let walking = Trotting {
animator: Animator::new(),
at: None,
};
let Some(mut session) = session("headless machine held", CORGI, walking) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let running = after(&mut session, 3);
session.game_mut().animator.hold();
let held = after(&mut session, 1);
session.set_frame_interval(STEP * 4 + STEP / 3);
let third = after(&mut session, 0);
let two_thirds = after(&mut session, 0);
assert!(drawn_pixels(&running) > 0, "the model is drawn at all");
assert_eq!(
third, held,
"a frame a third of a tick past the tick that held the machine draws \
the pose that tick left"
);
assert_eq!(
two_thirds, held,
"and so does the frame after it, further into the tick"
);
}
#[test]
fn a_machine_poses_a_draw_at_the_instant_the_frame_draws_it() {
let Some(start) = drawn(
"headless machine start",
CORGI,
Trotting {
animator: Animator::new(),
at: Some(0.0),
},
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let walking = Trotting {
animator: Animator::new(),
at: None,
};
let Some(mut session) = session("headless machine", CORGI, walking) else {
return;
};
let unrun = after(&mut session, 0);
let first = after(&mut session, 1);
let later = after(&mut session, 5);
assert!(drawn_pixels(&start) > 0, "the model is drawn at all");
assert_eq!(
unrun, start,
"a machine no tick has run poses the draw at the start of what it \
plays, which is that clip read through the engine's own path"
);
assert_eq!(
first, unrun,
"a tick and the step after it read one instant, so that step draws \
the pose the tick started the machine in"
);
assert_ne!(
later, first,
"and the steps that follow draw it further along that clip"
);
}
#[test]
fn a_scrubbed_machine_poses_a_draw_by_the_value_the_game_states() {
let closed = Opened {
animator: Animator::new(),
openness: Some(0.0),
at: None,
};
let Some(shut) = drawn("headless chest shut", CHEST, closed) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(rest) = drawn(
"headless chest rest",
CHEST,
Opened {
animator: Animator::new(),
openness: None,
at: None,
},
) else {
return;
};
let Some(whole) = drawn(
"headless chest open",
CHEST,
Opened {
animator: Animator::new(),
openness: Some(1.0),
at: None,
},
) else {
return;
};
let Some(last) = drawn(
"headless chest last",
CHEST,
Opened {
animator: Animator::new(),
openness: None,
at: Some(1e9),
},
) else {
return;
};
assert!(drawn_pixels(&shut) > 0, "the model is drawn at all");
assert_eq!(shut, rest, "a lid scrubbed to nothing draws the rest pose");
assert_ne!(shut, whole, "and one scrubbed whole draws another");
assert_eq!(whole, last, "which is the last key of the clip it scrubs");
}