diff --git a/Cargo.lock b/Cargo.lock index ac5da4f..87c4aec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -898,6 +898,8 @@ name = "funnybones" version = "0.1.0" dependencies = [ "cushy", + "pot", + "serde", ] [[package]] @@ -2077,6 +2079,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "pot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df842bdb3b0553a411589e64aaa1a7d0c0259f72fabcedfaa841683ae3019d80" +dependencies = [ + "byteorder", + "half", + "serde", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2484,18 +2497,18 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "serde" -version = "1.0.206" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.206" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f379fa8..75980d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,10 @@ required-features = ["cushy"] [dependencies] cushy = { git = "https://github.com/khonsulabs/cushy", optional = true } +serde = { version = "1.0.208", optional = true, features = ["derive"] } + +[dev-dependencies] +pot = "3.0.0" [lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/examples/skeleton.rs b/examples/skeleton.rs index fb006de..3092d88 100644 --- a/examples/skeleton.rs +++ b/examples/skeleton.rs @@ -17,17 +17,21 @@ use cushy::{ widgets::{slider::Slidable, Canvas}, Run, }; -use funnybones::{BoneId, BoneKind, JointId, Rotation, Skeleton, Vector}; +use funnybones::{BoneId, BoneKind, Joint, JointId, Rotation, Skeleton, Vector}; fn main() { // begin rustme snippet: readme let mut skeleton = Skeleton::default(); // Create our root bone: the spine - let spine = skeleton.push_bone(BoneKind::Rigid { length: 3. }, "spine"); + let spine = skeleton.push_bone(BoneKind::Rigid { length: 3. }.with_label("spine")); // Create the right-half of the hips. - let r_hip = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "r_hip"); + 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(Rotation::degrees(-90.), spine.axis_a(), r_hip.axis_a()); + skeleton.push_joint(Joint::new( + Rotation::degrees(-90.), + spine.axis_a(), + r_hip.axis_a(), + )); // Create the right leg as a jointed bone with equal sizes for the upper and // lower leg. let r_leg = skeleton.push_bone( @@ -35,67 +39,113 @@ fn main() { start_length: 1.5, end_length: 1.5, inverse: true, - }, - "r_leg", + } + .with_label("r_leg"), ); // Connect the right leg to the right hip. - skeleton.push_joint(Rotation::degrees(-90.), r_hip.axis_b(), r_leg.axis_a()); + skeleton.push_joint(Joint::new( + Rotation::degrees(-90.), + r_hip.axis_b(), + r_leg.axis_a(), + )); // Create the right foot. - let r_foot = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "r_foot"); + 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(Rotation::degrees(90.), r_leg.axis_b(), r_foot.axis_a()); + let r_ankle_id = skeleton.push_joint(Joint::new( + Rotation::degrees(90.), + r_leg.axis_b(), + r_foot.axis_a(), + )); // end rustme snippet // Create the left-half of our lower half. - let l_hip = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "l_hip"); - skeleton.push_joint(Rotation::degrees(90.), spine.axis_a(), l_hip.axis_a()); + let l_hip = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("l_hip")); + skeleton.push_joint(Joint::new( + Rotation::degrees(90.), + spine.axis_a(), + l_hip.axis_a(), + )); let l_leg = skeleton.push_bone( BoneKind::Jointed { start_length: 1.5, end_length: 1.5, inverse: false, - }, - "l_leg", + } + .with_label("l_leg"), ); - skeleton.push_joint(Rotation::degrees(90.), l_hip.axis_b(), l_leg.axis_a()); - let l_foot = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "l_foot"); - let l_ankle_id = skeleton.push_joint(Rotation::degrees(-90.), l_leg.axis_b(), l_foot.axis_a()); + skeleton.push_joint(Joint::new( + 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( + Rotation::degrees(-90.), + l_leg.axis_b(), + l_foot.axis_a(), + )); // Create our two arms in the same fashion as our leg structure. - let r_shoulder = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "r_shoulder"); - skeleton.push_joint(Rotation::degrees(-90.), spine.axis_b(), r_shoulder.axis_a()); + let r_shoulder = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("r_shoulder")); + skeleton.push_joint(Joint::new( + Rotation::degrees(-90.), + spine.axis_b(), + r_shoulder.axis_a(), + )); let r_arm = skeleton.push_bone( BoneKind::Jointed { start_length: 1.0, end_length: 1.0, inverse: true, - }, - "r_arm", + } + .with_label("r_arm"), ); - let r_arm_socket = - skeleton.push_joint(Rotation::degrees(-90.), r_shoulder.axis_b(), r_arm.axis_a()); - let r_hand = skeleton.push_bone(BoneKind::Rigid { length: 0.3 }, "r_hand"); - let r_wrist_id = skeleton.push_joint(Rotation::degrees(175.), r_arm.axis_b(), r_hand.axis_a()); + let r_arm_socket = skeleton.push_joint(Joint::new( + Rotation::degrees(-90.), + 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( + Rotation::degrees(175.), + r_arm.axis_b(), + r_hand.axis_a(), + )); - let l_shoulder = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "l_shoulder"); - skeleton.push_joint(Rotation::degrees(90.), spine.axis_b(), l_shoulder.axis_a()); + let l_shoulder = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("l_shoulder")); + skeleton.push_joint(Joint::new( + Rotation::degrees(90.), + spine.axis_b(), + l_shoulder.axis_a(), + )); let l_arm = skeleton.push_bone( BoneKind::Jointed { start_length: 1.0, end_length: 1.0, inverse: false, - }, - "l_arm", + } + .with_label("l_arm"), ); - let l_arm_socket = - skeleton.push_joint(Rotation::degrees(90.), l_shoulder.axis_b(), l_arm.axis_a()); - let l_hand = skeleton.push_bone(BoneKind::Rigid { length: 0.3 }, "l_hand"); - let l_wrist_id = skeleton.push_joint(Rotation::degrees(-175.), l_arm.axis_b(), l_hand.axis_a()); + let l_arm_socket = skeleton.push_joint(Joint::new( + Rotation::degrees(90.), + 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( + Rotation::degrees(-175.), + l_arm.axis_b(), + l_hand.axis_a(), + )); // Finally, create a bone to represent our head. - let head = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }, "head"); - let neck = skeleton.push_joint(Rotation::degrees(180.), spine.axis_b(), head.axis_a()); + let head = skeleton.push_bone(BoneKind::Rigid { length: 0.5 }.with_label("head")); + let neck = skeleton.push_joint(Joint::new( + Rotation::degrees(180.), + spine.axis_b(), + head.axis_a(), + )); let skeleton = Dynamic::new(skeleton); diff --git a/src/lib.rs b/src/lib.rs index 88aa8a4..30606ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,10 +5,15 @@ use std::{ f32::consts::PI, fmt::{Debug, Display}, ops::{Add, Index, IndexMut, Neg, Sub}, + sync::Arc, }; +#[cfg(feature = "serde")] +mod serde; + /// A two dimensionsional offset/measurement. #[derive(Default, Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct Vector { /// The x-axis component of this vector. pub x: f32, @@ -66,6 +71,7 @@ impl cushy::figures::FromComponents for Vector { /// A value representing a rotation between no rotation and a full rotation. #[derive(Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct Rotation { radians: f32, } @@ -173,6 +179,7 @@ impl Neg for Rotation { /// A representation of a bone structure inside of a [`Skeleton`]. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum BoneKind { /// A single bone of a fixed length. Rigid { @@ -194,6 +201,31 @@ pub enum BoneKind { }, } +impl BoneKind { + /// Attaches a label to this bone when pushed into a skeleton. + #[must_use] + pub fn with_label(self, label: impl Into) -> LabeledBoneKind { + LabeledBoneKind { + kind: self, + label: label.into(), + } + } +} + +/// A [`BoneKind`] with an associated label. +pub struct LabeledBoneKind { + /// The bone to create. + pub kind: BoneKind, + /// The label of the bone. + pub label: String, +} + +impl From for LabeledBoneKind { + fn from(kind: BoneKind) -> Self { + kind.with_label(String::new()) + } +} + /// A collection of [`Bone`]s. connected by [`Joint`]s. #[derive(Default, Debug)] pub struct Skeleton { @@ -202,6 +234,8 @@ pub struct Skeleton { joints: Vec, connections: HashMap>, generation: usize, + bones_by_label: HashMap, BoneId>, + joints_by_label: HashMap, JointId>, } impl Skeleton { @@ -211,17 +245,25 @@ impl Skeleton { /// The first bone pushed is considered the root of the skeleton. All other /// bones must be connected to the root directly or indirectly through /// [`Joint`]s. - pub fn push_bone(&mut self, bone: BoneKind, label: &'static str) -> BoneId { + pub fn push_bone(&mut self, bone: impl Into) -> BoneId { + let bone = bone.into(); let id = BoneId(u8::try_from(self.bones.len()).expect("too many bones")); if id == BoneId(0) { - let joint = self.push_joint(Rotation::default(), id.axis_a(), id.axis_a()); + let joint = self.push_joint(Joint::new(Rotation::default(), id.axis_a(), id.axis_a())); self.initial_joint = Some(joint); self.connections.insert(id.axis_a(), vec![joint]); } + let label = if bone.label.is_empty() { + None + } else { + let label = Arc::new(bone.label); + self.bones_by_label.insert(label.clone(), id); + Some(label) + }; self.bones.push(Bone { generation: self.generation, label, - kind: bone, + kind: bone.kind, start: Vector::default(), joint_pos: None, end: Vector::default(), @@ -232,14 +274,14 @@ impl Skeleton { /// Creates a new [`Joint`] in the skeleton, connecting two bones together /// by their [axis](BoneAxis). Returns the unique id of the created joint. - pub fn push_joint(&mut self, angle: Rotation, bone_a: BoneAxis, bone_b: BoneAxis) -> JointId { + pub fn push_joint(&mut self, joint: Joint) -> JointId { let id = JointId(u8::try_from(self.joints.len()).expect("too many joints")); - self.joints.push(Joint { - bone_a, - bone_b, - angle, - calculated_position: Vector::default(), - }); + let bone_a = joint.bone_a; + let bone_b = joint.bone_b; + if let Some(label) = joint.label.clone() { + self.joints_by_label.insert(label, id); + } + self.joints.push(joint); self.connections.entry(bone_a).or_default().push(id); if bone_a != bone_b { self.connections.entry(bone_b).or_default().push(id); @@ -296,7 +338,7 @@ impl Skeleton { println!( "Solving {}:{:?} at {current_position:?} - {current_rotation} - {inverse_root}", - self.bones[usize::from(axis.bone.0)].label, + self.bones[usize::from(axis.bone.0)].label(), axis.end ); @@ -313,7 +355,9 @@ impl Skeleton { bone.generation = self.generation; println!( " -> {joint_id:?} -> {}:{:?} ({})", - bone.label, other_axis.end, joint.angle + bone.label(), + other_axis.end, + joint.angle ); joint.calculated_position = if let Some(current_position) = current_position { bone.start = current_position; @@ -478,6 +522,7 @@ impl IndexMut for Skeleton { /// A specific end of a specific bone. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct BoneAxis { /// The unique id of the bone of this axis. pub bone: BoneId, @@ -500,7 +545,7 @@ impl BoneAxis { #[derive(Debug)] pub struct Bone { generation: usize, - label: &'static str, + label: Option>, kind: BoneKind, start: Vector, joint_pos: Option, @@ -547,13 +592,14 @@ impl Bone { /// Returns the label this bone was created with. #[must_use] pub fn label(&self) -> &str { - self.label + self.label.as_ref().map_or("", |s| s) } } /// A connection between two bones. #[derive(Debug)] pub struct Joint { + label: Option>, bone_a: BoneAxis, bone_b: BoneAxis, calculated_position: Vector, @@ -561,6 +607,34 @@ pub struct Joint { } impl Joint { + /// Returns a new joint formed by joining `bone_a` and `bone_b` at `angle`. + #[must_use] + pub const fn new(angle: Rotation, bone_a: BoneAxis, bone_b: BoneAxis) -> Self { + Self { + label: None, + bone_a, + bone_b, + calculated_position: Vector::new(0., 0.), + angle, + } + } + + /// Labels this joint and returns self. + #[must_use] + pub fn with_label(mut self, label: impl Into) -> Self { + let label = label.into(); + if !label.is_empty() { + self.label = Some(Arc::new(label)); + } + self + } + + /// Returns the label of this joint. + #[must_use] + pub fn label(&self) -> &str { + self.label.as_ref().map_or("", |s| s) + } + /// Given `axis` is one of the two connections in this joint, return the /// other axis. /// @@ -595,6 +669,7 @@ impl Joint { /// The unique ID of a [`Bone`] in a [`Skeleton`]. #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct BoneId(u8); impl BoneId { @@ -619,10 +694,12 @@ impl BoneId { /// The unique ID of a [`Joint`] in a [`Skeleton`]. #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct JointId(u8); /// A specific end of a [`Bone`]. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum BoneEnd { /// The first end of a bone. A, diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 0000000..3e6e42f --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,157 @@ +use std::borrow::Cow; + +use serde::{ + de::{self, Visitor}, + ser::SerializeStruct, + Deserialize, Serialize, +}; + +use crate::{Bone, BoneAxis, BoneKind, Joint, Rotation, Skeleton, Vector}; + +impl Serialize for Skeleton { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("Skeleton", 2)?; + s.serialize_field("bones", &self.bones)?; + s.serialize_field("joints", &self.joints)?; + s.end() + } +} + +impl<'de> Deserialize<'de> for Skeleton { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_struct( + "Skeleton", + &["bones", "joints"], + SkeletonVisitor::default(), + ) + } +} + +#[derive(Default)] +struct SkeletonVisitor { + bones: Vec, + joints: Vec, +} + +impl<'de> Visitor<'de> for SkeletonVisitor { + type Value = Skeleton; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a Skeleton") + } + + fn visit_map(mut self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + while let Some(key) = map.next_key::>()? { + match &*key { + "bones" => { + self.bones = map.next_value()?; + } + "joints" => { + self.joints = map.next_value()?; + } + _ => { + return Err(::custom(format!( + "unexpected field {key}" + ))) + } + } + } + + let mut skeleton = Skeleton::default(); + for bone in self.bones.drain(..) { + let id = skeleton.push_bone(bone.kind.with_label(bone.label)); + if let Some(target) = bone.target { + skeleton[id].set_desired_end(Some(target)); + } + } + for joint in self.joints.drain(..) { + skeleton + .push_joint(Joint::new(joint.angle, joint.from, joint.to).with_label(joint.label)); + } + Ok(skeleton) + } +} + +impl Serialize for Bone { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let field_count = + 1 + usize::from(self.label.is_some()) + usize::from(self.desired_end.is_some()); + let mut b = serializer.serialize_struct("Bone", field_count)?; + b.serialize_field("kind", &self.kind)?; + if let Some(label) = &self.label { + b.serialize_field("label", &**label)?; + } + if let Some(desired_end) = self.desired_end { + b.serialize_field("target", &desired_end)?; + } + b.end() + } +} + +#[derive(Deserialize)] +struct DeserializedBone { + #[serde(default)] + label: String, + kind: BoneKind, + #[serde(default)] + target: Option, +} + +impl Serialize for Joint { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let field_count = 3 + usize::from(self.label.is_some()); + let mut b = serializer.serialize_struct("Joint", field_count)?; + b.serialize_field("from", &self.bone_a)?; + b.serialize_field("to", &self.bone_b)?; + b.serialize_field("angle", &self.angle)?; + if let Some(label) = &self.label { + b.serialize_field("label", &**label)?; + } + b.end() + } +} + +#[derive(Deserialize)] +struct DeserializedJoint { + from: BoneAxis, + to: BoneAxis, + angle: Rotation, + #[serde(default)] + label: String, +} + +#[test] +fn roundtrip() { + let mut s = Skeleton::default(); + let spine = s.push_bone(BoneKind::Rigid { length: 1.0 }.with_label("spine")); + let other = s.push_bone(BoneKind::Jointed { + start_length: 2.0, + end_length: 3.0, + inverse: true, + }); + let joint = s.push_joint(Joint::new( + Rotation::radians(0.), + spine.axis_a(), + other.axis_b(), + )); + let serialized = pot::to_vec(&s).unwrap(); + let deserialized: Skeleton = dbg!(pot::from_slice(&serialized).unwrap()); + assert_eq!(deserialized[spine].label(), "spine"); + assert_eq!(deserialized[other].label(), ""); + assert_eq!(deserialized[joint].angle(), Rotation::radians(0.)); +}