From 67a12e42e2867974b6aad65eacf8a25e2c7392a8 Mon Sep 17 00:00:00 2001 From: Lee-Orr Date: Thu, 21 Mar 2024 20:35:25 -0400 Subject: [PATCH] Timeline Saving/Loading, Cameras, and Headless Playback (#98) --- .gitignore | 2 + studio-bvh/src/macros.rs | 1 + .../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 | 210 +++++++++++ studio/assets/scenes/test-3.scn.ron | 337 ++++++++++++++++++ studio/src/anim/timeline_animation.rs | 178 ++++++--- studio/src/headless.rs | 43 ++- studio/src/main-headless.rs | 15 +- studio/src/scene.rs | 24 +- 11 files changed, 748 insertions(+), 81 deletions(-) create mode 100644 studio/assets/scenes/test-2.scn.ron create mode 100644 studio/assets/scenes/test-3.scn.ron diff --git a/.gitignore b/.gitignore index aaee5b4..537644e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ Thumbs.db /target /screen_shots + +/frames \ No newline at end of file diff --git a/studio-bvh/src/macros.rs b/studio-bvh/src/macros.rs index 026470c..6e17800 100644 --- a/studio-bvh/src/macros.rs +++ b/studio-bvh/src/macros.rs @@ -501,6 +501,7 @@ mod tests { use std::time::Duration; // #[test] // FIXME + #[allow(dead_code)] fn macro_create() { let bvh = bvh! { HIERARCHY 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 +131,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, + pub time: f32, } #[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)] @@ -147,17 +147,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 +188,61 @@ 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, @@ -217,9 +263,16 @@ fn process_timeline_events( } } -fn update_tick(mut r_global_seek_time: ResMut, time: Res