use renew_ecs::{Entities, Store};
use renew_fixed::{Angle, Fixed, Vec2};
use renew_scene::{Global, Local, Parent, Propagated, Scratch, propagate};
#[derive(Default)]
struct World {
entities: Entities,
parents: Store<Parent>,
locals: Store<Local>,
globals: Store<Global>,
scratch: Scratch,
}
impl World {
fn new() -> Self {
Self::default()
}
fn node(&mut self, local: Local) -> renew_ecs::Entity {
let entity = self.entities.spawn();
self.locals.insert(entity.index(), local);
entity
}
fn attach(&mut self, child: renew_ecs::Entity, parent: renew_ecs::Entity) {
self.parents.insert(child.index(), Parent(parent));
}
fn run(&mut self) -> Propagated {
propagate(
&mut self.scratch,
&self.entities,
&self.parents,
&self.locals,
&mut self.globals,
)
}
#[allow(clippy::expect_used, reason = "a missing placement is the defect")]
fn placement(&self, entity: renew_ecs::Entity) -> Global {
*self
.globals
.get(entity.index())
.expect("every scene node is placed")
}
}
fn at(x: i32, y: i32) -> Vec2 {
Vec2::new(Fixed::from_int(x), Fixed::from_int(y))
}
#[test]
fn a_node_with_no_parent_lands_exactly_where_its_local_says() {
let mut world = World::new();
let lone = world.node(Local::new(at(3, -4), Angle::QUARTER));
let counts = world.run();
assert_eq!(world.placement(lone).translation(), at(3, -4));
assert_eq!(world.placement(lone).rotation(), Angle::QUARTER);
assert_eq!(
counts,
Propagated {
nodes: 1,
roots: 1,
orphaned: 0,
cyclic: 0
}
);
}
#[test]
fn a_child_orbits_when_its_parent_turns() {
let mut world = World::new();
let parent = world.node(Local::new(at(10, 0), Angle::QUARTER));
let child = world.node(Local::new(at(1, 0), Angle::ZERO));
world.attach(child, parent);
world.run();
let placed = world.placement(child);
assert_eq!(
placed.translation(),
at(10, 1),
"the child swung with x -> y"
);
assert_eq!(
placed.rotation(),
Angle::QUARTER,
"and inherited the turn itself"
);
}
#[test]
fn the_composition_formula_is_pinned_by_hand() {
let mut world = World::new();
let hub = world.node(Local::new(at(10, 0), Angle::from_degrees(90)));
let arm = world.node(Local::new(at(2, 0), Angle::from_degrees(90)));
world.attach(arm, hub);
let tip = world.node(Local::new(at(1, 0), Angle::ZERO));
world.attach(tip, arm);
world.run();
assert_eq!(world.placement(hub).translation(), at(10, 0));
assert_eq!(world.placement(hub).rotation(), Angle::from_degrees(90));
assert_eq!(world.placement(arm).translation(), at(10, 2));
assert_eq!(world.placement(arm).rotation(), Angle::from_degrees(180));
assert_eq!(world.placement(tip).translation(), at(9, 2));
assert_eq!(world.placement(tip).rotation(), Angle::from_degrees(180));
}
#[test]
fn a_chain_of_rotations_adds_exactly_and_drifts_not_at_all() {
let mut world = World::new();
let step = Angle::from_degrees(3);
let mut previous = world.node(Local::new(Vec2::ZERO, step));
for _ in 0..119 {
let next = world.node(Local::new(Vec2::ZERO, step));
world.attach(next, previous);
previous = next;
}
world.run();
let summed = Angle::from_bits(step.to_bits().wrapping_mul(120));
assert_eq!(world.placement(previous).rotation(), summed);
let mut world = World::new();
let quarter = Angle::from_degrees(90);
let mut previous = world.node(Local::new(Vec2::ZERO, quarter));
for _ in 0..3 {
let next = world.node(Local::new(Vec2::ZERO, quarter));
world.attach(next, previous);
previous = next;
}
world.run();
assert_eq!(world.placement(previous).rotation(), Angle::ZERO);
}
#[test]
fn slot_order_does_not_decide() {
let mut world = World::new();
let scratch: Vec<_> = (0..3).map(|_| world.entities.spawn()).collect();
for entity in scratch {
world.entities.despawn(entity);
}
let parent = world.node(Local::new(at(100, 0), Angle::ZERO));
let child = world.node(Local::new(at(5, 0), Angle::ZERO));
world.attach(child, parent);
assert!(
child.index() < parent.index(),
"premise: the child must hold the lower slot, or this test proves nothing \
(child {}, parent {})",
child.index(),
parent.index()
);
world.run();
assert_eq!(
world.placement(child).translation(),
at(105, 0),
"the child composed against its parent's placement from this tick"
);
world.run();
assert_eq!(world.placement(child).translation(), at(105, 0));
}
#[test]
fn a_dead_parent_leaves_an_orphan_and_never_a_stand_in() {
let mut world = World::new();
let parent = world.node(Local::new(at(100, 0), Angle::ZERO));
let child = world.node(Local::new(at(5, 0), Angle::ZERO));
world.attach(child, parent);
world.run();
assert_eq!(world.placement(child).translation(), at(105, 0));
world.entities.despawn(parent);
let counts = world.run();
assert_eq!(
world.placement(child).translation(),
at(5, 0),
"the child fell back to the world"
);
assert_eq!(counts.orphaned, 1);
assert_eq!(
counts.roots, 0,
"an orphan is not a root, and is counted apart"
);
let stand_in = world.node(Local::new(at(-70, 0), Angle::ZERO));
assert_eq!(
stand_in.index(),
parent.index(),
"premise: the slot must really be reused, or this proves nothing"
);
let counts = world.run();
assert_eq!(
world.placement(child).translation(),
at(5, 0),
"the child did not adopt the slot's new tenant"
);
assert_eq!(counts.orphaned, 1);
}
#[test]
fn a_parent_that_is_not_a_scene_node_orphans_its_child() {
let mut world = World::new();
let bare = world.entities.spawn();
let child = world.node(Local::new(at(5, 0), Angle::ZERO));
world.attach(child, bare);
let counts = world.run();
assert_eq!(world.placement(child).translation(), at(5, 0));
assert_eq!(counts.orphaned, 1);
assert_eq!(
counts.nodes, 1,
"the bare entity is not a node and is not placed"
);
}
#[test]
fn a_cycle_terminates_and_is_counted_rather_than_hanging() {
let mut world = World::new();
let first = world.node(Local::new(at(1, 0), Angle::ZERO));
let second = world.node(Local::new(at(2, 0), Angle::ZERO));
let third = world.node(Local::new(at(4, 0), Angle::ZERO));
world.attach(first, third);
world.attach(second, first);
world.attach(third, second);
let counts = world.run();
assert_eq!(counts.nodes, 3, "totality holds even inside a loop");
assert_eq!(counts.cyclic, 1, "the loop is cut in exactly one place");
assert_eq!(counts.roots, 0);
assert_eq!(world.placement(second).translation(), at(2, 0));
assert_eq!(world.placement(third).translation(), at(6, 0));
assert_eq!(world.placement(first).translation(), at(7, 0));
}
#[test]
fn a_node_parented_to_itself_is_a_loop_of_one_not_an_orphan() {
let mut world = World::new();
let ouroboros = world.node(Local::new(at(9, 0), Angle::ZERO));
world.attach(ouroboros, ouroboros);
let counts = world.run();
assert_eq!(counts.cyclic, 1);
assert_eq!(counts.orphaned, 0);
assert_eq!(world.placement(ouroboros).translation(), at(9, 0));
}
#[test]
fn every_local_gets_a_global_whatever_else_is_wrong() {
let mut world = World::new();
let root = world.node(Local::new(at(1, 1), Angle::ZERO));
let child = world.node(Local::new(at(1, 0), Angle::ZERO));
world.attach(child, root);
let doomed = world.node(Local::new(at(2, 2), Angle::ZERO));
let orphan = world.node(Local::new(at(3, 3), Angle::ZERO));
world.attach(orphan, doomed);
world.entities.despawn(doomed);
let looped = world.node(Local::new(at(4, 4), Angle::ZERO));
world.attach(looped, looped);
let counts = world.run();
for entity in [root, child, orphan, looped] {
assert!(
world.globals.get(entity.index()).is_some(),
"slot {} went unplaced",
entity.index()
);
}
assert_eq!(counts.nodes, 4);
assert_eq!(counts.roots, 1);
assert_eq!(counts.orphaned, 1);
assert_eq!(counts.cyclic, 1);
}
#[test]
fn the_scratch_carries_capacity_and_never_answers() {
let mut used = World::new();
let deep = used.node(Local::new(at(1, 0), Angle::from_degrees(30)));
let deeper = used.node(Local::new(at(1, 0), Angle::from_degrees(30)));
used.attach(deeper, deep);
used.run();
let mut fresh = World::new();
let lone = fresh.node(Local::new(at(7, 8), Angle::from_degrees(45)));
let first = propagate(
&mut fresh.scratch,
&fresh.entities,
&fresh.parents,
&fresh.locals,
&mut fresh.globals,
);
let expected = fresh.placement(lone);
let second = propagate(
&mut used.scratch,
&fresh.entities,
&fresh.parents,
&fresh.locals,
&mut fresh.globals,
);
assert_eq!(first, second);
assert_eq!(fresh.placement(lone), expected);
}
#[test]
fn overflow_saturates_and_is_counted() {
let mut world = World::new();
let huge = Vec2::new(Fixed::MAX, Fixed::ZERO);
let base = world.node(Local::new(huge, Angle::ZERO));
let further = world.node(Local::new(huge, Angle::ZERO));
world.attach(further, base);
let before = renew_fixed::saturations();
world.run();
let after = renew_fixed::saturations();
assert!(
after.0 > before.0,
"the overflow must be reported, not absorbed"
);
assert_eq!(
world.placement(further).translation().x,
Fixed::MAX,
"and clamped rather than wrapped to a negative"
);
}
#[test]
fn nothing_in_produces_nothing_out() {
let mut world = World::new();
assert_eq!(world.run(), Propagated::default());
}