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::Shape;
248 use crate::Signal;
249 use crate::{
250 Color, DrawCommand, Easing, Keyframes, PixelSize, Rect, Size, TimeLiteral, ValidationLimits,
251 };
252 use std::f64::consts::FRAC_PI_2;
253
254 fn transform(node: &Node) -> Transform {
255 let list = node
256 .compile(
257 Size::new(64.0, 64.0),
258 PixelSize::new(64, 64),
259 ValidationLimits::default(),
260 )
261 .unwrap();
262 list.commands
263 .iter()
264 .find_map(|command| match command {
265 DrawCommand::FillRect { transform, .. } => Some(*transform),
266 _ => None,
267 })
268 .unwrap()
269 }
270
271 fn close(actual: (f64, f64), expected: (f64, f64)) {
272 assert!(
273 (actual.0 - expected.0).abs() < 0.0001 && (actual.1 - expected.1).abs() < 0.0001,
274 "{actual:?} != {expected:?}"
275 );
276 }
277
278 #[test]
279 fn child_pivots_inherit_rotation_and_translation_but_not_sibling_pose()
280 -> Result<(), ValidationError> {
281 let rig = Skeleton::new([
282 Bone::new(None, (5.0, 7.0)),
283 Bone::new(Some(0), (10.0, 0.0)),
284 Bone::new(Some(1), (4.0, 0.0)),
285 Bone::new(Some(0), (0.0, 3.0)),
286 ])?;
287 let mut pose = Pose::default();
288 pose.bones[0] = BonePose {
289 translation: (2.0, 1.0),
290 rotation: FRAC_PI_2,
291 };
292 pose.bones[1] = BonePose {
293 translation: (2.0, 0.0),
294 rotation: -FRAC_PI_2,
295 };
296 let solved = rig.solve(&pose)?;
297 close(solved[0].position(), (7.0, 8.0));
298 close(solved[1].position(), (7.0, 20.0));
299 close(solved[2].position(), (11.0, 20.0));
300 close(solved[3].position(), (4.0, 8.0));
301 close(solved[1].point((3.0, 0.0)), (10.0, 20.0));
302 assert!(solved[1].rotation().abs() < 0.0001);
303 Ok(())
304 }
305
306 #[test]
307 fn pose_tracks_are_repeatable_and_keep_unwrapped_turns() -> Result<(), ValidationError> {
308 let rig = Skeleton::new([Bone::new(None, (0.0, 0.0)), Bone::new(Some(0), (10.0, 0.0))])?;
309 let start = Pose::default();
310 let mut end = start.clone();
311 end.bones[0].rotation = std::f64::consts::TAU;
312 let track = Keyframes::new(start.clone()).to(1.seconds(), end.clone(), Easing::Linear);
313 let time = 250.milliseconds().after_origin();
314 let first = rig.solve(&track.sample(time))?;
315 close(first[1].position(), (0.0, 10.0));
316 let _later = rig.solve(&track.sample(900.milliseconds().after_origin()))?;
317 assert_eq!(first, rig.solve(&track.sample(time))?);
318 assert_eq!(track.sample(crate::Time::ZERO), start);
319 assert_eq!(track.sample(1.seconds().after_origin()), end);
320 assert_eq!(
321 start.interpolate(&end, 1.5).bones[0].rotation,
322 std::f64::consts::TAU * 1.5
323 );
324 Ok(())
325 }
326
327 #[test]
328 fn hierarchy_channels_and_overflow_are_validated() -> Result<(), ValidationError> {
329 assert!(Skeleton::new([Bone::new(Some(0), (0.0, 0.0))]).is_err());
330 assert!(
331 Skeleton::new([
332 Bone::new(Some(1), (0.0, 0.0)),
333 Bone::new(Some(0), (0.0, 0.0))
334 ])
335 .is_err()
336 );
337 assert!(Skeleton::new([Bone::new(None, (f64::NAN, 0.0))]).is_err());
338 let rig = Skeleton::new([Bone::new(None, (0.0, 0.0))])?;
339 for invalid_value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
340 let mut pose = Pose::default();
341 pose.bones[0].rotation = invalid_value;
342 assert!(rig.solve(&pose).is_err());
343 pose.bones[0] = BonePose {
344 translation: (invalid_value, 0.0),
345 rotation: 0.0,
346 };
347 assert!(rig.solve(&pose).is_err());
348 }
349 let large = Skeleton::new([
350 Bone::new(None, (f64::MAX, 0.0)),
351 Bone::new(Some(0), (f64::MAX, 0.0)),
352 ])?;
353 assert!(large.solve(&Pose::default()).is_err());
354 let mut pose = Pose::default();
355 pose.bones[0].rotation = f64::MAX;
356 pose.bones[1].rotation = f64::MAX;
357 let rotations =
358 Skeleton::new([Bone::new(None, (0.0, 0.0)), Bone::new(Some(0), (0.0, 0.0))])?;
359 assert!(rotations.solve(&pose).is_err());
360 let mut translated = Pose::default();
361 translated.bones[0].translation = (f64::MAX, 0.0);
362 assert!(large.solve(&translated).is_err());
363 Ok(())
364 }
365
366 #[test]
367 fn attachments_preserve_artwork_local_transforms() -> Result<(), ValidationError> {
368 let rig = Skeleton::new([Bone::new(None, (20.0, 30.0))])?;
369 let pose = Pose::new([BonePose {
370 translation: (2.0, 3.0),
371 rotation: FRAC_PI_2,
372 }]);
373 let joint = rig.solve(&pose)?[0];
374 let artwork = Node::from(Rect::new(0.0, 0.0, 10.0, 4.0).fill(Color::BLACK))
375 .pivot(Pivot::Point(Point::new(3.0, 2.0)))
376 .scale_xy(-1.2, 0.8)
377 .rotate(0.3)
378 .translate(7.0, 9.0);
379 let point = (4.0, 2.0);
380 let local = transform(&artwork).apply(Point::new(point.0, point.1));
381 let expected = joint.point((local.x, local.y));
382 let attached = joint.apply(artwork);
383 let actual = transform(&attached).apply(Point::new(point.0, point.1));
384 close((actual.x, actual.y), expected);
385 Ok(())
386 }
387
388 #[test]
389 fn independent_roots_empty_rigs_and_artwork_are_supported() -> Result<(), ValidationError> {
390 let rig = Skeleton::new([Bone::new(None, (2.0, 3.0)), Bone::new(None, (8.0, 9.0))])?;
391 let solved = rig.solve(&Pose::default())?;
392 close(solved[0].position(), (2.0, 3.0));
393 close(solved[1].position(), (8.0, 9.0));
394 let artwork = Node::from(Rect::new(0.0, 0.0, 10.0, 4.0).fill(Color::BLACK))
395 .rotate(0.5)
396 .translate(2.0, 3.0);
397 assert!(
398 solved[1]
399 .apply(artwork)
400 .compile(
401 Size::new(64.0, 64.0),
402 PixelSize::new(64, 64),
403 ValidationLimits::default()
404 )
405 .is_ok()
406 );
407 let empty = Skeleton::new([])?;
408 assert_eq!(empty.solve(&Pose::default())?, []);
409 Ok(())
410 }
411}