mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-28 08:58:07 +00:00
move & update keyframes
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user