move & update keyframes

This commit is contained in:
Lee-Orr
2024-03-14 18:48:03 -04:00
parent baa9431c39
commit 7c7435d2cc
6 changed files with 502 additions and 134 deletions
@@ -34,7 +34,11 @@
background: #7A2C34E3; background: #7A2C34E3;
color: #FFF; color: #FFF;
&:hover { &.disabled {
opacity: 0.25;
}
&:not(.disabled):hover {
background: #90363F; background: #90363F;
} }
} }
@@ -95,4 +99,21 @@
display: grid; display: grid;
grid-template-columns: subgrid; grid-template-columns: subgrid;
grid-row: 2 / span var(--track-count); grid-row: 2 / span var(--track-count);
.track {
&:hover {
background: #0000001F;
}
&.selected {
background: #7A2C34E3;
color: #FFF;
font-weight: 500;
&:hover {
background: #90363F !important;
}
}
}
} }
+59 -16
View File
@@ -1,5 +1,5 @@
import * as studio from "@storyteller/studio"; import * as studio from "@storyteller/studio";
import { type CameraKeyframesEvent, type SeekTimeChanged, type UpdateAnimationTargets } from "@storyteller/studio"; import type { EntitySelectEvent, SeekTimeChanged, UpdateAnimationTargets } from "@storyteller/studio";
import { bind, drag, on } from "@storyteller/framework"; import { bind, drag, on } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult, PropertyValueMap } from "lit"; import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult, PropertyValueMap } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
@@ -37,9 +37,9 @@ export class AnimEditorElement extends LitElement {
@state() track_ordered : string[] = []; @state() track_ordered : string[] = [];
@state() keyframes : Record<string, number[]> = {}; @state() keyframes : Record<string, Keyframe> = {};
@state() selected_track : string | null = null; @state() selected_track : string | false = false;
trackCount(): number { trackCount(): number {
return Object.keys(this.tracks).length; return Object.keys(this.tracks).length;
@@ -81,12 +81,39 @@ export class AnimEditorElement extends LitElement {
this.tracks = tracks; this.tracks = tracks;
} }
@on("window:update-keyframes")
onUpdateKeyframes({detail: {added, removed}}: studio.UpdateKeyframes): void {
let addedKeyframes = Object.keys(added);
if (addedKeyframes.length === 0 && removed.length === 0) {
return;
}
console.log("Keyframes Changed");
let keyframes = {...this.keyframes};
for (let keyframe of addedKeyframes) {
keyframes[keyframe] = added[keyframe];
}
this.keyframes = keyframes;
}
@on("window:entity-select")
onEntitySelected({ detail: id }: EntitySelectEvent): void {
if (id && this.tracks[id]) {
this.selected_track = id;
} else {
this.selected_track = false;
}
}
onSidebarResize(event: PointerEvent): void { onSidebarResize(event: PointerEvent): void {
this.sidebarWidth = Math.max(Math.min(this.sidebarWidth + event.movementX, 512), 128); this.sidebarWidth = Math.max(Math.min(this.sidebarWidth + event.movementX, 512), 128);
} }
addKeyframe(): void { addKeyframe(): void {
studio.addCameraKeyframe(this.playHead); if (this.selected_track) {
studio.recordKeyframe([this.selected_track]);
}
} }
jumpToStart(): void { jumpToStart(): void {
@@ -123,6 +150,31 @@ export class AnimEditorElement extends LitElement {
'--track-count': `${this.trackCount()}`, '--track-count': `${this.trackCount()}`,
'--row-count': `calc(var(--track-count) + 1)` '--row-count': `calc(var(--track-count) + 1)`
}; };
const track_order_hashed : Record<string, number> = {};
for (const [index, track] of this.track_ordered.entries()) {
track_order_hashed[track] = index;
}
const tracklist = this.track_ordered.map((entity) => {
let track = this.tracks[entity];
let name = track.name ?? track.target_type;
let icon = TRACK_ICONS[track.target_type] ?? "minus";
return html`
<sts-tree class=${`track ` + (this.selected_track === entity ? `selected` : ``)}
role="listitem"
name=${name}
icon=${icon}
@click=${() => {
studio.select(entity);
}}
></sts-tree>
`;
});
const none_selected = !this.selected_track;
return html` return html`
<div class="sidebar" <div class="sidebar"
style=${styleMap({ style=${styleMap({
@@ -136,6 +188,7 @@ export class AnimEditorElement extends LitElement {
icon="key" icon="key"
aria-label="Add Keyframe" aria-label="Add Keyframe"
title="Add Keyframe" title="Add Keyframe"
?disabled=${none_selected}
@click=${this.addKeyframe} @click=${this.addKeyframe}
></sts-button> ></sts-button>
</sts-toolbar-group> </sts-toolbar-group>
@@ -173,18 +226,7 @@ export class AnimEditorElement extends LitElement {
</sts-toolbar-group> </sts-toolbar-group>
</sts-toolbar> </sts-toolbar>
<div class="tracklist" role="list"> <div class="tracklist" role="list">
${ ${tracklist}
this.track_ordered.map((entity) => {
let track = this.tracks[entity];
let name = track.name ?? track.target_type;
let icon = TRACK_ICONS[track.target_type] ?? "minus";
return html!`<sts-tree class="track"
role="listitem"
name=${name}
icon=${icon}
></sts-tree>`;
})
}
</div> </div>
</div> </div>
@@ -202,6 +244,7 @@ export class AnimEditorElement extends LitElement {
.clipEnd=${this.clipEnd} .clipEnd=${this.clipEnd}
.playHead=${bind(this, "playHead")} .playHead=${bind(this, "playHead")}
.keyframes=${bind(this, "keyframes")} .keyframes=${bind(this, "keyframes")}
.track_ordered=${track_order_hashed}
></sts-anim-timeline> ></sts-anim-timeline>
` `
}; };
@@ -1,6 +1,9 @@
:host { :host {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: grid;
grid-template-rows: subgrid;
grid-row: 1 / span var(--row-count);
} }
*:focus-visible { *:focus-visible {
@@ -8,21 +11,18 @@
} }
.wrapper { .wrapper {
position: absolute; position: relative;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: subgrid;
inset: 0; inset: 0;
grid-row: 1 / span var(--row-count);
&.panning { &.panning {
cursor: grabbing; cursor: grabbing;
} }
} }
.clip-region {
position: absolute;
top: 0;
bottom: 0;
background: #0004;
}
.hash { .hash {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -37,7 +37,7 @@
.timestamp { .timestamp {
position: absolute; position: absolute;
top: 8px; grid-row: 1;
color: #FFF6; color: #FFF6;
font: { font: {
family: "Fira Sans", sans-serif; family: "Fira Sans", sans-serif;
@@ -100,8 +100,7 @@
.keyframe { .keyframe {
position: absolute; position: absolute;
top: 50%; transform: translateX(-4.5px);
transform: translateX(-4.5px) translateY(-4.5px);
&::before { &::before {
content: ""; content: "";
@@ -62,7 +62,8 @@ export class AnimTimelineElement extends LitElement {
/** The time (in seconds) of the current play-head position */ /** The time (in seconds) of the current play-head position */
@property({ type: Number }) playHead = 0; @property({ type: Number }) playHead = 0;
/** Keyframe positions (in seconds) */ /** Keyframe positions (in seconds) */
@property({ attribute: false }) keyframes: Record<string, number> = {}; @property({ attribute: false }) keyframes: Record<string, studio.Keyframe> = {};
@property({ attribute: false }) track_ordered: Record<string, number> = {}
@state() _selectedKeyframe: string | null = null; @state() _selectedKeyframe: string | null = null;
@@ -131,8 +132,9 @@ export class AnimTimelineElement extends LitElement {
} }
if (Array.from(Object.keys(this.keyframes)).includes(id)) { if (Array.from(Object.keys(this.keyframes)).includes(id)) {
let keyframe = this.keyframes[id];
this._selectedKeyframe = id; this._selectedKeyframe = id;
this.dispatchEvent(new CustomEvent("playHead-change", { detail: this.keyframes[id]! })); studio.seekTime(keyframe.time);
} else { } else {
this._selectedKeyframe = null; this._selectedKeyframe = null;
} }
@@ -190,20 +192,12 @@ export class AnimTimelineElement extends LitElement {
} }
onKeyframeDrag(id: string, event: PointerEvent): void { onKeyframeDrag(id: string, event: PointerEvent): void {
const deltaSeconds = event.movementX * this.secondsPerPx.get(); let elementBounds = this.getBoundingClientRect();
const updated = this.keyframes[id] + deltaSeconds; let relativeX = event.clientX - elementBounds.left;
let ratioX = Math.max(0, Math.min(relativeX / elementBounds.width, 1));
const time = (this.rangeEnd - this.rangeStart) * ratioX + this.rangeStart;
studio.updateKeyframe(id, updated); studio.moveKeyframeTime(id, time);
const updatedKeyframes = Object.fromEntries(
Object
.entries(this.keyframes)
.map(entry => entry[0] === id
? [id, updated]
: entry
)
);
this.dispatchEvent(new CustomEvent("keyframes-change", { detail: updatedKeyframes }));
} }
#updateRenderWidth(): void { #updateRenderWidth(): void {
@@ -296,10 +290,6 @@ export class AnimTimelineElement extends LitElement {
})} })}
tabindex="-1" tabindex="-1"
> >
<div class="clip-region"
style=${styleMap({
})}
></div>
${subdivisions.map(({ seconds, fraction, isMajor}, idx) => { ${subdivisions.map(({ seconds, fraction, isMajor}, idx) => {
const secondsPercentage = fraction * 100; const secondsPercentage = fraction * 100;
@@ -342,14 +332,17 @@ export class AnimTimelineElement extends LitElement {
${repeat( ${repeat(
Object.entries(this.keyframes), Object.entries(this.keyframes),
([id]) => id, ([id]) => id,
([id, keyframe]) => html` ([id, keyframe]) => {
let row = this.track_ordered[keyframe.target];
return html`
<div id=${id} <div id=${id}
class=${classMap({ class=${classMap({
keyframe: true, keyframe: true,
selected: this._selectedKeyframe === id, selected: this._selectedKeyframe === id
})} })}
style=${styleMap({ style=${styleMap({
left: `${100 * (keyframe - this.rangeStart) / range}%` left: `${100 * (keyframe.time - this.rangeStart) / range}%`,
gridRow: row + 2,
})} })}
tabindex="-1" tabindex="-1"
${drag({ ${drag({
@@ -357,7 +350,8 @@ export class AnimTimelineElement extends LitElement {
})} })}
@click=${() => this.selectKeyframe(id)} @click=${() => this.selectKeyframe(id)}
></div> ></div>
` `;
}
)} )}
</div> </div>
`; `;
+386 -82
View File
@@ -5,37 +5,53 @@ use bevy::{
}, },
prelude::*, prelude::*,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use space_editor::space_prefab::editor_registry::EditorRegistryExt; use space_editor::space_prefab::editor_registry::EditorRegistryExt;
use crate::interaction::Selectable;
pub struct TimelinePlugin; pub struct TimelinePlugin;
impl Plugin for TimelinePlugin { impl Plugin for TimelinePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.register_type::<TimedAnimationClip>() app.register_type::<TimedAnimationClipAsset>()
.editor_registry::<TimedAnimationClip>() .editor_registry::<TimedAnimationClipAsset>()
.register_type::<AnimationTarget>() .register_type::<AnimationTarget>()
.editor_registry::<AnimationTarget>() .editor_registry::<AnimationTarget>()
.add_event::<TimelineEvent>(); .register_type::<TransformKeyframe>()
.editor_registry::<TransformKeyframe>()
.register_type::<Keyframe>()
.editor_registry::<Keyframe>()
.register_type::<KeyframeOrder>()
.editor_registry::<KeyframeOrder>()
.add_event::<TimelineInteractionEvent>()
.add_event::<KeyframeInteractionEvent>();
app.insert_resource(GlobalSeekTime { app.insert_resource(GlobalSeekTime {
time: 0., time: 0.,
playing: false, playing: false,
}); });
app.add_systems(PreUpdate, app.add_systems(PreUpdate, (process_timeline_events, update_tick).chain());
(process_timeline_events, update_tick).chain(),
); app.add_systems(Update, process_keyframe_events);
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
{ {
app.add_systems(First,
wasm::update_global_seek_time);
app.add_systems( app.add_systems(
Update, First,
(
wasm::update_global_seek_time,
wasm::update_keyframe_interaction,
),
);
app.add_systems(
PostUpdate,
( (
wasm::notify_seek_time_changes.map(bevy::utils::error), wasm::notify_seek_time_changes.map(bevy::utils::error),
wasm::notify_updated_animation_targets.map(bevy::utils::error) wasm::notify_keyframe_changes.map(bevy::utils::error),
wasm::notify_updated_animation_targets.map(bevy::utils::error),
), ),
); );
} }
@@ -43,12 +59,18 @@ impl Plugin for TimelinePlugin {
} }
#[derive(Event, Clone, Copy, Debug)] #[derive(Event, Clone, Copy, Debug)]
pub enum TimelineEvent { pub enum TimelineInteractionEvent {
Play, Play,
Pause, Pause,
Seek(f32), Seek(f32),
} }
#[derive(Event, Clone, Copy, Debug)]
pub enum KeyframeInteractionEvent {
RecordKeyframe { target: Entity },
MoveKeyframeTime { target: Entity, time: f32 },
}
#[derive(Resource, Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize)] #[derive(Resource, Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize)]
#[reflect(Resource)] #[reflect(Resource)]
pub struct GlobalSeekTime { pub struct GlobalSeekTime {
@@ -56,71 +78,110 @@ pub struct GlobalSeekTime {
pub playing: bool, pub playing: bool,
} }
#[derive(Component, Clone, Debug, Reflect, PartialEq)] #[derive(Clone, Debug, Copy, Default, Reflect, PartialEq, Serialize, Deserialize)]
#[reflect(Component, MapEntities)] pub enum EaseType {
pub struct TimedAnimationClip { #[default]
clip: Handle<AnimationClip>, Linear,
start_time: f32, CubicHermite,
duration: f32,
incoming_transition_duration: f32,
speed: f32,
target: Entity,
} }
impl PartialOrd for TimedAnimationClip { #[derive(Component, Clone, Debug, Reflect, PartialEq)]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { #[reflect(Component)]
match self.start_time.partial_cmp(&other.start_time) { pub struct TransformKeyframe {
Some(core::cmp::Ordering::Equal) => {} incoming_easing: EaseType,
ord => return ord, affect_translation: bool,
affect_rotation: bool,
affect_scale: bool,
}
impl Default for TransformKeyframe {
fn default() -> Self {
Self {
incoming_easing: Default::default(),
affect_translation: true,
affect_rotation: true,
affect_scale: true,
} }
}
}
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
#[reflect(Component, MapEntities)]
pub struct Keyframe {
target: Entity,
time: f32,
}
#[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)]
#[reflect(Component)]
pub struct KeyframeOrder(u32);
impl PartialOrd for Keyframe {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.time.partial_cmp(&other.time)
}
}
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(),
}
}
}
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
#[reflect(Component)]
pub struct TimedAnimationClipAsset {
clip: Handle<AnimationClip>,
duration: f32,
speed: f32,
}
impl PartialOrd for TimedAnimationClipAsset {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self.duration.partial_cmp(&other.duration) { match self.duration.partial_cmp(&other.duration) {
Some(core::cmp::Ordering::Equal) => {} Some(core::cmp::Ordering::Equal) => {}
ord => return ord, ord => return ord,
} }
match self.clip.partial_cmp(&other.clip) { match self.speed.partial_cmp(&other.speed) {
Some(core::cmp::Ordering::Equal) => {} Some(core::cmp::Ordering::Equal) => {}
ord => return ord, ord => return ord,
} }
match self None
.incoming_transition_duration
.partial_cmp(&other.incoming_transition_duration)
{
Some(core::cmp::Ordering::Equal) => {}
ord => return ord,
}
self.target.partial_cmp(&other.target)
} }
} }
impl Default for TimedAnimationClip { impl Default for TimedAnimationClipAsset {
fn default() -> Self { fn default() -> Self {
Self { Self {
clip: Default::default(), clip: Default::default(),
start_time: Default::default(),
duration: Default::default(), duration: Default::default(),
speed: 1.0, speed: 1.0,
incoming_transition_duration: Default::default(),
target: Entity::PLACEHOLDER,
} }
} }
} }
impl MapEntities for TimedAnimationClip {
fn map_entities(&mut self, entity_mapper: &mut EntityMapper) {
self.target = entity_mapper.get_or_reserve(self.target);
}
}
#[derive(Component, Clone, Debug, Default, Reflect)] #[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct AnimationTarget; pub struct AnimationTarget;
fn process_timeline_events(mut e_timeline_event: EventReader<TimelineEvent>, mut r_global_seek_time: ResMut<GlobalSeekTime>) { fn process_timeline_events(
mut e_timeline_event: EventReader<TimelineInteractionEvent>,
mut r_global_seek_time: ResMut<GlobalSeekTime>,
) {
for event in e_timeline_event.read() { for event in e_timeline_event.read() {
match event { match event {
TimelineEvent::Play => r_global_seek_time.playing = true, TimelineInteractionEvent::Play => r_global_seek_time.playing = true,
TimelineEvent::Pause => r_global_seek_time.playing = false, TimelineInteractionEvent::Pause => r_global_seek_time.playing = false,
TimelineEvent::Seek(time) => r_global_seek_time.time = *time, TimelineInteractionEvent::Seek(time) => r_global_seek_time.time = *time,
} }
} }
} }
@@ -131,39 +192,116 @@ fn update_tick(mut r_global_seek_time: ResMut<GlobalSeekTime>, time: Res<Time>)
} }
} }
fn process_keyframe_events(
mut commands: Commands,
mut e_timeline_event: EventReader<KeyframeInteractionEvent>,
r_global_seek_time: Res<GlobalSeekTime>,
q_animation_targets: Query<(Entity, &Transform), With<AnimationTarget>>,
mut q_keyframes: Query<&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(),
));
}
}
KeyframeInteractionEvent::MoveKeyframeTime { target, time } => {
if let Ok(mut keyframe) = q_keyframes.get_mut(*target) {
keyframe.time = *time;
}
}
}
}
}
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
mod wasm { mod wasm {
use bevy::{prelude::*, utils::hashbrown::HashMap, window::PrimaryWindow}; use bevy::{prelude::*, utils::hashbrown::HashMap, window::PrimaryWindow};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::{scene::SceneElement, wasm::{StaticBuffer, WasmMessageBuffer}}; use crate::{
scene::SceneElement,
wasm::{FromStr, StaticBuffer, WasmMessageBuffer},
};
use super::{AnimationTarget, GlobalSeekTime, TimelineEvent}; use super::{
AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent,
TimedAnimationClipAsset, TimelineInteractionEvent, TransformKeyframe,
};
static SEEK_TIME: WasmMessageBuffer<TimelineEvent> = WasmMessageBuffer::new(); static SEEK_TIME: WasmMessageBuffer<TimelineInteractionEvent> = WasmMessageBuffer::new();
static KEYFRAME_EVENT: WasmMessageBuffer<Vec<KeyframeInteractionEvent>> =
WasmMessageBuffer::new();
#[wasm_bindgen(js_name = seekTime)] #[wasm_bindgen(js_name = seekTime)]
pub fn seek_time(timestamp: f32) { pub fn seek_time(timestamp: f32) {
SEEK_TIME.write_message(TimelineEvent::Seek(timestamp)); SEEK_TIME.write_message(TimelineInteractionEvent::Seek(timestamp));
} }
#[wasm_bindgen(js_name = play)] #[wasm_bindgen(js_name = play)]
pub fn play() { pub fn play() {
SEEK_TIME.write_message(TimelineEvent::Play); SEEK_TIME.write_message(TimelineInteractionEvent::Play);
} }
#[wasm_bindgen(js_name = pause)] #[wasm_bindgen(js_name = pause)]
pub fn pause() { pub fn pause() {
SEEK_TIME.write_message(TimelineEvent::Pause); SEEK_TIME.write_message(TimelineInteractionEvent::Pause);
} }
pub(super) fn update_global_seek_time(mut e_timeline_event: EventWriter<TimelineEvent>) { #[wasm_bindgen(js_name = recordKeyframe)]
pub fn record_keyframe(entities: Vec<String>) {
KEYFRAME_EVENT.write_message(
entities
.iter()
.filter_map(|entity| Entity::from_str(entity).ok())
.map(|target| KeyframeInteractionEvent::RecordKeyframe { target })
.collect(),
);
}
#[wasm_bindgen(js_name = moveKeyframeTime)]
pub fn move_keyframe_time(entity: String, time: f32) {
let Ok(target) = Entity::from_str(&entity) else {
return;
};
KEYFRAME_EVENT.write_message(vec![KeyframeInteractionEvent::MoveKeyframeTime {
target,
time,
}]);
}
pub(super) fn update_global_seek_time(
mut e_timeline_event: EventWriter<TimelineInteractionEvent>,
) {
if let Some(timestamp) = SEEK_TIME.take_message() { if let Some(timestamp) = SEEK_TIME.take_message() {
e_timeline_event.send(timestamp); e_timeline_event.send(timestamp);
} }
} }
pub(super) fn update_keyframe_interaction(
mut e_keyframe_event: EventWriter<KeyframeInteractionEvent>,
) {
if let Some(keyframes) = KEYFRAME_EVENT.take_message() {
for keyframe in keyframes.into_iter() {
e_keyframe_event.send(keyframe);
}
}
}
#[wasm_bindgen(typescript_custom_section)] #[wasm_bindgen(typescript_custom_section)]
const TIMELINE_TYPES: &str = r#" const TIMELINE_TYPES: &str = r#"
export interface SeekTimeChanged extends CustomEvent { export interface SeekTimeChanged extends CustomEvent {
@@ -177,7 +315,37 @@ mod wasm {
export interface UpdateAnimationTargets extends CustomEvent { export interface UpdateAnimationTargets extends CustomEvent {
type: "update-animation-targets", type: "update-animation-targets",
details: { details: {
added: Record<string, { name?: string, target_type: 'camera' | 'transform' | 'light' | 'bone' | 'skeleton' | 'mesh' >, added: Record<string, { name?: string, target_type: 'camera' | 'transform' | 'light' | 'bone' | 'skeleton' | 'mesh' } >,
removed: string[]
}
}
export interface TransformKeyframeInfo {
target: string,
time: number,
entity: string,
incoming_easing: "linear" | "cubic-hermite",
affect_translation: bool,
affect_rotation: bool,
affect_scale: bool
}
export interface TimedAnimationClipInfo {
target: string,
time: number,
entity: string,
type: 'animation-clip',
clip: string,
duration: number,
speed: number
}
export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo;
export interface UpdateKeyframes extends CustomEvent {
type: "update-keyframes",
details: {
added: Record<string, Keyframe)>,
removed: string[] removed: string[]
} }
} }
@@ -186,55 +354,123 @@ mod wasm {
export interface GlobalEventHandlersEventMap { export interface GlobalEventHandlersEventMap {
"seek-time-changed": CameraKeyframesEvent; "seek-time-changed": CameraKeyframesEvent;
"update-animation-targets": UpdateAnimationTargets; "update-animation-targets": UpdateAnimationTargets;
"update-keyframes": UpdateKeyframes;
} }
} }
"#; "#;
const CAMERA : &str = "camera"; const CAMERA: &str = "camera";
const TRANSFORM : &str = "transform"; const TRANSFORM: &str = "transform";
const LIGHT : &str = "light"; const LIGHT: &str = "light";
const BONE : &str = "bone"; const BONE: &str = "bone";
const SKELETON : &str = "skeleton"; const SKELETON: &str = "skeleton";
const MESH : &str = "mesh"; const MESH: &str = "mesh";
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct AddedAnimationTargetRecordItem { struct AddedAnimationTargetRecordItem {
name: Option<String>, name: Option<String>,
target_type: String target_type: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct UpdatedAnimationTargetsEvent { struct UpdatedAnimationTargetsEvent {
added: HashMap<Entity, AddedAnimationTargetRecordItem>, added: HashMap<String, AddedAnimationTargetRecordItem>,
removed: Vec<Entity> removed: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct TimedAnimationClipInfo {
entity: String,
target: String,
time: f32,
clip: String,
duration: f32,
speed: f32,
}
#[derive(Serialize, Deserialize)]
struct TransformKeframeInfo {
entity: String,
target: String,
time: f32,
incoming_easing: EaseType,
affect_translation: bool,
affect_rotation: bool,
affect_scale: bool,
}
#[derive(Serialize, Deserialize, Debug)]
struct UpdateKeyframes {
added: HashMap<String, serde_json::Value>,
removed: Vec<String>,
} }
pub(super) fn notify_updated_animation_targets( pub(super) fn notify_updated_animation_targets(
q_window: Query<&Window, With<PrimaryWindow>>, q_window: Query<&Window, With<PrimaryWindow>>,
q_cameras: Query<(Entity, Option<&Name>), (Added<AnimationTarget>, With<Camera>)>, q_cameras: Query<(Entity, Option<&Name>), (Added<AnimationTarget>, With<Camera>)>,
q_scene_elements: Query<(Entity, Option<&Name>, &SceneElement), (Added<AnimationTarget>, Without<Camera>)>, q_scene_elements: Query<
q_transforms: Query<(Entity, Option<&Name>), (Added<AnimationTarget>, With<Transform>, Without<Camera>, Without<SceneElement>)>, (Entity, Option<&Name>, &SceneElement),
mut removed: bevy::prelude::RemovedComponents<AnimationTarget> (Added<AnimationTarget>, Without<Camera>),
>,
q_transforms: Query<
(Entity, Option<&Name>),
(
Added<AnimationTarget>,
With<Transform>,
Without<Camera>,
Without<SceneElement>,
),
>,
mut removed: bevy::prelude::RemovedComponents<AnimationTarget>,
) -> Result<(), String> { ) -> Result<(), String> {
let Ok(window) = q_window.get_single() else { let Ok(window) = q_window.get_single() else {
return Ok(()); return Ok(());
}; };
let added = q_transforms.iter().map(|(entity, name)| { let added = q_transforms
(entity, AddedAnimationTargetRecordItem { name: name.map(|v| v.to_string()), target_type: TRANSFORM.to_owned()}) .iter()
}).chain(q_cameras.iter().map(|(entity, name)| (entity, AddedAnimationTargetRecordItem { name: name.map(|v| v.to_string()), target_type: CAMERA.to_owned()}))) .map(|(entity, name)| {
.chain(q_scene_elements.iter().map(|(entity, name, element)| (entity, AddedAnimationTargetRecordItem { name: name.map(|v| v.to_string()), target_type: match element { (
SceneElement::Generic => TRANSFORM, format!("{entity:?}"),
SceneElement::Mesh => MESH, AddedAnimationTargetRecordItem {
SceneElement::Skeleton => SKELETON, name: name.map(|v| v.to_string()),
SceneElement::Bone => BONE, target_type: TRANSFORM.to_owned(),
SceneElement::Light => LIGHT, },
SceneElement::Camera => CAMERA, )
}.to_string()}))).collect::<HashMap<_,_>>(); })
.chain(q_cameras.iter().map(|(entity, name)| {
(
format!("{entity:?}"),
AddedAnimationTargetRecordItem {
name: name.map(|v| v.to_string()),
target_type: CAMERA.to_owned(),
},
)
}))
.chain(q_scene_elements.iter().map(|(entity, name, element)| {
(
format!("{entity:?}"),
AddedAnimationTargetRecordItem {
name: name.map(|v| v.to_string()),
target_type: match element {
SceneElement::Generic => TRANSFORM,
SceneElement::Mesh => MESH,
SceneElement::Skeleton => SKELETON,
SceneElement::Bone => BONE,
SceneElement::Light => LIGHT,
SceneElement::Camera => CAMERA,
}
.to_string(),
},
)
}))
.collect::<HashMap<_, _>>();
let removed = removed.read().collect::<Vec<_>>(); let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>();
if added.is_empty() && removed.is_empty() {
return Ok(());
}
let event = UpdatedAnimationTargetsEvent { added, removed }; let event = UpdatedAnimationTargetsEvent { added, removed };
@@ -245,6 +481,74 @@ mod wasm {
) )
} }
pub(super) fn notify_keyframe_changes(
q_window: Query<&Window, With<PrimaryWindow>>,
q_transform_keyframes: Query<
(Entity, &Keyframe, &TransformKeyframe),
Or<(Changed<Keyframe>, Changed<TransformKeyframe>)>,
>,
q_clips: Query<
(Entity, &Keyframe, &TimedAnimationClipAsset),
Or<(Changed<Keyframe>, Changed<TimedAnimationClipAsset>)>,
>,
mut removed: bevy::prelude::RemovedComponents<Keyframe>,
) -> Result<(), String> {
let Ok(window) = q_window.get_single() else {
return Ok(());
};
let added = q_transform_keyframes
.iter()
.map(|(entity, keyframe, transform_keyframe)| {
match serde_json::to_value(TransformKeframeInfo {
entity: format!("{entity:?}"),
target: format!("{:?}", keyframe.target),
time: keyframe.time,
incoming_easing: transform_keyframe.incoming_easing,
affect_translation: transform_keyframe.affect_translation,
affect_rotation: transform_keyframe.affect_rotation,
affect_scale: transform_keyframe.affect_scale,
})
.map_err(|e| format!("{e}"))
{
Ok(value) => Ok((format!("{entity:?}"), value)),
Err(e) => Err(e),
}
})
.chain(q_clips.iter().map(|(entity, keyframe, clip)| {
match serde_json::to_value(TimedAnimationClipInfo {
entity: format!("{entity:?}"),
target: format!("{:?}", keyframe.target),
time: keyframe.time,
clip: clip.clip.id().to_string(),
duration: clip.duration,
speed: clip.speed,
})
.map_err(|e| format!("{e}"))
{
Ok(value) => Ok((format!("{entity:?}"), value)),
Err(e) => Err(e),
}
}))
.collect::<Result<HashMap<_, _>, String>>()?;
let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>();
if added.is_empty() && removed.is_empty() {
return Ok(());
}
let event = UpdateKeyframes { added, removed };
info!("Updating keyframes: {event:?}");
crate::wasm::dispatch_to_js(
window,
"update-keyframes",
&serde_json::to_value(&event).map_err(|e| format!("{e}"))?,
)
}
pub(super) fn notify_seek_time_changes( pub(super) fn notify_seek_time_changes(
r_global_seek_time: Res<GlobalSeekTime>, r_global_seek_time: Res<GlobalSeekTime>,
q_window: Query<&Window, With<PrimaryWindow>>, q_window: Query<&Window, With<PrimaryWindow>>,
+8 -1
View File
@@ -23,7 +23,14 @@ use wasm_bindgen::prelude::wasm_bindgen;
use wgpu::{TextureDescriptor, TextureUsages}; use wgpu::{TextureDescriptor, TextureUsages};
use crate::{ use crate::{
anim::timeline_animation::AnimationTarget, camera_controller::{CameraController, CameraControllerBundle}, change_tracking::{ChangeTracker, ChangeTrackingPlugin}, debug::DebugPoint, headless::{is_headless, is_not_headless, HeadlessSettings}, math::{Combinable, Extrema}, screenshot::ScreenCapture, MainCamera anim::timeline_animation::AnimationTarget,
camera_controller::{CameraController, CameraControllerBundle},
change_tracking::{ChangeTracker, ChangeTrackingPlugin},
debug::DebugPoint,
headless::{is_headless, is_not_headless, HeadlessSettings},
math::{Combinable, Extrema},
screenshot::ScreenCapture,
MainCamera,
}; };
pub struct ScenePlugin; pub struct ScenePlugin;