From 081443e04deedde22b337c6dcce9d29e00197b3d Mon Sep 17 00:00:00 2001 From: Lee-Orr Date: Thu, 21 Mar 2024 17:27:05 -0400 Subject: [PATCH] convert animation target to use an enum w/ values as a hash, rather than Entity. --- .../src/app/anim-editor.element.ts | 13 +- .../src/app/anim-timeline.element.ts | 5 +- studio/Cargo.toml | 1 + studio/assets/scenes/test-2.scn.ron | 269 ++++++++------ studio/assets/scenes/test-3.scn.ron | 337 ++++++++++++++++++ studio/src/anim/timeline_animation.rs | 136 ++++--- studio/src/scene.rs | 6 +- 7 files changed, 615 insertions(+), 152 deletions(-) create mode 100644 studio/assets/scenes/test-3.scn.ron diff --git a/studio-frontend/src/app/anim-editor.element.ts b/studio-frontend/src/app/anim-editor.element.ts index c27ff19..77b9874 100644 --- a/studio-frontend/src/app/anim-editor.element.ts +++ b/studio-frontend/src/app/anim-editor.element.ts @@ -47,9 +47,9 @@ implements TreeSelectionProvider { @state() playHead = 0; @state() playing = false; - @state() tracks : Record = {}; + @state() tracks : Record = {}; - @state() track_ordered : string[] = []; + @state() track_ordered : [string, studio.AnimationTarget][] = []; @state() keyframes : Record = {}; @@ -112,7 +112,7 @@ implements TreeSelectionProvider { delete tracks[track_entity]; } - this.track_ordered = Object.keys(tracks); + this.track_ordered = Object.keys(tracks).map(entity => [entity,tracks[entity].animation_target]); if (typeof this.selectedTrack === "object") { if (!isOutsideEntity(this.selectedTrack) && !(this.selectedTrack.track in tracks)) { @@ -262,9 +262,11 @@ implements TreeSelectionProvider { protected override render(): TemplateResult { const track_order_hashed : Record = {}; + const track_animation_target_order_hashed : Record = {}; - for (const [index, track] of this.track_ordered.entries()) { + for (const [index, [track, target]] of this.track_ordered.entries()) { track_order_hashed[track] = index; + track_animation_target_order_hashed[target] = index; } const selected = !isOutsideEntity(this.selectedTrack) && this.selectedTrack?.track || false; @@ -282,7 +284,7 @@ implements TreeSelectionProvider { '--clip-start': `${clip_start * 100}%` }; - const tracklist = this.track_ordered.map((entity) => { + const tracklist = this.track_ordered.map(([entity,]) => { let track = this.tracks[entity]; let name = track.name ?? track.target_type; let icon = this.sceneData?.entityMap.get(entity)?.icon ?? "minus"; @@ -381,6 +383,7 @@ implements TreeSelectionProvider { .playHead=${bind(this, "playHead")} .keyframes=${bind(this, "keyframes")} .track_ordered=${track_order_hashed} + .track_animation_target_ordered=${track_animation_target_order_hashed} > ` }; diff --git a/studio-frontend/src/app/anim-timeline.element.ts b/studio-frontend/src/app/anim-timeline.element.ts index 21db416..7717818 100644 --- a/studio-frontend/src/app/anim-timeline.element.ts +++ b/studio-frontend/src/app/anim-timeline.element.ts @@ -69,7 +69,8 @@ export class AnimTimelineElement extends LitElement { @property({ type: Number }) playHead = 0; /** Keyframe positions (in seconds) */ @property({ attribute: false }) keyframes: Record = {}; - @property({ attribute: false }) track_ordered: Record = {} + @property({ attribute: false }) track_ordered: Record = {}; + @property({ attribute: false }) track_animation_target_ordered: Record = {}; @state() _selectedKeyframe: string | null = null; @@ -344,7 +345,7 @@ export class AnimTimelineElement extends LitElement { Object.entries(this.keyframes), ([id]) => id, ([id, keyframe]) => { - let row = this.track_ordered[keyframe.target]; + let row = this.track_animation_target_ordered[keyframe.target]; return html`
() .editor_registry::() .register_type::() + .register_type::() .add_event::() .add_event::(); @@ -131,10 +132,10 @@ impl Default for TransformKeyframe { } #[derive(Component, Clone, Debug, Reflect, PartialEq)] -#[reflect(Component, MapEntities)] +#[reflect(Component)] pub struct Keyframe { - target: Entity, - time: f32, + target: AnimationTarget, + time: f32 } #[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)] @@ -147,17 +148,11 @@ impl PartialOrd for Keyframe { } } -impl MapEntities for Keyframe { - fn map_entities(&mut self, entity_mapper: &mut EntityMapper) { - self.target = entity_mapper.get_or_reserve(self.target); - } -} - impl Default for Keyframe { fn default() -> Self { Self { - target: Entity::PLACEHOLDER, time: Default::default(), + target: AnimationTarget::Unknown } } } @@ -194,9 +189,59 @@ impl Default for TimedAnimationClipAsset { } } -#[derive(Component, Clone, Debug, Default, Reflect)] +#[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)] #[reflect(Component)] -pub struct AnimationTarget; +#[serde(try_from = "&str", into = "String")] +pub enum AnimationTarget { + #[default] + Unknown, + MainCamera, + Uuid(uuid::Uuid) +} + +impl AnimationTarget { + pub fn new() -> Self { + Self::Uuid(uuid::Uuid::new_v4()) + } +} + +impl ToString for AnimationTarget { + fn to_string(&self) -> String { + match self { + Self::Unknown => "unknown".to_string(), + Self::MainCamera => "MainCamera".to_string(), + Self::Uuid(uuid) => uuid.to_string() + } + } +} + +impl From for String { + fn from(val: AnimationTarget) -> Self { + val.to_string() + } +} + +impl TryFrom<&str> for AnimationTarget { + type Error = uuid::Error; + + fn try_from(value: &str) -> Result { + value.parse() + } +} + +impl FromStr for AnimationTarget { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + if s == "unknown" { + Ok(Self::Unknown) + } else if s == "MainCamera" { + Ok(Self::MainCamera) + } else { + Ok(Self::Uuid(s.parse()?)) + } + } +} fn process_timeline_events( mut e_timeline_event: EventReader, @@ -229,7 +274,7 @@ fn process_keyframe_events( mut commands: Commands, mut e_timeline_event: EventReader, r_global_seek_time: Res, - q_animation_targets: Query<(Entity, &Transform), With>, + q_animation_targets: Query<(Entity, &Transform, &AnimationTarget)>, q_potential_targets: Query, Without)>, mut q_keyframes: Query<(Entity, &mut Keyframe)>, ) { @@ -237,10 +282,10 @@ fn process_keyframe_events( info!("Processing Keyframe Event {event:?}"); match event { KeyframeInteractionEvent::RecordKeyframe { target } => { - if let Ok((target, transform)) = q_animation_targets.get(*target) { + if let Ok((target, transform, animation_target)) = q_animation_targets.get(*target) { let mut existing = None; for (entity, keyframe) in q_keyframes.iter_mut() { - if keyframe.target == target + if keyframe.target == *animation_target && (keyframe.time - r_global_seek_time.time).abs() < MIN_KEYFRAME_DISTANCE { @@ -253,8 +298,8 @@ fn process_keyframe_events( *transform, TransformKeyframe::default(), Keyframe { - target, time: r_global_seek_time.time, + target: *animation_target, }, )); } else { @@ -266,8 +311,8 @@ fn process_keyframe_events( }, Selectable, Keyframe { - target, time: r_global_seek_time.time, + target: *animation_target, }, TransformKeyframe::default(), )); @@ -282,7 +327,7 @@ fn process_keyframe_events( KeyframeInteractionEvent::AddEntityTrack { target } => { if let Ok(target) = q_potential_targets.get(*target) { info!("Adding Animation Target to Entity"); - commands.entity(target).insert(AnimationTarget); + commands.entity(target).insert(AnimationTarget::new()); } else { warn!("Can't add animation target to entity"); } @@ -304,10 +349,11 @@ fn generate_transform_animation( (&Keyframe, &Transform, &TransformKeyframe, Entity), Without, >, - added_targets: Query>, - valid_targets: Query>, + added_targets: Query<(Entity, &AnimationTarget), Added>, + valid_targets: Query<(Entity, &AnimationTarget)>, removed: Query<&Keyframe, With>, ) { + let available_targets = valid_targets.iter().map(|(e,a)| (*a,e)).collect::>(); let changed_targets = changed .iter() .map(|keyframe| keyframe.target) @@ -315,18 +361,20 @@ fn generate_transform_animation( info!("Processing a removed keyframe {keyframe:?}"); keyframe.target })) - .chain(added_targets.iter()) + .chain(added_targets.iter().map(|(_,a)| *a)) .collect::>(); let keyframes = keyframes .iter() - .filter(|(keyframe, _, _, _)| changed_targets.contains(&keyframe.target)) - .group_by(|(keyframe, _, _, _)| keyframe.target); + .filter(|(keyframe, _,_,_)| changed_targets.contains(&keyframe.target)) + .group_by(|(keyframe,_, _, _)| keyframe.target); for (target, keyframes) in &keyframes { - if !valid_targets.contains(target) { + let Some(target) = available_targets.get(&target) else { info!("No valid target."); continue; - } + }; + let target = *target; + let keyframes = keyframes.sorted_by(|a, b| a.0.time.total_cmp(&b.0.time)); let (_, _, _, translation, rotation, scale) = keyframes.fold( @@ -604,13 +652,13 @@ mod wasm { export interface UpdateAnimationTargets extends CustomEvent { type: "update-animation-targets", detail: { - added: Record, + added: Record, removed: string[] } } export interface TransformKeyframeInfo { - target: string, + target: AnimationTarget, time: number, entity: string, incoming_easing: "linear" | "cubic-hermite", @@ -620,7 +668,7 @@ mod wasm { } export interface TimedAnimationClipInfo { - target: string, + target: AnimationTarget, time: number, entity: string, type: 'animation-clip', @@ -631,6 +679,8 @@ mod wasm { export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo; + export type AnimationTarget = string; + export interface UpdateKeyframes extends CustomEvent { type: "update-keyframes", detail: { @@ -652,6 +702,7 @@ mod wasm { struct AddedAnimationTargetRecordItem { name: Option, target_type: SceneElement, + animation_target: AnimationTarget } #[derive(Serialize, Deserialize)] @@ -663,7 +714,7 @@ mod wasm { #[derive(Serialize, Deserialize)] struct TimedAnimationClipInfo { entity: String, - target: String, + target: AnimationTarget, time: f32, clip: String, duration: f32, @@ -674,7 +725,7 @@ mod wasm { #[serde(rename_all = "camelCase")] struct TransformKeframeInfo { entity: String, - target: String, + target: AnimationTarget, time: f32, incoming_easing: EaseType, affect_translation: bool, @@ -690,13 +741,13 @@ mod wasm { pub(super) fn notify_updated_animation_targets( q_window: Query<&Window, With>, - q_cameras: Query<(Entity, Option<&Name>), (Added, With)>, + q_cameras: Query<(Entity, Option<&Name>, &AnimationTarget), (Added, With)>, q_scene_elements: Query< - (Entity, Option<&Name>, &SceneElement), + (Entity, Option<&Name>, &SceneElement, &AnimationTarget), (Added, Without), >, q_transforms: Query< - (Entity, Option<&Name>), + (Entity, Option<&Name>, &AnimationTarget), ( Added, With, @@ -712,30 +763,33 @@ mod wasm { let added = q_transforms .iter() - .map(|(entity, name)| { + .map(|(entity, name, target)| { ( format!("{entity:?}"), AddedAnimationTargetRecordItem { name: name.map(|v| v.to_string()), target_type: SceneElement::Generic, + animation_target: *target }, ) }) - .chain(q_cameras.iter().map(|(entity, name)| { + .chain(q_cameras.iter().map(|(entity, name, target)| { ( format!("{entity:?}"), AddedAnimationTargetRecordItem { name: name.map(|v| v.to_string()), target_type: SceneElement::Camera, + animation_target: *target }, ) })) - .chain(q_scene_elements.iter().map(|(entity, name, element)| { + .chain(q_scene_elements.iter().map(|(entity, name, element, target)| { ( format!("{entity:?}"), AddedAnimationTargetRecordItem { name: name.map(|v| v.to_string()), target_type: *element, + animation_target: *target }, ) })) @@ -777,7 +831,7 @@ mod wasm { .map(|(entity, keyframe, transform_keyframe)| { match serde_json::to_value(TransformKeframeInfo { entity: format!("{entity:?}"), - target: format!("{:?}", keyframe.target), + target: keyframe.target, time: keyframe.time, incoming_easing: transform_keyframe.incoming_easing, affect_translation: transform_keyframe.affect_translation, @@ -793,7 +847,7 @@ mod wasm { .chain(q_clips.iter().map(|(entity, keyframe, clip)| { match serde_json::to_value(TimedAnimationClipInfo { entity: format!("{entity:?}"), - target: format!("{:?}", keyframe.target), + target: keyframe.target, time: keyframe.time, clip: clip.clip.id().to_string(), duration: clip.duration, diff --git a/studio/src/scene.rs b/studio/src/scene.rs index 4302613..fa3f778 100644 --- a/studio/src/scene.rs +++ b/studio/src/scene.rs @@ -106,7 +106,7 @@ impl Plugin for ScenePlugin { fn minimal_scene_setup(mut cmd: Commands) { cmd.spawn(( Name::new("Directional Light"), - AnimationTarget, + AnimationTarget::new(), DirectionalLightBundle { directional_light: DirectionalLight { shadows_enabled: false, @@ -271,7 +271,7 @@ fn initialize(mut cmd: Commands) { cmd.spawn(( Name::new("Main Camera"), MainCamera, - AnimationTarget, + AnimationTarget::MainCamera, ChangeTracker, Camera3dBundle { transform: camera_xform, @@ -328,7 +328,7 @@ fn initialiaze_headless( cmd.spawn(( Name::new("Main Camera"), MainCamera, - AnimationTarget, + AnimationTarget::MainCamera, Camera3dBundle { transform: camera_xform, tonemapping: Tonemapping::BlenderFilmic,