From 89448006ae71c01de7e1fe0ea957bcb32e42ddd6 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Mon, 2 Sep 2024 10:36:43 -0700 Subject: [PATCH] Skeleton T-pose + Angle vs Rotation + more --- Cargo.lock | 4 +- examples/skeleton.rs | 43 +++++----- src/animation.rs | 10 +-- src/cushy.rs | 22 +++-- src/cushy/skeleton_canvas.rs | 4 +- src/funnybones.rs | 142 +++++++++++++++----------------- src/lib.rs | 154 +++++++++++++++++++++-------------- src/serde.rs | 8 +- 8 files changed, 208 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a74e488..324c3a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,7 +631,7 @@ checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "cushy" version = "0.4.0" -source = "git+https://github.com/khonsulabs/cushy#0dd18826c903f4e87f69e6b671ac5456abc43116" +source = "git+https://github.com/khonsulabs/cushy#6a346ea3ef1e6b6b37767d8c61e0d6a9f8581422" dependencies = [ "ahash", "alot", @@ -658,7 +658,7 @@ dependencies = [ [[package]] name = "cushy-macros" version = "0.4.0" -source = "git+https://github.com/khonsulabs/cushy#0dd18826c903f4e87f69e6b671ac5456abc43116" +source = "git+https://github.com/khonsulabs/cushy#6a346ea3ef1e6b6b37767d8c61e0d6a9f8581422" dependencies = [ "attribute-derive", "manyhow", diff --git a/examples/skeleton.rs b/examples/skeleton.rs index f4bd50e..e75b030 100644 --- a/examples/skeleton.rs +++ b/examples/skeleton.rs @@ -17,11 +17,12 @@ use cushy::{ widgets::{slider::Slidable, Canvas}, Run, }; -use funnybones::{Angle, BoneId, BoneKind, Joint, JointId, Skeleton, Vector}; +use funnybones::{Angle, BoneId, BoneKind, Joint, JointId, Rotation, Skeleton, Vector}; fn main() { // begin rustme snippet: readme let mut skeleton = Skeleton::default(); + skeleton.set_rotation(Rotation::degrees(-90.)); // Create our root bone: the spine let spine = skeleton.push_bone(BoneKind::Rigid { length: 3. }.with_label("spine")); @@ -29,7 +30,7 @@ fn main() { let r_hip = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("r_hip")); // Connect the right hip to the spine. skeleton.push_joint(Joint::new( - Angle::degrees(-90.), + Rotation::degrees(90.), spine.axis_a(), r_hip.axis_a(), )); @@ -46,7 +47,7 @@ fn main() { // Connect the right leg to the right hip. skeleton.push_joint(Joint::new( - Angle::degrees(0.), + Rotation::degrees(-90.), r_hip.axis_b(), r_leg.axis_a(), )); @@ -54,7 +55,7 @@ fn main() { let r_foot = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("r_foot")); // Connect the right foot to the right leg. let r_ankle_id = skeleton.push_joint(Joint::new( - Angle::degrees(90.), + Rotation::degrees(0.), r_leg.axis_b(), r_foot.axis_a(), )); @@ -63,7 +64,7 @@ fn main() { // Create the left-half of our lower half. let l_hip = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("l_hip")); skeleton.push_joint(Joint::new( - Angle::degrees(90.), + Rotation::degrees(-90.), spine.axis_a(), l_hip.axis_a(), )); @@ -76,13 +77,13 @@ fn main() { .with_label("l_leg"), ); skeleton.push_joint(Joint::new( - Angle::degrees(90.), + Rotation::degrees(90.), l_hip.axis_b(), l_leg.axis_a(), )); let l_foot = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("l_foot")); let l_ankle_id = skeleton.push_joint(Joint::new( - Angle::degrees(-90.), + Rotation::degrees(0.), l_leg.axis_b(), l_foot.axis_a(), )); @@ -90,7 +91,7 @@ fn main() { // Create our two arms in the same fashion as our leg structure. let r_shoulder = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("r_shoulder")); skeleton.push_joint(Joint::new( - Angle::degrees(-90.), + Rotation::degrees(-90.), spine.axis_b(), r_shoulder.axis_a(), )); @@ -103,20 +104,20 @@ fn main() { .with_label("r_arm"), ); skeleton.push_joint(Joint::new( - Angle::degrees(-90.), + Rotation::degrees(0.), r_shoulder.axis_b(), r_arm.axis_a(), )); let r_hand = skeleton.push_bone(BoneKind::Rigid { length: 0.3 }.with_label("r_hand")); let r_wrist_id = skeleton.push_joint(Joint::new( - Angle::degrees(175.), + Rotation::degrees(0.), r_arm.axis_b(), r_hand.axis_a(), )); let l_shoulder = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("l_shoulder")); skeleton.push_joint(Joint::new( - Angle::degrees(90.), + Rotation::degrees(90.), spine.axis_b(), l_shoulder.axis_a(), )); @@ -129,13 +130,13 @@ fn main() { .with_label("l_arm"), ); skeleton.push_joint(Joint::new( - Angle::degrees(90.), + Rotation::degrees(0.), l_shoulder.axis_b(), l_arm.axis_a(), )); let l_hand = skeleton.push_bone(BoneKind::Rigid { length: 0.3 }.with_label("l_hand")); let l_wrist_id = skeleton.push_joint(Joint::new( - Angle::degrees(-175.), + Rotation::degrees(0.), l_arm.axis_b(), l_hand.axis_a(), )); @@ -143,7 +144,7 @@ fn main() { // Finally, create a bone to represent our head. let head = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("head")); let neck = skeleton.push_joint(Joint::new( - Angle::degrees(180.), + Rotation::degrees(0.), spine.axis_b(), head.axis_a(), )); @@ -155,7 +156,7 @@ fn main() { .for_each_cloned({ let skeleton = skeleton.clone(); move |rotation| { - skeleton.lock().set_rotation(rotation); + skeleton.lock().set_rotation(rotation.into()); } }) .persist(); @@ -237,14 +238,14 @@ fn main() { .contain() .and(bone_widget( "Left Leg", - Angle::degrees(90.), + Angle::degrees(0.), &skeleton, l_leg, )) .and(joint_widget("Left Ankle", &skeleton, l_ankle_id)) .and(bone_widget( "Right Leg", - Angle::degrees(-90.), + Angle::degrees(0.), &skeleton, r_leg, )) @@ -285,7 +286,7 @@ fn joint_widget(label: &str, skeleton: &Dynamic, joint: JointId) -> im } }) .persist(); - let angle_slider = angle.slider_between(Angle::degrees(0.), Angle::degrees(359.9)); + let angle_slider = angle.slider_between(Rotation::degrees(0.), Rotation::degrees(359.9)); label.and(angle_slider).into_rows().contain() } @@ -307,8 +308,10 @@ fn bone_widget( move |direction| { let mut skeleton = skeleton.lock(); let current_end = skeleton[bone].desired_end().unwrap_or_default(); - skeleton[bone] - .set_desired_end(Some(Vector::new(current_end.magnitude, *direction))); + skeleton[bone].set_desired_end(Some(Vector::new( + current_end.magnitude, + Rotation::from(*direction), + ))); } }) .persist(); diff --git a/src/animation.rs b/src/animation.rs index 31a3299..31ce3cf 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -11,7 +11,7 @@ use std::{ use easing_function::easings::StandardEasing; -use crate::{Angle, Bone, BoneId, Coordinate, Joint, JointId, Skeleton, Vector}; +use crate::{Rotation, Bone, BoneId, Coordinate, Joint, JointId, Skeleton, Vector}; #[derive(Default, Debug, PartialEq, Clone)] pub struct Animation(Arc); @@ -258,7 +258,7 @@ impl BoneProperty { match self { BoneProperty::Target => Value::Vector( bone.desired_end() - .unwrap_or_else(|| Vector::new(bone.kind().full_length(), Angle::default())), + .unwrap_or_else(|| Vector::new(bone.kind().full_length(), Rotation::default())), ), // BoneProperty::Scale => , BoneProperty::Inverse => Value::Bool(bone.kind().is_inverse()), @@ -303,7 +303,7 @@ impl JointProperty { let Value::Number(value) = value else { return; }; - joint.set_angle(Angle::radians(value)); + joint.set_angle(Rotation::radians(value)); } } } @@ -566,7 +566,7 @@ impl Lerp for Coordinate { } } -impl Lerp for Angle { +impl Lerp for Rotation { fn lerp(self, target: Self, percent: f32) -> Self { let delta_neg = self.radians - target.radians; let delta_pos = target.radians - self.radians; @@ -613,7 +613,7 @@ fn basic() { let mut skeleton = Skeleton::default(); let root = skeleton.push_bone(BoneKind::Rigid { length: 1. }); let arm = skeleton.push_bone(BoneKind::Rigid { length: 1. }); - let joint = skeleton.push_joint(Joint::new(Angle::default(), root.axis_b(), arm.axis_a())); + let joint = skeleton.push_joint(Joint::new(Rotation::default(), root.axis_b(), arm.axis_a())); let animation = Animation::default().with( Timeline::new(Target::Joint { diff --git a/src/cushy.rs b/src/cushy.rs index 87ec22e..73cc8cc 100644 --- a/src/cushy.rs +++ b/src/cushy.rs @@ -1,21 +1,21 @@ //! Widgets for editing and rendering skeletons. use cushy::{ - animation::{LinearInterpolate, PercentBetween}, + animation::{LinearInterpolate, PercentBetween, ZeroToOne}, figures::{IntoComponents, Ranged}, }; -use crate::{Angle, Coordinate, Vector}; +use crate::{Angle, Coordinate, Rotation, Vector}; pub mod skeleton_canvas; -impl PercentBetween for Angle { - fn percent_between(&self, min: &Self, max: &Self) -> cushy::animation::ZeroToOne { +impl PercentBetween for Rotation { + fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne { self.radians.percent_between(&min.radians, &max.radians) } } -impl LinearInterpolate for Angle { +impl LinearInterpolate for Rotation { fn lerp(&self, target: &Self, percent: f32) -> Self { Self { radians: self.radians.lerp(&target.radians, percent), @@ -48,3 +48,15 @@ impl Ranged for Angle { const MIN: Self = Self::MIN; const MAX: Self = Self::MAX; } + +impl PercentBetween for Angle { + fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne { + self.0.percent_between(&min.0, &max.0) + } +} + +impl LinearInterpolate for Angle { + fn lerp(&self, target: &Self, percent: f32) -> Self { + Self(self.0.lerp(&target.0, percent).clamped()) + } +} diff --git a/src/cushy/skeleton_canvas.rs b/src/cushy/skeleton_canvas.rs index 6169c0a..477c534 100644 --- a/src/cushy/skeleton_canvas.rs +++ b/src/cushy/skeleton_canvas.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] use core::f32; -use crate::{Angle, BoneEnd, BoneId, Coordinate, JointId, Skeleton, Vector}; +use crate::{Rotation, BoneEnd, BoneId, Coordinate, JointId, Skeleton, Vector}; use cushy::{ context::{EventContext, GraphicsContext, LayoutContext, Trackable}, figures::{ @@ -430,5 +430,5 @@ struct DragInfo { #[derive(Clone, Copy, Debug, PartialEq)] pub enum SkeletonMutation { SetDesiredEnd { bone: BoneId, end: Vector }, - SetJointRotation { joint: JointId, rotation: Angle }, + SetJointRotation { joint: JointId, rotation: Rotation }, } diff --git a/src/funnybones.rs b/src/funnybones.rs index 6a48ad1..9e6291b 100644 --- a/src/funnybones.rs +++ b/src/funnybones.rs @@ -4,14 +4,14 @@ use std::ops::ControlFlow; use cushy::{ animation::ZeroToOne, - value::{Destination, Dynamic, DynamicRead, Source, Switchable}, + value::{Destination, Dynamic, DynamicRead, Source, Switchable, Watcher}, widget::{MakeWidget, WidgetList}, widgets::{checkbox::Checkable, input::InputValue, slider::Slidable, Space}, Run, }; use funnybones::{ cushy::skeleton_canvas::{SkeletonCanvas, SkeletonMutation}, - Angle, BoneAxis, BoneId, BoneKind, Joint, JointId, LabeledBoneKind, Skeleton, Vector, + Angle, BoneAxis, BoneId, BoneKind, Joint, JointId, LabeledBoneKind, Rotation, Skeleton, Vector, }; #[derive(Default, Eq, PartialEq, Debug, Clone, Copy)] @@ -21,35 +21,6 @@ enum Mode { Animation, } -#[derive(Debug, Clone)] -struct ChangeAggregator(Dynamic); - -impl ChangeAggregator { - pub fn new(mut when_changed: F) -> (Self, Dynamic) - where - F: FnMut() -> T + Send + 'static, - T: PartialEq + Send + 'static, - { - let counter = Dynamic::new(0); - let result = counter.map_each(move |_| when_changed()); - - (Self(counter), result) - } - - pub fn watch(&self, other: &Dynamic) - where - T: Send + 'static, - { - let counter = self.0.clone(); - other - .for_each_subsequent_generational(move |guard| { - drop(guard); - *counter.lock() += 1; - }) - .persist(); - } -} - fn add_bones_to_skeleton( connected_to: BoneAxis, bones: &Dynamic>, @@ -58,11 +29,15 @@ fn add_bones_to_skeleton( let bones = bones.read(); for bone in &*bones { let (kind, vector) = bone.as_bone_kind(); + let angle = if let BoneKind::Jointed { .. } = &kind.kind { + Rotation::default() + } else { + bone.joint_angle.get().into() + }; let new_bone = skeleton.push_bone(kind); skeleton[new_bone].set_desired_end(Some(vector)); skeleton.push_joint( - Joint::new(bone.joint_angle.get(), connected_to, new_bone.axis_a()) - .with_label(bone.joint_label.get()), + Joint::new(angle, connected_to, new_bone.axis_a()).with_label(bone.joint_label.get()), ); add_bones_to_skeleton(new_bone.axis_b(), &bone.connected_bones, skeleton); } @@ -70,12 +45,15 @@ fn add_bones_to_skeleton( fn main() -> cushy::Result { let editing_skeleton = EditingSkeleton::default(); - let (watcher, skeleton) = ChangeAggregator::new({ + let watcher = Watcher::default(); + let skeleton = watcher.map_changed({ let editing_skeleton = editing_skeleton.clone(); move || { let mut skeleton = Skeleton::default(); + skeleton.set_rotation(editing_skeleton.root.joint_angle.get().into()); let (kind, _vector) = editing_skeleton.root.as_bone_kind(); let root = skeleton.push_bone(kind); + add_bones_to_skeleton( root.axis_b(), &editing_skeleton.root.connected_bones, @@ -93,13 +71,13 @@ fn main() -> cushy::Result { SkeletonMutation::SetDesiredEnd { bone, end } => { let bone = editing_skeleton.find_bone(bone).expect("missing bone"); bone.desired_length.set(end.magnitude); - bone.joint_angle.set(end.direction); + bone.joint_angle.set(end.direction.into()); } SkeletonMutation::SetJointRotation { joint, rotation } => editing_skeleton .find_joint(joint) .expect("missing joint") .joint_angle - .set(rotation), + .set(rotation.into()), } }); let zoom = canvas @@ -235,7 +213,7 @@ impl SkeletalBone { ( kind.with_label(self.label.get()), - Vector::new(vector_length, self.joint_angle.get()), + Vector::new(vector_length, self.joint_angle.get().into()), ) } } @@ -244,7 +222,7 @@ impl Default for SkeletalBone { fn default() -> Self { Self { joint_label: Dynamic::default(), - joint_angle: Dynamic::new(Angle::degrees(90.)), + joint_angle: Dynamic::default(), label: Dynamic::default(), length: Dynamic::new(1.), jointed: Dynamic::default(), @@ -256,7 +234,7 @@ impl Default for SkeletalBone { } } -fn skeleton_editor(skeleton: &EditingSkeleton, watcher: &ChangeAggregator) -> impl MakeWidget { +fn skeleton_editor(skeleton: &EditingSkeleton, watcher: &Watcher) -> impl MakeWidget { bone_property_editor(skeleton.root.clone(), watcher, true) .and(bones_editor( "Upper Root Bones", @@ -272,7 +250,7 @@ fn skeleton_editor(skeleton: &EditingSkeleton, watcher: &ChangeAggregator) -> im fn bones_editor( label: &str, bones: &Dynamic>, - watcher: &ChangeAggregator, + watcher: &Watcher, ) -> impl MakeWidget { watcher.watch(bones); let bone_editors = Dynamic::new( @@ -302,7 +280,7 @@ fn bones_editor( .collapsed(collapsed) .contain() } -fn bone_editor(bone: SkeletalBone, watcher: &ChangeAggregator) -> impl MakeWidget { +fn bone_editor(bone: SkeletalBone, watcher: &Watcher) -> impl MakeWidget { let bones = bones_editor("Connected Bones", &bone.connected_bones, watcher); bone_property_editor(bone, watcher, false) .and(bones) @@ -310,11 +288,7 @@ fn bone_editor(bone: SkeletalBone, watcher: &ChangeAggregator) -> impl MakeWidge } #[allow(clippy::too_many_lines)] -fn bone_property_editor( - bone: SkeletalBone, - watcher: &ChangeAggregator, - is_root: bool, -) -> impl MakeWidget { +fn bone_property_editor(bone: SkeletalBone, watcher: &Watcher, is_root: bool) -> impl MakeWidget { watcher.watch(&bone.joint_label); watcher.watch(&bone.inverse); watcher.watch(&bone.jointed); @@ -324,6 +298,8 @@ fn bone_property_editor( watcher.watch(&bone.joint_ratio); watcher.watch(&bone.desired_length); + let columns_wide = 3 + u8::from(!is_root); + bone.jointed .for_each_cloned({ let mut was_jointed = bone.jointed.get(); @@ -360,56 +336,66 @@ fn bone_property_editor( let rotation = bone.joint_angle.slider(); - let joint_row = "Second Bone Segment Length" + let joint_angle = if is_root { "Rotation" } else { "Joint Angle" } .align_left() - .and(bone.joint_ratio.slider().with_enabled(bone.jointed.clone())) + .and(rotation.clone()) .into_rows() - .fit_horizontally() - .align_top() .expand() + .make_widget(); + + let joint_row = joint_angle + .clone() + .and( + "Midpoint" + .align_left() + .and(bone.joint_ratio.slider().with_enabled(bone.jointed.clone())) + .into_rows() + .fit_horizontally() + .align_top() + .expand(), + ) .and( bone.inverse .into_checkbox("Inverse") .with_enabled(bone.jointed.clone()) .fit_horizontally(), ) - .and(Space::clear().expand_weighted(2)) + .and(Space::clear().expand_weighted(columns_wide - 3)) .into_columns() .make_widget(); - let non_joint_row = "Joint Angle" - .align_left() - .and(rotation) - .into_rows() - .expand() - .and(Space::clear().expand_weighted(4)) + let non_joint_row = joint_angle + .and(Space::clear().expand_weighted(columns_wide - 1)) .into_columns() .make_widget(); - let second_row = if is_root { - joint_row - .collapse_vertically(bone.jointed.map_each_cloned(|j| !j)) - .make_widget() + let second_row = bone + .jointed + .clone() + .switcher(move |jointed, _| { + if *jointed { + joint_row.clone() + } else { + non_joint_row.clone() + } + }) + .make_widget(); + + let first_row = if is_root { + WidgetList::new() } else { - bone.jointed - .clone() - .switcher(move |jointed, _| { - if *jointed { - joint_row.clone() - } else { - non_joint_row.clone() - } - }) - .make_widget() + WidgetList::new().and( + "Joint Name" + .align_left() + .and(joint_label_editor) + .into_rows() + .fit_horizontally() + .align_top() + .expand(), + ) }; - "Joint Name" - .align_left() - .and(joint_label_editor) - .into_rows() - .fit_horizontally() - .align_top() - .expand() + first_row .and( "Bone Name" .align_left() diff --git a/src/lib.rs b/src/lib.rs index c9acb2e..ea2d02d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,8 +50,8 @@ impl Coordinate { /// Returns the angle formed a line passing through 0,0 towards this vector. #[must_use] - pub fn as_rotation(self) -> Angle { - Angle::radians(self.y.atan2(self.x)) + pub fn as_rotation(self) -> Rotation { + Rotation::radians(self.y.atan2(self.x)) } /// Returns a vector pointing from `self` to `other`. @@ -105,25 +105,54 @@ impl Div for Coordinate { } } -/// A value representing a direction. -#[derive(Clone, Copy, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct Angle { - radians: f32, -} +/// A value representing a rotation between 0. and `2π`. +#[derive(Debug, PartialEq, Clone, Copy, PartialOrd, Default)] +pub struct Angle(Rotation); impl Angle { /// The minimum rotation represented by this type. - pub const MIN: Self = Self { radians: 0. }; + pub const MIN: Self = Self(Rotation { radians: 0. }); /// The maximum rotation represented by this type. - pub const MAX: Self = Self { + pub const MAX: Self = Self(Rotation { radians: PI * 2. - f32::EPSILON, - }; + }); - /// Returns a rotation representing the given radians. + /// Returns an angle representing the given radians. #[must_use] pub fn radians(radians: f32) -> Self { - Self { radians }.normalized() + Self(Rotation::radians(radians).clamped()) + } + + /// Returns an angle representing the given degrees. + #[must_use] + pub fn degrees(degrees: f32) -> Self { + Self::radians(degrees * PI / 180.0) + } +} + +impl From for Rotation { + fn from(value: Angle) -> Self { + value.0 + } +} +impl From for Angle { + fn from(value: Rotation) -> Self { + Self(value.clamped()) + } +} + +/// A value representing a rotation in 2d space. +#[derive(Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +pub struct Rotation { + radians: f32, +} + +impl Rotation { + /// Returns a rotation representing the given radians. + #[must_use] + pub const fn radians(radians: f32) -> Self { + Self { radians } } /// Returns a rotation representing the given degrees. @@ -133,24 +162,20 @@ impl Angle { } /// Returns this rotation represented in degrees. - /// - /// This value will always be greater than or equal to 0 and will always be - /// less than 360.0. #[must_use] pub fn to_degrees(self) -> f32 { self.radians * 180.0 / PI } /// Returns this rotation represented in radians. - /// - /// This value will always be greater than or equal to 0 and will always be - /// less than `2π`. #[must_use] pub const fn to_radians(self) -> f32 { self.radians } - fn normalized(mut self) -> Self { + /// Returns this angle constrained between 0 and `2π`. + #[must_use] + pub fn clamped(mut self) -> Self { const TWO_PI: f32 = PI * 2.0; while self.radians >= TWO_PI { self.radians -= TWO_PI; @@ -174,25 +199,25 @@ impl Angle { } } -impl Debug for Angle { +impl Debug for Rotation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } -impl Display for Angle { +impl Display for Rotation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}°", self.to_degrees()) } } -impl Default for Angle { +impl Default for Rotation { fn default() -> Self { Self { radians: 0. } } } -impl Add for Angle { +impl Add for Rotation { type Output = Self; fn add(self, rhs: Self) -> Self::Output { @@ -200,13 +225,13 @@ impl Add for Angle { } } -impl AddAssign for Angle { +impl AddAssign for Rotation { fn add_assign(&mut self, rhs: Self) { self.radians = (*self + rhs).radians; } } -impl Sub for Angle { +impl Sub for Rotation { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -214,13 +239,13 @@ impl Sub for Angle { } } -impl SubAssign for Angle { +impl SubAssign for Rotation { fn sub_assign(&mut self, rhs: Self) { self.radians = (*self - rhs).radians; } } -impl Neg for Angle { +impl Neg for Rotation { type Output = Self; fn neg(self) -> Self::Output { @@ -235,13 +260,13 @@ pub struct Vector { /// The length of the vector. pub magnitude: f32, /// The direction the vector is heading. - pub direction: Angle, + pub direction: Rotation, } impl Vector { /// Returns a new vector for the given magnitude and direction. #[must_use] - pub const fn new(magnitude: f32, direction: Angle) -> Self { + pub const fn new(magnitude: f32, direction: Rotation) -> Self { Self { magnitude, direction, @@ -291,19 +316,19 @@ impl Sub for Coordinate { } } -impl Add for Vector { +impl Add for Vector { type Output = Self; - fn add(mut self, rhs: Angle) -> Self::Output { + fn add(mut self, rhs: Rotation) -> Self::Output { self.direction += rhs; self } } -impl Sub for Vector { +impl Sub for Vector { type Output = Self; - fn sub(mut self, rhs: Angle) -> Self::Output { + fn sub(mut self, rhs: Rotation) -> Self::Output { self.direction -= rhs; self } @@ -417,7 +442,7 @@ impl Borrow for ArcString { #[derive(Default, Debug, PartialEq)] pub struct Skeleton { bones: Vec, - rotation: Angle, + rotation: Rotation, joints: Vec, connections: HashMap>, generation: usize, @@ -452,7 +477,7 @@ impl Skeleton { joint_pos: None, end: Coordinate::default(), desired_end: None, - entry_angle: Angle::default(), + entry_angle: Rotation::default(), }); id } @@ -544,13 +569,13 @@ impl Skeleton { } /// Sets a base rotation to apply to the entire skeleton. - pub fn set_rotation(&mut self, rotation: Angle) { + pub fn set_rotation(&mut self, rotation: Rotation) { self.rotation = rotation; } /// Returns the base rotation being applied to the entire skeleton. #[must_use] - pub const fn rotation(&self) -> Angle { + pub const fn rotation(&self) -> Rotation { self.rotation } @@ -566,11 +591,11 @@ impl Skeleton { fn solve_axis(&mut self) { let mut axis_solved = HashSet::new(); let root_bone = &mut self.bones[0]; - let (end, mid) = determine_end_position( + let (end, mid, _) = determine_end_position( root_bone.start, root_bone.desired_end, self.rotation, - Angle::radians(0.), + Rotation::radians(0.), &root_bone.kind, ); root_bone.entry_angle = self.rotation; @@ -582,7 +607,7 @@ impl Skeleton { ( root_bone.id.axis_a(), root_bone.start, - angle + Angle::radians(PI), + angle + Rotation::radians(PI), ), (root_bone.id.axis_b(), root_bone.end, angle), ]; @@ -610,13 +635,14 @@ impl Skeleton { bone.start = current_position; joint.calculated_position = current_position; - let (end, mid) = determine_end_position( + let (end, mid, angle_offset) = determine_end_position( current_position, bone.desired_end, current_rotation, joint.angle, &bone.kind, ); + bone.entry_angle += angle_offset; bone.end = end; bone.joint_pos = mid; @@ -633,14 +659,16 @@ impl Skeleton { fn determine_end_position( start: Coordinate, desired_end: Option, - current_rotation: Angle, - joint_angle: Angle, + current_rotation: Rotation, + joint_angle: Rotation, bone: &BoneKind, -) -> (Coordinate, Option) { +) -> (Coordinate, Option, Rotation) { + let entry_angle = current_rotation + joint_angle; match bone { BoneKind::Rigid { length } => ( - start + Vector::new(*length, current_rotation + joint_angle), + start + Vector::new(*length, entry_angle), None, + Rotation::default(), ), BoneKind::Jointed { start_length, @@ -648,7 +676,7 @@ fn determine_end_position( inverse, } => { if let Some(desired_end) = desired_end { - let desired_angle = desired_end.direction + current_rotation; + let desired_angle = desired_end.direction + entry_angle; let distance = desired_end.magnitude; let full_length = start_length + end_length; let minimum_size = (start_length - end_length).abs(); @@ -671,11 +699,11 @@ fn determine_end_position( *end_length, ); - (end, Some(joint)) + (end, Some(joint), joint_angle) } else { - let joint = start + Vector::new(*start_length, current_rotation); - let end = joint + Vector::new(*end_length, current_rotation); - (end, Some(joint)) + let joint = start + Vector::new(*start_length, entry_angle); + let end = joint + Vector::new(*end_length, entry_angle); + (end, Some(joint), joint_angle) } } } @@ -685,7 +713,7 @@ fn get_third_point( inverse: bool, start: Coordinate, distance: f32, - hyp_angle: Angle, + hyp_angle: Rotation, first: f32, second: f32, ) -> Coordinate { @@ -695,7 +723,7 @@ fn get_third_point( start + Vector::new(first, hyp_angle) } else { let first_angle = hyp_angle - - Angle { + - Rotation { radians: if inverse { -first_angle } else { first_angle }, }; start + Vector::new(first, first_angle) @@ -762,7 +790,7 @@ pub struct Bone { joint_pos: Option, end: Coordinate, desired_end: Option, - entry_angle: Angle, + entry_angle: Rotation, } impl Bone { @@ -808,7 +836,7 @@ impl Bone { /// Returns the angle of the previous bone segment connecting to this bone. #[must_use] - pub const fn entry_angle(&self) -> Angle { + pub const fn entry_angle(&self) -> Rotation { self.entry_angle } @@ -853,7 +881,7 @@ pub struct Joint { bone_a: BoneAxis, bone_b: BoneAxis, calculated_position: Coordinate, - angle: Angle, + angle: Rotation, } impl Joint { @@ -865,7 +893,7 @@ impl Joint { /// Returns a new joint formed by joining `bone_a` and `bone_b` at `angle`. #[must_use] - pub const fn new(angle: Angle, bone_a: BoneAxis, bone_b: BoneAxis) -> Self { + pub const fn new(angle: Rotation, bone_a: BoneAxis, bone_b: BoneAxis) -> Self { Self { id: JointId(0), label: None, @@ -913,13 +941,13 @@ impl Joint { /// /// This setting is ignored if the bone furthest from the root of the joint /// is a [`BoneKind::Jointed`] bone. - pub fn set_angle(&mut self, angle: Angle) { + pub fn set_angle(&mut self, angle: Rotation) { self.angle = angle; } /// Returns the rotation of this joint. #[must_use] - pub const fn angle(&self) -> Angle { + pub const fn angle(&self) -> Rotation { self.angle } } @@ -993,15 +1021,15 @@ impl BoneEnd { #[allow(clippy::cast_possible_truncation)] fn rotation() { assert_eq!( - (Angle::degrees(90.) + Angle::degrees(180.)) - .normalized() + (Rotation::degrees(90.) + Rotation::degrees(180.)) + .clamped() .to_degrees() .round() as i32, 270, ); assert_eq!( - (Angle::degrees(90.) + Angle::degrees(-180.)) - .normalized() + (Rotation::degrees(90.) + Rotation::degrees(-180.)) + .clamped() .to_degrees() .round() as i32, 270, diff --git a/src/serde.rs b/src/serde.rs index bb8b325..0f187a1 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -6,7 +6,7 @@ use serde::{ Deserialize, Serialize, }; -use crate::{Angle, Bone, BoneAxis, BoneKind, Joint, Skeleton, Vector}; +use crate::{Rotation, Bone, BoneAxis, BoneKind, Joint, Skeleton, Vector}; impl Serialize for Skeleton { fn serialize(&self, serializer: S) -> Result @@ -130,7 +130,7 @@ impl Serialize for Joint { struct DeserializedJoint { from: BoneAxis, to: BoneAxis, - angle: Angle, + angle: Rotation, #[serde(default)] label: String, } @@ -145,7 +145,7 @@ fn roundtrip() { inverse: true, }); let joint = s.push_joint(Joint::new( - Angle::radians(0.), + Rotation::radians(0.), spine.axis_a(), other.axis_b(), )); @@ -153,5 +153,5 @@ fn roundtrip() { let deserialized: Skeleton = pot::from_slice(&serialized).unwrap(); assert_eq!(deserialized[spine].label(), "spine"); assert_eq!(deserialized[other].label(), ""); - assert_eq!(deserialized[joint].angle(), Angle::radians(0.)); + assert_eq!(deserialized[joint].angle(), Rotation::radians(0.)); }