1use crate::{Interpolate, Node, NodeModifiers, Pivot, Point, Transform, ValidationError};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct Bone {
12 parent: Option<usize>,
13 offset: (f64, f64),
14}
15
16impl Bone {
17 pub fn new(parent: Option<usize>, offset: (f64, f64)) -> Self {
21 Self { parent, offset }
22 }
23}
24
25#[derive(Clone, Copy, Debug, Default, PartialEq)]
35pub struct BonePose {
36 pub translation: (f64, f64),
38 pub rotation: f64,
40}
41
42impl Interpolate for BonePose {
43 fn interpolate(&self, target: &Self, progress: f64) -> Self {
44 Self {
45 translation: self.translation.interpolate(&target.translation, progress),
46 rotation: self.rotation.interpolate(&target.rotation, progress),
47 }
48 }
49}
50
51#[derive(Clone, Debug, PartialEq)]
58pub struct Pose<const BONES: usize> {
59 pub bones: [BonePose; BONES],
61}
62
63impl<const BONES: usize> Pose<BONES> {
64 pub fn new(bones: [BonePose; BONES]) -> Self {
66 Self { bones }
67 }
68}
69
70impl<const BONES: usize> Default for Pose<BONES> {
71 fn default() -> Self {
72 Self::new([BonePose::default(); BONES])
73 }
74}
75
76impl<const BONES: usize> Interpolate for Pose<BONES> {
77 fn interpolate(&self, target: &Self, progress: f64) -> Self {
78 Self::new(std::array::from_fn(|index| {
79 self.bones[index].interpolate(&target.bones[index], progress)
80 }))
81 }
82}
83
84#[derive(Clone, Debug)]
112pub struct Skeleton<const BONES: usize> {
113 bones: [Bone; BONES],
114}
115
116impl<const BONES: usize> Skeleton<BONES> {
117 pub fn new(bones: [Bone; BONES]) -> Result<Self, ValidationError> {
124 for (index, bone) in bones.iter().enumerate() {
125 if bone.parent.is_some_and(|parent| parent >= index)
126 || ![bone.offset.0, bone.offset.1]
127 .iter()
128 .all(|value| value.is_finite())
129 {
130 return Err(invalid(
131 "Skeleton requires finite offsets and parent-first bone indices",
132 ));
133 }
134 }
135 Ok(Self { bones })
136 }
137
138 pub fn solve(&self, pose: &Pose<BONES>) -> Result<[BoneTransform; BONES], ValidationError> {
149 let mut solved = [BoneTransform {
150 position: (0.0, 0.0),
151 rotation: 0.0,
152 }; BONES];
153 for (index, bone) in self.bones.iter().enumerate() {
154 let channel = pose.bones[index];
155 if ![
156 channel.translation.0,
157 channel.translation.1,
158 channel.rotation,
159 ]
160 .iter()
161 .all(|value| value.is_finite())
162 {
163 return Err(invalid("Bone pose channels must be finite"));
164 }
165 let local = (
166 bone.offset.0 + channel.translation.0,
167 bone.offset.1 + channel.translation.1,
168 );
169 let joint = if let Some(parent) = bone.parent {
170 BoneTransform {
171 position: solved[parent].point(local),
172 rotation: solved[parent].rotation + channel.rotation,
173 }
174 } else {
175 BoneTransform {
176 position: local,
177 rotation: channel.rotation,
178 }
179 };
180 if ![
181 local.0,
182 local.1,
183 joint.position.0,
184 joint.position.1,
185 joint.rotation,
186 ]
187 .iter()
188 .all(|value| value.is_finite())
189 {
190 return Err(invalid("Bone pose produces overflowing joint transforms"));
191 }
192 solved[index] = joint;
193 }
194 Ok(solved)
195 }
196}
197
198#[derive(Clone, Copy, Debug, PartialEq)]
203pub struct BoneTransform {
204 position: (f64, f64),
205 rotation: f64,
206}
207
208impl BoneTransform {
209 pub fn position(self) -> (f64, f64) {
211 self.position
212 }
213
214 pub fn rotation(self) -> f64 {
216 self.rotation
217 }
218
219 pub fn point(self, point: (f64, f64)) -> (f64, f64) {
223 let point = Transform::rotate(self.rotation)
224 .then(Transform::translate(self.position.0, self.position.1))
225 .apply(Point::new(point.0, point.1));
226 (point.x, point.y)
227 }
228
229 pub fn apply(self, artwork: impl Into<Node>) -> Node {
233 Node::group([artwork.into()])
234 .pivot(Pivot::Point(Point::ORIGIN))
235 .rotate(self.rotation)
236 .translate(self.position.0, self.position.1)
237 }
238}
239
240fn invalid(message: &str) -> ValidationError {
241 ValidationError::invalid("skeleton", "input", message)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::Animation;
248 use crate::{
249 Color, DrawCommand, Easing, Keyframes, PixelSize, Rect, Size, TimeLiteral, ValidationLimits,
250 };
251 use std::f64::consts::FRAC_PI_2;
252
253 fn transform(node: &Node) -> Transform {
254 let list = node
255 .compile(
256 Size::new(64.0, 64.0),
257 PixelSize::new(64, 64),
258 ValidationLimits::default(),
259 )
260 .unwrap();
261 list.commands
262 .iter()
263 .find_map(|command| match command {
264 DrawCommand::FillRect { transform, .. } => Some(*transform),
265 _ => None,
266 })
267 .unwrap()
268 }
269
270 fn close(actual: (f64, f64), expected: (f64, f64)) {
271 assert!(
272 (actual.0 - expected.0).abs() < 0.0001 && (actual.1 - expected.1).abs() < 0.0001,
273 "{actual:?} != {expected:?}"
274 );
275 }
276
277 #[test]
278 fn child_pivots_inherit_rotation_and_translation_but_not_sibling_pose()
279 -> Result<(), ValidationError> {
280 let rig = Skeleton::new([
281 Bone::new(None, (5.0, 7.0)),
282 Bone::new(Some(0), (10.0, 0.0)),
283 Bone::new(Some(1), (4.0, 0.0)),
284 Bone::new(Some(0), (0.0, 3.0)),
285 ])?;
286 let mut pose = Pose::default();
287 pose.bones[0] = BonePose {
288 translation: (2.0, 1.0),
289 rotation: FRAC_PI_2,
290 };
291 pose.bones[1] = BonePose {
292 translation: (2.0, 0.0),
293 rotation: -FRAC_PI_2,
294 };
295 let solved = rig.solve(&pose)?;
296 close(solved[0].position(), (7.0, 8.0));
297 close(solved[1].position(), (7.0, 20.0));
298 close(solved[2].position(), (11.0, 20.0));
299 close(solved[3].position(), (4.0, 8.0));
300 close(solved[1].point((3.0, 0.0)), (10.0, 20.0));
301 assert!(solved[1].rotation().abs() < 0.0001);
302 Ok(())
303 }
304
305 #[test]
306 fn pose_tracks_are_repeatable_and_keep_unwrapped_turns() -> Result<(), ValidationError> {
307 let rig = Skeleton::new([Bone::new(None, (0.0, 0.0)), Bone::new(Some(0), (10.0, 0.0))])?;
308 let start = Pose::default();
309 let mut end = start.clone();
310 end.bones[0].rotation = std::f64::consts::TAU;
311 let track = Keyframes::new(start.clone()).to(1.seconds(), end.clone(), Easing::Linear);
312 let time = 250.milliseconds();
313 let first = rig.solve(&track.sample(time))?;
314 close(first[1].position(), (0.0, 10.0));
315 let _later = rig.solve(&track.sample(900.milliseconds()))?;
316 assert_eq!(first, rig.solve(&track.sample(time))?);
317 assert_eq!(track.sample(crate::Time::ZERO), start);
318 assert_eq!(track.sample(1.seconds()), end);
319 assert_eq!(
320 start.interpolate(&end, 1.5).bones[0].rotation,
321 std::f64::consts::TAU * 1.5
322 );
323 Ok(())
324 }
325
326 #[test]
327 fn hierarchy_channels_and_overflow_are_validated() -> Result<(), ValidationError> {
328 assert!(Skeleton::new([Bone::new(Some(0), (0.0, 0.0))]).is_err());
329 assert!(
330 Skeleton::new([
331 Bone::new(Some(1), (0.0, 0.0)),
332 Bone::new(Some(0), (0.0, 0.0))
333 ])
334 .is_err()
335 );
336 assert!(Skeleton::new([Bone::new(None, (f64::NAN, 0.0))]).is_err());
337 let rig = Skeleton::new([Bone::new(None, (0.0, 0.0))])?;
338 for invalid_value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
339 let mut pose = Pose::default();
340 pose.bones[0].rotation = invalid_value;
341 assert!(rig.solve(&pose).is_err());
342 pose.bones[0] = BonePose {
343 translation: (invalid_value, 0.0),
344 rotation: 0.0,
345 };
346 assert!(rig.solve(&pose).is_err());
347 }
348 let large = Skeleton::new([
349 Bone::new(None, (f64::MAX, 0.0)),
350 Bone::new(Some(0), (f64::MAX, 0.0)),
351 ])?;
352 assert!(large.solve(&Pose::default()).is_err());
353 let mut pose = Pose::default();
354 pose.bones[0].rotation = f64::MAX;
355 pose.bones[1].rotation = f64::MAX;
356 let rotations =
357 Skeleton::new([Bone::new(None, (0.0, 0.0)), Bone::new(Some(0), (0.0, 0.0))])?;
358 assert!(rotations.solve(&pose).is_err());
359 let mut translated = Pose::default();
360 translated.bones[0].translation = (f64::MAX, 0.0);
361 assert!(large.solve(&translated).is_err());
362 Ok(())
363 }
364
365 #[test]
366 fn attachments_preserve_artwork_local_transforms() -> Result<(), ValidationError> {
367 let rig = Skeleton::new([Bone::new(None, (20.0, 30.0))])?;
368 let pose = Pose::new([BonePose {
369 translation: (2.0, 3.0),
370 rotation: FRAC_PI_2,
371 }]);
372 let joint = rig.solve(&pose)?[0];
373 let artwork = Node::from(Rect::new(0.0, 0.0, 10.0, 4.0).fill(Color::BLACK))
374 .pivot(Pivot::Point(Point::new(3.0, 2.0)))
375 .scale_xy(-1.2, 0.8)
376 .rotate(0.3)
377 .translate(7.0, 9.0);
378 let point = (4.0, 2.0);
379 let local = transform(&artwork).apply(Point::new(point.0, point.1));
380 let expected = joint.point((local.x, local.y));
381 let attached = joint.apply(artwork);
382 let actual = transform(&attached).apply(Point::new(point.0, point.1));
383 close((actual.x, actual.y), expected);
384 Ok(())
385 }
386
387 #[test]
388 fn independent_roots_empty_rigs_and_artwork_are_supported() -> Result<(), ValidationError> {
389 let rig = Skeleton::new([Bone::new(None, (2.0, 3.0)), Bone::new(None, (8.0, 9.0))])?;
390 let solved = rig.solve(&Pose::default())?;
391 close(solved[0].position(), (2.0, 3.0));
392 close(solved[1].position(), (8.0, 9.0));
393 let artwork = Node::from(Rect::new(0.0, 0.0, 10.0, 4.0).fill(Color::BLACK))
394 .rotate(0.5)
395 .translate(2.0, 3.0);
396 assert!(
397 solved[1]
398 .apply(artwork)
399 .compile(
400 Size::new(64.0, 64.0),
401 PixelSize::new(64, 64),
402 ValidationLimits::default()
403 )
404 .is_ok()
405 );
406 let empty = Skeleton::new([])?;
407 assert_eq!(empty.solve(&Pose::default())?, []);
408 Ok(())
409 }
410}