use glam::Vec2;
use rayon::prelude::*;
use crate::{Ctx, Entity, Transform2D, World};
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[repr(C)] pub struct Attached {
pub to: Entity,
pub offset: Vec2,
pub rotation: f32,
}
impl Attached {
#[must_use]
pub fn to(to: Entity) -> Self {
Self {
to,
offset: Vec2::ZERO,
rotation: 0.0,
}
}
#[must_use]
pub fn offset(self, offset: Vec2) -> Self {
Self { offset, ..self }
}
#[must_use]
pub fn rotated(self, rotation: f32) -> Self {
Self { rotation, ..self }
}
}
const MAX_DEPTH: usize = 64;
#[derive(Default)]
struct Scratch {
attached: Vec<(Entity, Attached)>,
placed: Vec<Option<(Vec2, f32)>>,
}
pub fn follow_attachments(ctx: &mut Ctx) {
let mut s = ctx.world.remove_resource::<Scratch>().unwrap_or_default();
ctx.world
.query::<(Entity, &Attached)>()
.with::<Transform2D>()
.par_collect_into(&mut s.attached, |(e, a)| (e, *a));
if !s.attached.is_empty() {
let world = &ctx.world;
s.placed.clear();
s.attached
.par_iter()
.map(|&(_, a)| place(world, a))
.collect_into_vec(&mut s.placed);
let placed = &s.placed;
ctx.world
.query::<(&mut Transform2D, &Attached)>()
.par_for_each_indexed(|i, (t, _)| {
if let Some((position, rotation)) = placed[i] {
t.position = position;
t.rotation = rotation;
}
});
for (&(e, _), placed) in s.attached.iter().zip(&s.placed) {
if placed.is_none() {
ctx.commands.despawn(e);
}
}
}
ctx.world.insert_resource(s);
}
fn place(world: &World, a: Attached) -> Option<(Vec2, f32)> {
let mut links = [a; MAX_DEPTH];
let mut depth = 1;
let mut up = a.to;
while let Some(&next) = world.get::<Attached>(up) {
if depth == MAX_DEPTH {
return None;
}
links[depth] = next;
depth += 1;
up = next.to;
}
let root = world.get::<Transform2D>(up)?;
if !world.is_alive(up) {
return None;
}
let (mut position, mut rotation) = (root.position, root.rotation);
let mut scale = root.scale;
for i in (0..depth).rev() {
let link = links[i];
position += Vec2::from_angle(rotation).rotate(link.offset * scale);
rotation += link.rotation;
if i > 0 {
let this = links[i - 1].to;
scale = world.get::<Transform2D>(this).map_or(Vec2::ONE, |t| t.scale);
}
}
Some((position, rotation))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vec2;
#[test]
fn chains_compose_from_the_root_down() {
let mut w = World::new();
let root = w.spawn((Transform2D::at(vec2(10.0, 0.0)).rotated(std::f32::consts::FRAC_PI_2),));
let arm = w.spawn((Transform2D::default(), Attached::to(root).offset(vec2(1.0, 0.0))));
let hand = Attached::to(arm).offset(vec2(1.0, 0.0));
let (p, r) = place(&w, hand).unwrap();
assert!((p - vec2(10.0, 2.0)).length() < 1e-5, "{p}");
assert!((r - std::f32::consts::FRAC_PI_2).abs() < 1e-6);
}
#[test]
fn loops_and_missing_roots_place_nothing() {
let mut w = World::new();
let a = w.spawn((Transform2D::default(),));
let b = w.spawn((Transform2D::default(), Attached::to(a)));
w.insert(a, Attached::to(b)); assert_eq!(place(&w, Attached::to(a)), None);
let gone = w.spawn((Transform2D::default(),));
w.despawn(gone);
assert_eq!(place(&w, Attached::to(gone)), None);
}
}