diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f0d2de5..0026748 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,33 +62,6 @@ jobs: - name: Run clippy run: cargo clippy -- -D warnings - # Run cargo publish pipeline as a dry run - dry_run: - name: Dry Run Publish - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout sources - uses: actions/checkout@v4 - - name: Cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@stable - - name: Install Dependencies - run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - - name: Run cargo publish dry run for library - run: cargo publish --dry-run -p bevy_animation_graph - - name: Run cargo publish dry run for editor - run: cargo publish --dry-run -p bevy_animation_graph_editor - # Run cargo fmt --all -- --check format: name: Format diff --git a/.github/workflows/dry_run.yaml b/.github/workflows/dry_run.yaml new file mode 100644 index 0000000..56d55ff --- /dev/null +++ b/.github/workflows/dry_run.yaml @@ -0,0 +1,34 @@ +name: DryRun + +on: {} + +env: + CARGO_TERM_COLOR: always + +jobs: + # Run cargo publish pipeline as a dry run + dry_run: + name: Dry Run Publish + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install Dependencies + run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + - name: Run cargo publish dry run for library + run: cargo publish --dry-run -p bevy_animation_graph + - name: Run cargo publish dry run for editor + run: cargo publish --dry-run -p bevy_animation_graph_editor diff --git a/Cargo.lock b/Cargo.lock index 5925bab..cc6f334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -483,6 +483,7 @@ version = "0.2.0" dependencies = [ "bevy", "indexmap 2.2.1", + "regex", "ron", "serde", "thiserror", diff --git a/assets/animation_graphs/human.animgraph.ron b/assets/animation_graphs/human.animgraph.ron index 6c6b998..da08e07 100644 --- a/assets/animation_graphs/human.animgraph.ron +++ b/assets/animation_graphs/human.animgraph.ron @@ -2,7 +2,7 @@ nodes: [ ( name: "Walk Flip LR", - node: FlipLR, + node: FlipLR(), ), ( name: "Walk Clip", @@ -18,7 +18,7 @@ ), ( name: "Run Flip LR", - node: FlipLR, + node: FlipLR(), ), ( name: "Run Chain", @@ -114,4 +114,4 @@ input_position: (-242.0, 134.0), output_position: (860.0, -42.0), ), -) \ No newline at end of file +) diff --git a/assets/animation_graphs/human_ik.animgraph.ron b/assets/animation_graphs/human_ik.animgraph.ron index 62d24e4..11d0148 100644 --- a/assets/animation_graphs/human_ik.animgraph.ron +++ b/assets/animation_graphs/human_ik.animgraph.ron @@ -6,7 +6,7 @@ ), ( name: "Walk Flip LR", - node: FlipLR, + node: FlipLR(), ), ( name: "Walk Clip 2", @@ -22,7 +22,7 @@ ), ( name: "Run Flip LR", - node: FlipLR, + node: FlipLR(), ), ( name: "Extend to skeleton", @@ -140,4 +140,4 @@ input_position: (69.0, 330.0), output_position: (1229.0, 56.0), ), -) \ No newline at end of file +) diff --git a/crates/bevy_animation_graph/Cargo.toml b/crates/bevy_animation_graph/Cargo.toml index 2314aad..dbf718c 100644 --- a/crates/bevy_animation_graph/Cargo.toml +++ b/crates/bevy_animation_graph/Cargo.toml @@ -14,3 +14,4 @@ thiserror = "1.0.50" ron = "0.8.1" serde = { version = "1.0.193", features = ["derive"] } indexmap = { version = "2.2.1", features = ["serde"] } +regex = "1.10.3" diff --git a/crates/bevy_animation_graph/src/core/animation_graph/loader.rs b/crates/bevy_animation_graph/src/core/animation_graph/loader.rs index 473b895..2004580 100644 --- a/crates/bevy_animation_graph/src/core/animation_graph/loader.rs +++ b/crates/bevy_animation_graph/src/core/animation_graph/loader.rs @@ -129,7 +129,9 @@ impl AssetLoader for AnimationGraphLoader { } AnimationNodeTypeSerial::Blend => BlendNode::new().wrapped(&serial_node.name), AnimationNodeTypeSerial::Chain => ChainNode::new().wrapped(&serial_node.name), - AnimationNodeTypeSerial::FlipLR => FlipLRNode::new().wrapped(&serial_node.name), + AnimationNodeTypeSerial::FlipLR { config } => { + FlipLRNode::new(config.clone()).wrapped(&serial_node.name) + } AnimationNodeTypeSerial::Loop => LoopNode::new().wrapped(&serial_node.name), AnimationNodeTypeSerial::Speed => SpeedNode::new().wrapped(&serial_node.name), AnimationNodeTypeSerial::Rotation => { diff --git a/crates/bevy_animation_graph/src/core/animation_graph/serial.rs b/crates/bevy_animation_graph/src/core/animation_graph/serial.rs index f4cd213..1487fb9 100644 --- a/crates/bevy_animation_graph/src/core/animation_graph/serial.rs +++ b/crates/bevy_animation_graph/src/core/animation_graph/serial.rs @@ -1,7 +1,7 @@ use super::{pin, AnimationGraph, Extra}; use crate::{ core::frame::PoseSpec, - prelude::{AnimationNode, AnimationNodeType, ParamSpec, ParamValue}, + prelude::{config::FlipConfig, AnimationNode, AnimationNodeType, ParamSpec, ParamValue}, utils::ordered_map::OrderedMap, }; use bevy::utils::HashMap; @@ -55,7 +55,10 @@ pub enum AnimationNodeTypeSerial { Clip(String, Option), Blend, Chain, - FlipLR, + FlipLR { + #[serde(default)] + config: FlipConfig, + }, Loop, Speed, Rotation, @@ -124,7 +127,9 @@ impl From<&AnimationNodeType> for AnimationNodeTypeSerial { ), AnimationNodeType::Blend(_) => AnimationNodeTypeSerial::Blend, AnimationNodeType::Chain(_) => AnimationNodeTypeSerial::Chain, - AnimationNodeType::FlipLR(_) => AnimationNodeTypeSerial::FlipLR, + AnimationNodeType::FlipLR(n) => AnimationNodeTypeSerial::FlipLR { + config: n.config.clone(), + }, AnimationNodeType::Loop(_) => AnimationNodeTypeSerial::Loop, AnimationNodeType::Speed(_) => AnimationNodeTypeSerial::Speed, AnimationNodeType::Rotation(_) => AnimationNodeTypeSerial::Rotation, diff --git a/crates/bevy_animation_graph/src/core/plugin.rs b/crates/bevy_animation_graph/src/core/plugin.rs index 31e55a2..4bb7662 100644 --- a/crates/bevy_animation_graph/src/core/plugin.rs +++ b/crates/bevy_animation_graph/src/core/plugin.rs @@ -8,6 +8,7 @@ use super::{ systems::{animation_player, animation_player_deferred_gizmos}, }; use crate::prelude::{ + config::{FlipConfig, FlipNameMapper, PatternMapper, PatternMapperSerial}, AbsF32, AddF32, AnimationGraph, AnimationGraphPlayer, AnimationNodeType, BlendNode, ChainNode, ClampF32, ClipNode, DivF32, DummyNode, ExtendSkeleton, FlipLRNode, GraphClip, GraphNode, IntoBoneSpaceNode, IntoCharacterSpaceNode, IntoGlobalSpaceNode, LoopNode, MulF32, @@ -57,7 +58,10 @@ impl AnimationGraphPlugin { .register_type::() .register_type::() .register_type::() - + .register_type::() + .register_type::() + .register_type::() + .register_type::() // --- Node registrations // ------------------------------------------ .register_type::() diff --git a/crates/bevy_animation_graph/src/flipping/config.rs b/crates/bevy_animation_graph/src/flipping/config.rs new file mode 100644 index 0000000..8e29900 --- /dev/null +++ b/crates/bevy_animation_graph/src/flipping/config.rs @@ -0,0 +1,140 @@ +use bevy::reflect::{std_traits::ReflectDefault, Reflect}; +use regex::{escape, Regex}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Reflect, Clone, Serialize, Deserialize)] +#[reflect(Default)] +pub struct FlipConfig { + pub name_mapper: FlipNameMapper, +} + +#[derive(Debug, Reflect, Clone)] +#[reflect(Default)] +pub struct PatternMapper { + key_1: String, + key_2: String, + pattern_before: String, + pattern_after: String, + #[reflect(ignore, default = "default_regex")] + regex: Regex, +} + +pub fn default_regex() -> Regex { + Regex::new("").unwrap() +} + +impl TryFrom for PatternMapper { + type Error = regex::Error; + + fn try_from(value: PatternMapperSerial) -> Result { + let regex = Regex::new(&format!( + "({})({}|{})({})", + &value.pattern_before, + escape(&value.key_1), + escape(&value.key_2), + &value.pattern_after, + ))?; + + Ok(Self { + key_1: value.key_1, + key_2: value.key_2, + pattern_before: value.pattern_before, + pattern_after: value.pattern_after, + regex, + }) + } +} + +impl From for PatternMapperSerial { + fn from(value: PatternMapper) -> Self { + Self { + key_1: value.key_1, + key_2: value.key_2, + pattern_before: value.pattern_before, + pattern_after: value.pattern_after, + } + } +} + +impl Default for PatternMapper { + fn default() -> Self { + Self::try_from(PatternMapperSerial::default()).unwrap() + } +} + +impl Serialize for PatternMapper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + PatternMapperSerial::from(self.clone()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PatternMapper { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + PatternMapperSerial::deserialize(deserializer).map(|r| r.try_into().unwrap()) + } +} + +impl PatternMapper { + pub fn flip(&self, input: &str) -> Option { + if let Some(captures) = self.regex.captures(input) { + let key_capture = captures.get(2).unwrap().as_str(); + let replacement_key = if key_capture == self.key_1 { + &self.key_2 + } else { + &self.key_1 + }; + Some( + self.regex + .replace(input, format!("${{1}}{replacement_key}${{3}}")) + .into(), + ) + } else { + None + } + } +} + +#[derive(Debug, Reflect, Serialize, Deserialize, Clone)] +pub struct PatternMapperSerial { + pub key_1: String, + pub key_2: String, + pub pattern_before: String, + pub pattern_after: String, +} + +impl Default for PatternMapperSerial { + fn default() -> Self { + Self { + key_1: "L".into(), + key_2: "R".into(), + pattern_before: r"^.*".into(), + pattern_after: r"$".into(), + } + } +} + +#[derive(Debug, Reflect, Clone, Serialize, Deserialize)] +#[reflect(Default)] +pub enum FlipNameMapper { + Pattern(PatternMapper), +} + +impl Default for FlipNameMapper { + fn default() -> Self { + Self::Pattern(PatternMapper::default()) + } +} + +impl FlipNameMapper { + pub fn flip(&self, input: &str) -> Option { + match self { + Self::Pattern(pattern) => pattern.flip(input), + } + } +} diff --git a/crates/bevy_animation_graph/src/flipping/mod.rs b/crates/bevy_animation_graph/src/flipping/mod.rs index 3327fdb..e05d092 100644 --- a/crates/bevy_animation_graph/src/flipping/mod.rs +++ b/crates/bevy_animation_graph/src/flipping/mod.rs @@ -1,3 +1,6 @@ +pub mod config; + +use self::config::FlipConfig; use crate::core::{ animation_clip::EntityPath, frame::{BoneFrame, BonePoseFrame, InnerPoseFrame, ValueFrame}, @@ -5,11 +8,11 @@ use crate::core::{ use bevy::math::prelude::*; pub trait FlipXBySuffix { - fn flipped_by_suffix(&self, suffix_1: String, suffix_2: String) -> Self; + fn flipped(&self, config: &FlipConfig) -> Self; } impl FlipXBySuffix for ValueFrame { - fn flipped_by_suffix(&self, _suffix_1: String, _suffix_2: String) -> Self { + fn flipped(&self, _: &FlipConfig) -> Self { let mut out = self.clone(); out.prev.x *= -1.; @@ -20,7 +23,7 @@ impl FlipXBySuffix for ValueFrame { } impl FlipXBySuffix for ValueFrame { - fn flipped_by_suffix(&self, _suffix_1: String, _suffix_2: String) -> Self { + fn flipped(&self, _: &FlipConfig) -> Self { let mut out = self.clone(); out.prev.x *= -1.; @@ -33,16 +36,10 @@ impl FlipXBySuffix for ValueFrame { } impl FlipXBySuffix for BoneFrame { - fn flipped_by_suffix(&self, suffix_1: String, suffix_2: String) -> Self { + fn flipped(&self, config: &FlipConfig) -> Self { BoneFrame { - rotation: self - .rotation - .clone() - .map(|v| v.flipped_by_suffix(suffix_1.clone(), suffix_2.clone())), - translation: self - .translation - .clone() - .map(|v| v.flipped_by_suffix(suffix_1.clone(), suffix_2.clone())), + rotation: self.rotation.clone().map(|v| v.flipped(config)), + translation: self.translation.clone().map(|v| v.flipped(config)), scale: self.scale.clone(), weights: self.weights.clone(), } @@ -50,23 +47,18 @@ impl FlipXBySuffix for BoneFrame { } impl FlipXBySuffix for InnerPoseFrame { - fn flipped_by_suffix(&self, suffix_1: String, suffix_2: String) -> Self { + fn flipped(&self, config: &FlipConfig) -> Self { let mut out = InnerPoseFrame::default(); for (path, bone_id) in self.paths.iter() { - let channel = - self.bones[*bone_id].flipped_by_suffix(suffix_1.clone(), suffix_2.clone()); + let channel = self.bones[*bone_id].flipped(config); let new_path = EntityPath { parts: path .parts .iter() .map(|part| { let mut part = part.to_string(); - if part.ends_with(&suffix_1) { - part = part.strip_suffix(&suffix_1).unwrap().into(); - part.push_str(&suffix_2); - } else if part.ends_with(&suffix_2) { - part = part.strip_suffix(&suffix_2).unwrap().into(); - part.push_str(&suffix_1); + if let Some(flipped) = config.name_mapper.flip(&part) { + part = flipped; } part.into() }) @@ -80,7 +72,7 @@ impl FlipXBySuffix for InnerPoseFrame { } impl FlipXBySuffix for BonePoseFrame { - fn flipped_by_suffix(&self, suffix_1: String, suffix_2: String) -> Self { - BonePoseFrame(self.0.flipped_by_suffix(suffix_1, suffix_2)) + fn flipped(&self, config: &FlipConfig) -> Self { + BonePoseFrame(self.0.flipped(config)) } } diff --git a/crates/bevy_animation_graph/src/nodes/flip_lr_node.rs b/crates/bevy_animation_graph/src/nodes/flip_lr_node.rs index c7e48a4..679105f 100644 --- a/crates/bevy_animation_graph/src/nodes/flip_lr_node.rs +++ b/crates/bevy_animation_graph/src/nodes/flip_lr_node.rs @@ -4,17 +4,20 @@ use crate::core::duration_data::DurationData; use crate::core::errors::GraphError; use crate::core::frame::{BonePoseFrame, PoseFrame, PoseFrameData, PoseSpec}; use crate::flipping::FlipXBySuffix; +use crate::prelude::config::FlipConfig; use crate::prelude::{PassContext, SpecContext}; use crate::utils::unwrap::Unwrap; use bevy::prelude::*; #[derive(Reflect, Clone, Debug)] #[reflect(Default)] -pub struct FlipLRNode {} +pub struct FlipLRNode { + pub config: FlipConfig, +} impl Default for FlipLRNode { fn default() -> Self { - Self::new() + Self::new(FlipConfig::default()) } } @@ -22,8 +25,8 @@ impl FlipLRNode { pub const INPUT: &'static str = "Pose In"; pub const OUTPUT: &'static str = "Pose Out"; - pub fn new() -> Self { - Self {} + pub fn new(config: FlipConfig) -> Self { + Self { config } } pub fn wrapped(self, name: impl Into) -> AnimationNode { @@ -43,7 +46,7 @@ impl NodeLike for FlipLRNode { ) -> Result, GraphError> { let in_pose_frame = ctx.pose_back(Self::INPUT, input)?; let bone_frame: BonePoseFrame = in_pose_frame.data.unwrap(); - let flipped_pose_frame = bone_frame.flipped_by_suffix("R".into(), "L".into()); + let flipped_pose_frame = bone_frame.flipped(&self.config); Ok(Some(PoseFrame { data: PoseFrameData::BoneSpace(flipped_pose_frame), diff --git a/crates/bevy_animation_graph_editor/src/egui_inspector_impls.rs b/crates/bevy_animation_graph_editor/src/egui_inspector_impls.rs index ff7c513..734e92a 100644 --- a/crates/bevy_animation_graph_editor/src/egui_inspector_impls.rs +++ b/crates/bevy_animation_graph_editor/src/egui_inspector_impls.rs @@ -1,7 +1,8 @@ use bevy::{ app::Plugin, asset::{Asset, AssetServer, Assets, Handle, UntypedAssetId}, - ecs::system::Resource, + ecs::{prelude::AppTypeRegistry, system::Resource}, + prelude::App, reflect::{FromReflect, Reflect, TypePath, TypeRegistry}, utils::HashMap, }; @@ -12,6 +13,7 @@ use bevy_animation_graph::{ frame::PoseSpec, parameters::{BoneMask, ParamSpec, ParamValue}, }, + flipping::config::{PatternMapper, PatternMapperSerial}, prelude::OrderedMap, }; use bevy_inspector_egui::{ @@ -25,7 +27,7 @@ use std::{ pub struct BetterInspectorPlugin; impl Plugin for BetterInspectorPlugin { - fn build(&self, app: &mut bevy::prelude::App) { + fn build(&self, app: &mut App) { app.insert_resource(EguiInspectorBuffers::< HashMap, Vec<(EntityPath, f32)>, @@ -49,7 +51,9 @@ impl Plugin for BetterInspectorPlugin { app.register_type::>(); app.register_type::>(); - let type_registry = app.world.resource::(); + PatternMapperInspector::register(app); + + let type_registry = app.world.resource::(); let mut type_registry = type_registry.write(); let type_registry = &mut type_registry; @@ -223,6 +227,60 @@ pub fn entity_path_readonly( ui.label(slashed_path); } +pub struct PatternMapperInspector; + +impl PatternMapperInspector { + pub fn register(app: &mut App) { + app.insert_resource(EguiInspectorBuffers::::default()); + let type_registry = app.world.resource::(); + let mut type_registry = type_registry.write(); + let type_registry = &mut type_registry; + add_no_many::(type_registry, Self::mutable, Self::readonly); + } + + pub fn mutable( + value: &mut dyn Any, + ui: &mut egui::Ui, + _options: &dyn Any, + id: egui::Id, + mut env: InspectorUi<'_, '_>, + ) -> bool { + let value = value.downcast_mut::().unwrap(); + let buffered = get_buffered::( + env.context.world.as_mut().unwrap(), + id, + || value.clone().into(), + ); + match env.ui_for_reflect_with_options(buffered, ui, id, &()) { + true => { + if let Ok(mapper) = PatternMapper::try_from(buffered.clone()) { + *value = mapper; + true + } else { + false + } + } + false => false, + } + } + + pub fn readonly( + value: &dyn Any, + ui: &mut egui::Ui, + _options: &dyn Any, + id: egui::Id, + mut env: InspectorUi<'_, '_>, + ) { + let value = value.downcast_ref::().unwrap(); + let buffered = get_buffered::( + env.context.world.as_mut().unwrap(), + id, + || value.clone().into(), + ); + env.ui_for_reflect_readonly_with_options(buffered, ui, id, &()); + } +} + pub fn animation_graph_mut( value: &mut dyn Any, ui: &mut egui::Ui,