diff --git a/studio-frontend/src/app/anim-editor.element.ts b/studio-frontend/src/app/anim-editor.element.ts index 9049e24..a80e41f 100644 --- a/studio-frontend/src/app/anim-editor.element.ts +++ b/studio-frontend/src/app/anim-editor.element.ts @@ -101,6 +101,10 @@ export class AnimEditorElement extends LitElement { keyframes[keyframe] = added[keyframe]; } + for (let keyframe of removed) { + delete keyframes[keyframe]; + } + this.keyframes = keyframes; let min = Infinity diff --git a/studio/src/anim/timeline_animation.rs b/studio/src/anim/timeline_animation.rs index 7541ab7..940bfb0 100644 --- a/studio/src/anim/timeline_animation.rs +++ b/studio/src/anim/timeline_animation.rs @@ -11,14 +11,13 @@ use bevy::{ use bevy_tweening::{ lens::{TransformPositionLens, TransformRotationLens, TransformScaleLens}, - Animator, EaseMethod, Sequence, Targetable, Tracks, Tween, - TweenCompleted, + Animator, EaseMethod, Sequence, Targetable, Tracks, Tween, TweenCompleted, }; use itertools::Itertools; use serde::{Deserialize, Serialize}; use space_editor::space_prefab::editor_registry::EditorRegistryExt; -use crate::interaction::Selectable; +use crate::interaction::{MarkedForDeletion, Selectable}; pub struct TimelinePlugin; @@ -53,6 +52,7 @@ impl Plugin for TimelinePlugin { timeline_bound_component_animator_system::, ), ); + app.add_systems(PostUpdate, generate_transform_animation); @@ -77,6 +77,8 @@ impl Plugin for TimelinePlugin { } } +const MIN_KEYFRAME_DISTANCE: f32 = 0.01; + #[derive(Event, Clone, Copy, Debug)] pub enum TimelineInteractionEvent { Play, @@ -88,7 +90,7 @@ pub enum TimelineInteractionEvent { pub enum KeyframeInteractionEvent { RecordKeyframe { target: Entity }, MoveKeyframeTime { target: Entity, time: f32 }, - AddEntityTrack { target: Entity } + AddEntityTrack { target: Entity }, } #[derive(Resource, Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize)] @@ -227,29 +229,50 @@ fn process_keyframe_events( r_global_seek_time: Res, q_animation_targets: Query<(Entity, &Transform), With>, q_potential_targets: Query, Without)>, - mut q_keyframes: Query<&mut Keyframe>, + mut q_keyframes: Query<(Entity, &mut Keyframe)>, ) { for event in e_timeline_event.read() { info!("Processing Keyframe Event {event:?}"); match event { KeyframeInteractionEvent::RecordKeyframe { target } => { if let Ok((target, transform)) = q_animation_targets.get(*target) { - commands.spawn(( - SpatialBundle { - transform: *transform, - ..Default::default() - }, - Selectable, - Keyframe { - target, - time: r_global_seek_time.time, - }, - TransformKeyframe::default(), - )); + let mut existing = None; + for (entity, keyframe) in q_keyframes.iter_mut() { + if keyframe.target == target + && (keyframe.time - r_global_seek_time.time).abs() + < MIN_KEYFRAME_DISTANCE + { + existing = Some(entity); + break; + } + } + if let Some(existing) = existing { + commands.entity(existing).insert(( + *transform, + TransformKeyframe::default(), + Keyframe { + target, + time: r_global_seek_time.time, + }, + )); + } else { + commands.spawn(( + SpatialBundle { + transform: *transform, + ..Default::default() + }, + Selectable, + Keyframe { + target, + time: r_global_seek_time.time, + }, + TransformKeyframe::default(), + )); + } } } KeyframeInteractionEvent::MoveKeyframeTime { target, time } => { - if let Ok(mut keyframe) = q_keyframes.get_mut(*target) { + if let Ok((_, mut keyframe)) = q_keyframes.get_mut(*target) { keyframe.time = *time; } } @@ -260,7 +283,7 @@ fn process_keyframe_events( } else { warn!("Can't add animation target to entity"); } - }, + } } } } @@ -268,11 +291,16 @@ fn process_keyframe_events( fn generate_transform_animation( mut commands: Commands, changed: Query<&Keyframe, (With, Changed)>, - keyframes: Query<(&Keyframe, &Transform, &TransformKeyframe, Entity)>, + keyframes: Query<(&Keyframe, &Transform, &TransformKeyframe, Entity), Without>, + removed: Query<&Keyframe, With>, ) { let changed_targets = changed .iter() .map(|keyframe| keyframe.target) + .chain(removed.iter().map(|keyframe| { + info!("Processing a removed keyframe {keyframe:?}"); + keyframe.target + })) .collect::>(); let keyframes = keyframes .iter() @@ -467,13 +495,11 @@ mod wasm { use wasm_bindgen::prelude::*; use crate::{ - scene::SceneElement, - wasm::{FromStr, StaticBuffer, WasmMessageBuffer}, + interaction::MarkedForDeletion, scene::SceneElement, wasm::{FromStr, StaticBuffer, WasmMessageBuffer} }; use super::{ - AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent, - TimedAnimationClipAsset, TimelineInteractionEvent, TransformKeyframe, + AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent, TimedAnimationClipAsset, TimelineInteractionEvent, TransformKeyframe }; static SEEK_TIME: WasmMessageBuffer = WasmMessageBuffer::new(); @@ -522,9 +548,7 @@ mod wasm { let Ok(target) = Entity::from_str(&entity) else { return; }; - KEYFRAME_EVENT.write_message(vec![KeyframeInteractionEvent::AddEntityTrack { - target - }]); + KEYFRAME_EVENT.write_message(vec![KeyframeInteractionEvent::AddEntityTrack { target }]); } pub(super) fn update_global_seek_time( @@ -734,7 +758,7 @@ mod wasm { (Entity, &Keyframe, &TimedAnimationClipAsset), Or<(Changed, Changed)>, >, - mut removed: bevy::prelude::RemovedComponents, + removed: Query>, ) -> Result<(), String> { let Ok(window) = q_window.get_single() else { return Ok(()); @@ -775,7 +799,7 @@ mod wasm { })) .collect::, String>>()?; - let removed = removed.read().map(|v| format!("{v:?}")).collect::>(); + let removed = removed.iter().map(|v| format!("{v:?}")).collect::>(); if added.is_empty() && removed.is_empty() { return Ok(()); diff --git a/studio/src/interaction/selection.rs b/studio/src/interaction/selection.rs index 3c42ae9..997996e 100644 --- a/studio/src/interaction/selection.rs +++ b/studio/src/interaction/selection.rs @@ -27,6 +27,7 @@ impl Plugin for SelectionPlugin { PostUpdate, draw_selection_outline.run_if(resource_changed_or_removed::()), ) + .add_systems(First, finalize_delete) .add_systems( OnEnter(SceneState::Active), (apply_deferred, tag_scene_elements) @@ -158,6 +159,9 @@ fn update_selection( } } +#[derive(Component)] +pub struct MarkedForDeletion; + fn process_delete_inputs( mut cmd: Commands, mut er_input: EventReader, @@ -172,7 +176,7 @@ fn process_delete_inputs( }) { let entity = **r_selection; - cmd.entity(entity).despawn_recursive(); + cmd.entity(entity).insert(MarkedForDeletion); cmd.remove_resource::(); ew_changes.send(NewChange { change: Arc::new(RemovedEntity { entity }), @@ -180,6 +184,15 @@ fn process_delete_inputs( } } +fn finalize_delete( + mut cmd: Commands, + q: Query> +) { + for entity in &q { + cmd.entity(entity).despawn_recursive(); + } +} + fn draw_selection_outline( mut cmd: Commands, q_selected: Query<&Children, With>,