mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-16 00:03:29 +00:00
Merge branch 'main' into update-version
This commit is contained in:
@@ -1,208 +0,0 @@
|
||||
use bevy::prelude::*;
|
||||
use itertools::Itertools;
|
||||
use space_editor::space_prefab::editor_registry::EditorRegistryExt;
|
||||
|
||||
use crate::math::{InvLerp, Lerp};
|
||||
|
||||
use super::GlobalSeekTime;
|
||||
|
||||
pub struct CameraAnimPlugin;
|
||||
impl Plugin for CameraAnimPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<CameraKeyframe>()
|
||||
.editor_registry::<CameraKeyframe>()
|
||||
.register_type::<CameraTargetKeyframe>()
|
||||
.editor_registry::<CameraTargetKeyframe>();
|
||||
|
||||
app.add_systems(Update, draw_camera_keyframes);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
wasm::spawn_camera_keyframes,
|
||||
wasm::notify_keyframe_changes.map(bevy::utils::error),
|
||||
wasm::process_update_keyframe,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct CameraKeyframe(pub f32);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct CameraTargetKeyframe(pub f32);
|
||||
|
||||
fn draw_camera_keyframes(
|
||||
mut gizmos: Gizmos,
|
||||
r_seek_time: Res<GlobalSeekTime>,
|
||||
q_cam_keyframe_xforms: Query<&GlobalTransform, With<CameraKeyframe>>,
|
||||
q_cam_target_keyframe_xforms: Query<&GlobalTransform, With<CameraTargetKeyframe>>,
|
||||
q_cam_keyframes: Query<(&CameraKeyframe, &GlobalTransform)>,
|
||||
) {
|
||||
if q_cam_keyframes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for world_xform in q_cam_keyframe_xforms
|
||||
.iter()
|
||||
.chain(q_cam_target_keyframe_xforms.iter())
|
||||
{
|
||||
let origin = world_xform.translation();
|
||||
let x_axis = world_xform.right() * 0.25;
|
||||
let y_axis = world_xform.up() * 0.25;
|
||||
let z_axis = world_xform.back() * 0.25;
|
||||
|
||||
gizmos.line(origin, origin + x_axis, Color::RED);
|
||||
gizmos.line(origin, origin + y_axis, Color::GREEN);
|
||||
gizmos.line(origin, origin + z_axis, Color::BLUE);
|
||||
}
|
||||
|
||||
let seek_time = **r_seek_time;
|
||||
let keyframes_sorted = q_cam_keyframes
|
||||
.iter()
|
||||
.sorted_by(|(a, _), (b, _)| a.partial_cmp(&b.0).unwrap())
|
||||
.collect_vec();
|
||||
|
||||
let (timestamp_first, xform_first) = keyframes_sorted.first().unwrap();
|
||||
let (timestamp_last, xform_last) = keyframes_sorted.last().unwrap();
|
||||
|
||||
if seek_time <= timestamp_first.0 {
|
||||
let (_, rot, pos) = xform_first.to_scale_rotation_translation();
|
||||
gizmos.sphere(pos, rot, 0.25, Color::WHITE);
|
||||
} else if seek_time >= timestamp_last.0 {
|
||||
let (_, rot, pos) = xform_last.to_scale_rotation_translation();
|
||||
gizmos.sphere(pos, rot, 0.25, Color::WHITE);
|
||||
} else if let Some(((timestamp_a, xform_a), (timestamp_b, xform_b))) = keyframes_sorted
|
||||
.iter()
|
||||
.tuple_windows()
|
||||
.find(|((a, _), (b, _))| a.0 <= seek_time && b.0 >= seek_time)
|
||||
{
|
||||
let t = seek_time.inv_lerp(timestamp_a.0, timestamp_b.0);
|
||||
let (_, rot_a, pos_a) = xform_a.to_scale_rotation_translation();
|
||||
let (_, rot_b, pos_b) = xform_b.to_scale_rotation_translation();
|
||||
let pos = pos_a.lerp(pos_b, t);
|
||||
let rot = Quat::slerp(rot_a, rot_b, t);
|
||||
|
||||
gizmos.sphere(pos, rot, 0.25, Color::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use space_editor::prelude::space_undo::{self, AddedEntity};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
camera_controller::CameraController,
|
||||
wasm::{self, FromStr, StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
|
||||
use super::{CameraKeyframe, CameraTargetKeyframe};
|
||||
|
||||
static ADD_KEYFRAME: WasmMessageBuffer<f32> = WasmMessageBuffer::new();
|
||||
static UPDATE_KEYFRAME: WasmMessageBuffer<(Entity, f32)> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = addCameraKeyframe)]
|
||||
pub fn add_camera_keyframe(timestamp: f32) {
|
||||
ADD_KEYFRAME.write_message(timestamp);
|
||||
}
|
||||
|
||||
pub(super) fn spawn_camera_keyframes(
|
||||
mut cmd: Commands,
|
||||
mut ew_changes: EventWriter<space_undo::NewChange>,
|
||||
q_camera: Query<(&CameraController, &GlobalTransform)>,
|
||||
) {
|
||||
if let Some(timestamp) = ADD_KEYFRAME.take_message() {
|
||||
let Ok((cam_controller, world_xform)) = q_camera.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity = cmd
|
||||
.spawn((
|
||||
Name::new("Camera Target Keyframe"),
|
||||
TransformBundle::from_transform(Transform::from_translation(
|
||||
cam_controller.focus,
|
||||
)),
|
||||
CameraTargetKeyframe(timestamp),
|
||||
))
|
||||
.id();
|
||||
ew_changes.send(space_undo::NewChange {
|
||||
change: Arc::new(AddedEntity { entity }),
|
||||
});
|
||||
|
||||
let entity = cmd
|
||||
.spawn((
|
||||
Name::new("Camera Keyframe"),
|
||||
TransformBundle::from_transform(Transform::from(*world_xform)),
|
||||
CameraKeyframe(timestamp),
|
||||
))
|
||||
.id();
|
||||
ew_changes.send(space_undo::NewChange {
|
||||
change: Arc::new(AddedEntity { entity }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = updateKeyframe)]
|
||||
pub fn update_keyframe(entity: &str, timestamp: f32) {
|
||||
let entity = Entity::from_str(entity).unwrap();
|
||||
UPDATE_KEYFRAME.write_message((entity, timestamp));
|
||||
}
|
||||
|
||||
pub(super) fn process_update_keyframe(
|
||||
mut q_camera_keyframes: Query<&mut CameraKeyframe>,
|
||||
mut q_camera_target_keyframes: Query<&mut CameraTargetKeyframe>,
|
||||
) {
|
||||
if let Some((ent, timestamp)) = UPDATE_KEYFRAME.take_message() {
|
||||
if let Ok(mut keyframe) = q_camera_keyframes.get_mut(ent) {
|
||||
**keyframe = timestamp;
|
||||
} else if let Ok(mut keyframe) = q_camera_target_keyframes.get_mut(ent) {
|
||||
**keyframe = timestamp;
|
||||
} else {
|
||||
warn!("No matching keyframe found for entity: {ent:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const CAMERA_KEYFRAMES_EVENT: &str = r#"
|
||||
export interface CameraKeyframesEvent extends CustomEvent {
|
||||
type: "camera-keyframes";
|
||||
detail: Record<string, number>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"camera-keyframes": CameraKeyframesEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub(super) fn notify_keyframe_changes(
|
||||
mut l_notified: Local<serde_json::Map<String, serde_json::Value>>,
|
||||
q_keyframes: Query<(Entity, &CameraKeyframe)>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
if l_notified.len() == q_keyframes.iter().count() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
l_notified.clear();
|
||||
for (ent, timestamp) in q_keyframes.iter() {
|
||||
l_notified.insert(format!("{ent:?}"), timestamp.0.into());
|
||||
}
|
||||
|
||||
wasm::dispatch_to_js(window, "camera-keyframes", &*l_notified)
|
||||
}
|
||||
}
|
||||
+5
-33
@@ -9,8 +9,8 @@ use space_editor::space_prefab::editor_registry::EditorRegistryExt;
|
||||
use crate::{hierarchy, scene::SceneElement};
|
||||
pub use retargeting::*;
|
||||
|
||||
mod camera;
|
||||
mod retargeting;
|
||||
pub mod timeline_animation;
|
||||
|
||||
pub struct AnimPlugin;
|
||||
impl Plugin for AnimPlugin {
|
||||
@@ -29,19 +29,14 @@ impl Plugin for AnimPlugin {
|
||||
.editor_registry::<InvBindPose>();
|
||||
|
||||
app.add_systems(Update, process_skinned_meshes);
|
||||
app.insert_resource(GlobalSeekTime(0.));
|
||||
|
||||
app.add_plugins((retargeting::RetargetingPlugin, camera::CameraAnimPlugin));
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(Update, wasm::update_global_seek_time);
|
||||
app.add_plugins((
|
||||
retargeting::RetargetingPlugin,
|
||||
timeline_animation::TimelinePlugin,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct GlobalSeekTime(pub f32);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Skeleton;
|
||||
@@ -149,26 +144,3 @@ fn process_skinned_meshes(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
use super::GlobalSeekTime;
|
||||
|
||||
static SEEK_TIME: WasmMessageBuffer<f32> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = seekTime)]
|
||||
pub fn seek_time(timestamp: f32) {
|
||||
SEEK_TIME.write_message(timestamp);
|
||||
}
|
||||
|
||||
pub(super) fn update_global_seek_time(mut r_seek_time: ResMut<GlobalSeekTime>) {
|
||||
if let Some(timestamp) = SEEK_TIME.take_message() {
|
||||
**r_seek_time = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,993 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use bevy::{prelude::*, utils::hashbrown::HashMap};
|
||||
|
||||
use bevy_tweening::{
|
||||
lens::{TransformPositionLens, TransformRotationLens, TransformScaleLens},
|
||||
Animator, EaseMethod, Sequence, Targetable, Tracks, Tween, TweenCompleted,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use space_editor::{prelude::PrefabMarker, space_prefab::editor_registry::EditorRegistryExt};
|
||||
|
||||
use crate::{
|
||||
headless::HeadlessSettings,
|
||||
interaction::{MarkedForDeletion, Selectable},
|
||||
scene::SceneElement,
|
||||
};
|
||||
|
||||
pub struct TimelinePlugin;
|
||||
|
||||
impl Plugin for TimelinePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<TimedAnimationClipAsset>()
|
||||
.editor_registry::<TimedAnimationClipAsset>()
|
||||
.register_type::<AnimationTarget>()
|
||||
.editor_registry::<AnimationTarget>()
|
||||
.register_type::<TransformKeyframe>()
|
||||
.editor_registry::<TransformKeyframe>()
|
||||
.register_type::<Keyframe>()
|
||||
.editor_registry::<Keyframe>()
|
||||
.register_type::<KeyframeOrder>()
|
||||
.editor_registry::<KeyframeOrder>()
|
||||
.register_type::<EaseType>()
|
||||
.register_type::<uuid::Uuid>()
|
||||
.add_event::<TimelineInteractionEvent>()
|
||||
.add_event::<KeyframeInteractionEvent>()
|
||||
.add_state::<KeyingMode>();
|
||||
|
||||
app.insert_resource(GlobalSeekTime {
|
||||
time: 0.,
|
||||
playing: false,
|
||||
delta: None,
|
||||
})
|
||||
.add_event::<TweenCompleted>();
|
||||
|
||||
app.add_systems(First, generate_transform_animation);
|
||||
app.add_systems(PreUpdate, (process_timeline_events, update_tick).chain());
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
process_auto_key.run_if(in_state(KeyingMode::Auto)),
|
||||
process_keyframe_events,
|
||||
timeline_bound_component_animator_system::<Transform>,
|
||||
),
|
||||
);
|
||||
|
||||
// app.add_systems(PostUpdate, generate_transform_animation);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
{
|
||||
app.add_systems(
|
||||
Last,
|
||||
(
|
||||
wasm::update_global_seek_time,
|
||||
wasm::update_keyframe_interaction,
|
||||
),
|
||||
);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
wasm::notify_seek_time_changes.map(bevy::utils::error),
|
||||
wasm::notify_keyframe_changes.map(bevy::utils::error),
|
||||
wasm::notify_updated_animation_targets.map(bevy::utils::error),
|
||||
wasm::notify_keying_mode_changed.map(bevy::utils::error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_KEYFRAME_DISTANCE: f32 = 0.01;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Event, Clone, Copy, Debug)]
|
||||
pub enum TimelineInteractionEvent {
|
||||
Play,
|
||||
Pause,
|
||||
Seek(f32),
|
||||
ToggleAutoKey,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Event, Clone, Copy, Debug)]
|
||||
pub enum KeyframeInteractionEvent {
|
||||
RecordKeyframe { target: Entity },
|
||||
MoveKeyframeTime { target: Entity, time: f32 },
|
||||
AddEntityTrack { target: Entity },
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize)]
|
||||
#[reflect(Resource)]
|
||||
pub struct GlobalSeekTime {
|
||||
pub time: f32,
|
||||
pub playing: bool,
|
||||
pub delta: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(States, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum KeyingMode {
|
||||
#[default]
|
||||
Manual,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Copy, Default, Reflect, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EaseType {
|
||||
#[default]
|
||||
Linear,
|
||||
CubicHermite,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub struct TransformKeyframe {
|
||||
incoming_easing: EaseType,
|
||||
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)]
|
||||
pub struct Keyframe {
|
||||
target: AnimationTarget,
|
||||
pub 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 Default for Keyframe {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: Default::default(),
|
||||
target: AnimationTarget::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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) {
|
||||
Some(core::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
match self.speed.partial_cmp(&other.speed) {
|
||||
Some(core::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimedAnimationClipAsset {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
clip: Default::default(),
|
||||
duration: Default::default(),
|
||||
speed: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Component, Clone, Debug, Default, Reflect, PartialEq, Eq, Hash, Copy, Serialize, Deserialize,
|
||||
)]
|
||||
#[reflect(Component)]
|
||||
#[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<AnimationTarget> 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<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AnimationTarget {
|
||||
type Err = uuid::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<TimelineInteractionEvent>,
|
||||
mut r_global_seek_time: ResMut<GlobalSeekTime>,
|
||||
key_mode: Res<State<KeyingMode>>,
|
||||
mut next_keying_mode: ResMut<NextState<KeyingMode>>,
|
||||
) {
|
||||
for event in e_timeline_event.read() {
|
||||
match event {
|
||||
TimelineInteractionEvent::Play => r_global_seek_time.playing = true,
|
||||
TimelineInteractionEvent::Pause => {
|
||||
r_global_seek_time.playing = false;
|
||||
}
|
||||
TimelineInteractionEvent::Seek(time) => {
|
||||
r_global_seek_time.time = time.max(0.);
|
||||
}
|
||||
TimelineInteractionEvent::ToggleAutoKey => {
|
||||
next_keying_mode.set(match key_mode.get() {
|
||||
KeyingMode::Manual => KeyingMode::Auto,
|
||||
KeyingMode::Auto => KeyingMode::Manual,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
r_global_seek_time.delta = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_tick(
|
||||
mut r_global_seek_time: ResMut<GlobalSeekTime>,
|
||||
time: Res<Time>,
|
||||
headless_settings: Option<Res<HeadlessSettings>>,
|
||||
) {
|
||||
if r_global_seek_time.playing {
|
||||
let delta = match headless_settings {
|
||||
Some(headless_settings) => headless_settings.frame_delta,
|
||||
None => time.delta_seconds(),
|
||||
};
|
||||
r_global_seek_time.time += delta;
|
||||
r_global_seek_time.delta = Some(delta);
|
||||
}
|
||||
}
|
||||
|
||||
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, &AnimationTarget)>,
|
||||
q_potential_targets: Query<Entity, (With<Transform>, Without<AnimationTarget>)>,
|
||||
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, animation_target)) = q_animation_targets.get(*target)
|
||||
{
|
||||
let mut existing = None;
|
||||
for (entity, keyframe) in q_keyframes.iter_mut() {
|
||||
if keyframe.target == *animation_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 {
|
||||
time: r_global_seek_time.time,
|
||||
target: *animation_target,
|
||||
},
|
||||
));
|
||||
} else {
|
||||
commands.spawn((
|
||||
PrefabMarker,
|
||||
SpatialBundle {
|
||||
transform: *transform,
|
||||
..Default::default()
|
||||
},
|
||||
Selectable,
|
||||
Keyframe {
|
||||
time: r_global_seek_time.time,
|
||||
target: *animation_target,
|
||||
},
|
||||
TransformKeyframe::default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyframeInteractionEvent::MoveKeyframeTime { target, time } => {
|
||||
if let Ok((_, mut keyframe)) = q_keyframes.get_mut(*target) {
|
||||
keyframe.time = *time;
|
||||
}
|
||||
}
|
||||
KeyframeInteractionEvent::AddEntityTrack { target } => {
|
||||
if let Ok(target) = q_potential_targets.get(*target) {
|
||||
info!("Adding Animation Target to Entity");
|
||||
commands.entity(target).insert(AnimationTarget::new());
|
||||
} else {
|
||||
warn!("Can't add animation target to entity");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_auto_key(
|
||||
changed: Query<Entity, (With<AnimationTarget>, Changed<Transform>)>,
|
||||
r_global_seek_time: Res<GlobalSeekTime>,
|
||||
mut event: EventWriter<KeyframeInteractionEvent>,
|
||||
) {
|
||||
if r_global_seek_time.playing || r_global_seek_time.is_changed() {
|
||||
info!("Skipping Auto Key");
|
||||
return;
|
||||
}
|
||||
|
||||
for entity in &changed {
|
||||
info!("Setting Auto Key");
|
||||
event.send(KeyframeInteractionEvent::RecordKeyframe { target: entity })
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_transform_animation(
|
||||
mut commands: Commands,
|
||||
keyframes: Query<
|
||||
(&Keyframe, &Transform, &TransformKeyframe, Entity),
|
||||
Without<MarkedForDeletion>,
|
||||
>,
|
||||
valid_targets: Query<(
|
||||
Entity,
|
||||
&AnimationTarget,
|
||||
Option<&Name>,
|
||||
Option<&SceneElement>,
|
||||
)>,
|
||||
mut e_timeline_event: EventReader<TimelineInteractionEvent>,
|
||||
) {
|
||||
let mut playing = false;
|
||||
for e in e_timeline_event.read() {
|
||||
if matches!(
|
||||
e,
|
||||
TimelineInteractionEvent::Play | TimelineInteractionEvent::Seek(_)
|
||||
) {
|
||||
playing = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !playing {
|
||||
return;
|
||||
}
|
||||
info!("Generating Animation From Keyframes");
|
||||
|
||||
let available_targets = valid_targets
|
||||
.iter()
|
||||
.map(|(e, a, name, element)| (*a, (e, name, element)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let keyframes = keyframes
|
||||
.iter()
|
||||
.into_group_map_by(|(keyframe, _, _, _)| keyframe.target);
|
||||
|
||||
for (target, keyframes) in keyframes.into_iter() {
|
||||
info!("Keyframes For Target {target:?}");
|
||||
let Some(target) = available_targets.get(&target) else {
|
||||
info!("No valid target.");
|
||||
continue;
|
||||
};
|
||||
let (target, target_name, target_scene_element) = *target;
|
||||
|
||||
let keyframes = keyframes.iter();
|
||||
|
||||
let keyframes = keyframes.sorted_by(|a, b| a.0.time.total_cmp(&b.0.time));
|
||||
|
||||
let (_, _, _, translation, rotation, scale) = keyframes.fold(
|
||||
(
|
||||
Option::<(f32, Vec3)>::None,
|
||||
Option::<(f32, Quat)>::None,
|
||||
Option::<(f32, Vec3)>::None,
|
||||
Vec::<Tween<Transform>>::new(),
|
||||
Vec::<Tween<Transform>>::new(),
|
||||
Vec::<Tween<Transform>>::new(),
|
||||
),
|
||||
|(
|
||||
last_translation,
|
||||
last_rotation,
|
||||
last_scale,
|
||||
mut translation_list,
|
||||
mut rotation_list,
|
||||
mut scale_list,
|
||||
),
|
||||
(keyframe, transform, transform_keyframe, _)| {
|
||||
let time = keyframe.time;
|
||||
let translation = transform.translation;
|
||||
let rotation = transform.rotation;
|
||||
let scale = transform.scale;
|
||||
|
||||
if transform_keyframe.affect_translation {
|
||||
let (last_time, start) = match last_translation {
|
||||
Some(v) => v,
|
||||
None => (0., translation),
|
||||
};
|
||||
|
||||
if (time - last_time).abs() > 0.00001 {
|
||||
translation_list.push(Tween::new(
|
||||
EaseMethod::Linear,
|
||||
Duration::from_secs_f32((time - last_time).max(0.00001)),
|
||||
TransformPositionLens {
|
||||
start,
|
||||
end: translation,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if transform_keyframe.affect_rotation {
|
||||
let (last_time, start) = match last_rotation {
|
||||
Some(v) => v,
|
||||
None => (0., rotation),
|
||||
};
|
||||
|
||||
if (time - last_time).abs() > 0.00001 {
|
||||
rotation_list.push(Tween::new(
|
||||
EaseMethod::Linear,
|
||||
Duration::from_secs_f32((time - last_time).max(0.00001)),
|
||||
TransformRotationLens {
|
||||
start,
|
||||
end: rotation,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if transform_keyframe.affect_scale {
|
||||
let (last_time, start) = match last_scale {
|
||||
Some(v) => v,
|
||||
None => (0., scale),
|
||||
};
|
||||
|
||||
if (time - last_time).abs() > 0.00001 {
|
||||
scale_list.push(Tween::new(
|
||||
EaseMethod::Linear,
|
||||
Duration::from_secs_f32((time - last_time).max(0.00001)),
|
||||
TransformScaleLens { start, end: scale },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let next_translation = if transform_keyframe.affect_translation {
|
||||
Some((time, translation))
|
||||
} else {
|
||||
last_translation
|
||||
};
|
||||
let next_rotation = if transform_keyframe.affect_rotation {
|
||||
Some((time, rotation))
|
||||
} else {
|
||||
last_rotation
|
||||
};
|
||||
let next_scale = if transform_keyframe.affect_scale {
|
||||
Some((time, scale))
|
||||
} else {
|
||||
last_scale
|
||||
};
|
||||
|
||||
(
|
||||
next_translation,
|
||||
next_rotation,
|
||||
next_scale,
|
||||
translation_list,
|
||||
rotation_list,
|
||||
scale_list,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let mut tracks = Vec::with_capacity(3);
|
||||
|
||||
if !translation.is_empty() {
|
||||
tracks.push(Sequence::new(translation));
|
||||
} else {
|
||||
info!("No translation");
|
||||
}
|
||||
if !rotation.is_empty() {
|
||||
tracks.push(Sequence::new(rotation));
|
||||
} else {
|
||||
info!("No rotation");
|
||||
}
|
||||
if !scale.is_empty() {
|
||||
tracks.push(Sequence::new(scale));
|
||||
} else {
|
||||
info!("No scale");
|
||||
}
|
||||
|
||||
if tracks.is_empty() {
|
||||
info!("Empty Animation Track for {target_name:?} {target_scene_element:?} {target:?}");
|
||||
commands.entity(target).remove::<Animator<Transform>>();
|
||||
} else {
|
||||
info!("Adding Animation Track for {target_name:?} {target_scene_element:?} {target:?}");
|
||||
let tracks = Tracks::new(tracks);
|
||||
commands.entity(target).insert(Animator::new(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComponentTarget<'a, T: Component>(&'a mut T);
|
||||
|
||||
impl<'a, T: Component> ComponentTarget<'a, T> {
|
||||
fn new(target: Mut<'a, T>) -> Self {
|
||||
Self(target.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Component> Targetable<T> for ComponentTarget<'a, T> {
|
||||
fn target_mut(&mut self) -> &mut T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timeline_bound_component_animator_system<T: Component>(
|
||||
time: Res<GlobalSeekTime>,
|
||||
mut query: Query<(Entity, &mut T, &mut Animator<T>)>,
|
||||
events: ResMut<Events<TweenCompleted>>,
|
||||
) {
|
||||
if time.is_changed() && time.time >= 0.0 {
|
||||
let mut events: Mut<Events<TweenCompleted>> = events.into();
|
||||
for (entity, target, mut animator) in query.iter_mut() {
|
||||
if animator.tweenable().duration() <= Duration::from_secs_f32(0.0001) {
|
||||
continue;
|
||||
}
|
||||
let mut target = ComponentTarget::new(target);
|
||||
if let Some(delta) = time.delta {
|
||||
animator.tweenable_mut().tick(
|
||||
Duration::from_secs_f32(delta),
|
||||
&mut target,
|
||||
entity,
|
||||
&mut events,
|
||||
);
|
||||
} else {
|
||||
let tween = animator.tweenable_mut();
|
||||
tween.set_elapsed(Duration::from_secs_f32(time.time.max(0.)));
|
||||
tween.tick(Duration::ZERO, &mut target, entity, &mut events);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use bevy::{prelude::*, utils::hashbrown::HashMap, window::PrimaryWindow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
interaction::MarkedForDeletion,
|
||||
scene::SceneElement,
|
||||
wasm::{FromStr, StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
|
||||
use super::{
|
||||
AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent, KeyingMode,
|
||||
TimedAnimationClipAsset, TimelineInteractionEvent, TransformKeyframe,
|
||||
};
|
||||
|
||||
static SEEK_TIME: WasmMessageBuffer<TimelineInteractionEvent> = WasmMessageBuffer::new();
|
||||
static KEYFRAME_EVENT: WasmMessageBuffer<Vec<KeyframeInteractionEvent>> =
|
||||
WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = seekTime)]
|
||||
pub fn seek_time(timestamp: f32) {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::Seek(timestamp));
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = play)]
|
||||
pub fn play() {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::Play);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = pause)]
|
||||
pub fn pause() {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::Pause);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toggleAutoKey)]
|
||||
pub fn toggle_auto_key() {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::ToggleAutoKey);
|
||||
}
|
||||
|
||||
#[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,
|
||||
}]);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = addEntityTrack)]
|
||||
pub fn add_entity_track(entity: String) {
|
||||
let Ok(target) = Entity::from_str(&entity) else {
|
||||
return;
|
||||
};
|
||||
KEYFRAME_EVENT.write_message(vec![KeyframeInteractionEvent::AddEntityTrack { target }]);
|
||||
}
|
||||
|
||||
pub(super) fn update_global_seek_time(
|
||||
mut e_timeline_event: EventWriter<TimelineInteractionEvent>,
|
||||
) {
|
||||
if let Some(timestamp) = SEEK_TIME.take_message() {
|
||||
info!("Updating Seek Time {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)]
|
||||
const TIMELINE_TYPES: &str = r#"
|
||||
export interface SeekTimeChanged extends CustomEvent {
|
||||
type: "seek-time-changed";
|
||||
detail: {
|
||||
time: number,
|
||||
playing: boolean
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateAnimationTargets extends CustomEvent {
|
||||
type: "update-animation-targets",
|
||||
detail: {
|
||||
added: Record<string, { name?: string, target_type: SceneElement, animation_target: AnimationTarget } >,
|
||||
removed: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransformKeyframeInfo {
|
||||
target: AnimationTarget,
|
||||
time: number,
|
||||
entity: string,
|
||||
incoming_easing: "linear" | "cubic-hermite",
|
||||
affect_translation: bool,
|
||||
affect_rotation: bool,
|
||||
affect_scale: bool
|
||||
}
|
||||
|
||||
export interface TimedAnimationClipInfo {
|
||||
target: AnimationTarget,
|
||||
time: number,
|
||||
entity: string,
|
||||
type: 'animation-clip',
|
||||
clip: string,
|
||||
duration: number,
|
||||
speed: number
|
||||
}
|
||||
|
||||
export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo;
|
||||
|
||||
export type AnimationTarget = string;
|
||||
|
||||
export interface UpdateKeyframes extends CustomEvent {
|
||||
type: "update-keyframes",
|
||||
detail: {
|
||||
added: Record<string, Keyframe>,
|
||||
removed: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateKeyingMode extends CustomEvent {
|
||||
type: "keying-mode-changed",
|
||||
detail: {
|
||||
automatic: boolean
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"seek-time-changed": CameraKeyframesEvent;
|
||||
"update-animation-targets": UpdateAnimationTargets;
|
||||
"update-keyframes": UpdateKeyframes;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddedAnimationTargetRecordItem {
|
||||
name: Option<String>,
|
||||
target_type: SceneElement,
|
||||
animation_target: AnimationTarget,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UpdatedAnimationTargetsEvent {
|
||||
added: HashMap<String, AddedAnimationTargetRecordItem>,
|
||||
removed: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UpdateKeyingMode {
|
||||
automatic: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TimedAnimationClipInfo {
|
||||
entity: String,
|
||||
target: AnimationTarget,
|
||||
time: f32,
|
||||
clip: String,
|
||||
duration: f32,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TransformKeframeInfo {
|
||||
entity: String,
|
||||
target: AnimationTarget,
|
||||
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(
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
q_cameras: Query<
|
||||
(Entity, Option<&Name>, &AnimationTarget),
|
||||
(Added<AnimationTarget>, With<Camera>),
|
||||
>,
|
||||
q_scene_elements: Query<
|
||||
(Entity, Option<&Name>, &SceneElement, &AnimationTarget),
|
||||
(Added<AnimationTarget>, Without<Camera>),
|
||||
>,
|
||||
q_transforms: Query<
|
||||
(Entity, Option<&Name>, &AnimationTarget),
|
||||
(
|
||||
Added<AnimationTarget>,
|
||||
With<Transform>,
|
||||
Without<Camera>,
|
||||
Without<SceneElement>,
|
||||
),
|
||||
>,
|
||||
mut removed: bevy::prelude::RemovedComponents<AnimationTarget>,
|
||||
) -> Result<(), String> {
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let added = q_transforms
|
||||
.iter()
|
||||
.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, 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, target)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: *element,
|
||||
animation_target: *target,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>();
|
||||
|
||||
if added.is_empty() && removed.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let event = UpdatedAnimationTargetsEvent { added, removed };
|
||||
|
||||
crate::wasm::dispatch_to_js(
|
||||
window,
|
||||
"update-animation-targets",
|
||||
&serde_json::to_value(&event).map_err(|e| format!("{e}"))?,
|
||||
)
|
||||
}
|
||||
|
||||
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>)>,
|
||||
>,
|
||||
removed: Query<Entity, With<MarkedForDeletion>>,
|
||||
) -> 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: 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: 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.iter().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(
|
||||
r_global_seek_time: Res<GlobalSeekTime>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
if !r_global_seek_time.is_changed() {
|
||||
return Ok(());
|
||||
}
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
info!("Current Seek Time: {r_global_seek_time:?}");
|
||||
|
||||
crate::wasm::dispatch_to_js(
|
||||
window,
|
||||
"seek-time-changed",
|
||||
&serde_json::to_value(r_global_seek_time.as_ref()).map_err(|e| format!("{e}"))?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn notify_keying_mode_changed(
|
||||
r_keying_mode: Res<State<KeyingMode>>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
if !r_keying_mode.is_changed() {
|
||||
return Ok(());
|
||||
}
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
crate::wasm::dispatch_to_js(
|
||||
window,
|
||||
"keying-mode-changed",
|
||||
&serde_json::to_value(UpdateKeyingMode {
|
||||
automatic: match r_keying_mode.get() {
|
||||
KeyingMode::Manual => false,
|
||||
KeyingMode::Auto => true,
|
||||
},
|
||||
})
|
||||
.map_err(|e| format!("{e}"))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use bevy::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
input::{Action, CameraMotion, InputAction, Lifecycle, Selection},
|
||||
interaction::Selected,
|
||||
input::{self, Action, CameraMotion, InputAction, Lifecycle},
|
||||
interaction::Selection,
|
||||
math::{
|
||||
Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq,
|
||||
NearlyZero, Velocity,
|
||||
@@ -155,20 +155,25 @@ fn process_physical_inputs(
|
||||
|
||||
fn process_focus_inputs(
|
||||
mut cmd: Commands,
|
||||
r_selection: Option<Res<Selection>>,
|
||||
mut input: EventReader<InputAction>,
|
||||
mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>,
|
||||
q_interp_vel: Query<&InterpVelocity>,
|
||||
mut q_interp_pos: Query<&mut InterpPosition>,
|
||||
q_selected_xform: Query<&GlobalTransform, With<Selected>>,
|
||||
q_global_xforms: Query<&GlobalTransform>,
|
||||
) {
|
||||
let Some(selection) = r_selection else {
|
||||
return;
|
||||
};
|
||||
|
||||
if input.read().any(|action| {
|
||||
matches!(
|
||||
action,
|
||||
InputAction(Lifecycle::Stop, Action::Selection(Selection::Focus))
|
||||
InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Focus))
|
||||
)
|
||||
}) {
|
||||
let Ok(target) = q_selected_xform
|
||||
.get_single()
|
||||
let Ok(target) = q_global_xforms
|
||||
.get(**selection)
|
||||
.map(|xform| xform.translation())
|
||||
else {
|
||||
return;
|
||||
|
||||
+48
-2
@@ -5,15 +5,17 @@ use bevy::{
|
||||
core_pipeline::Skybox,
|
||||
prelude::*,
|
||||
};
|
||||
use bevy_web_asset::WebAssetPlugin;
|
||||
use space_editor::prelude::{EditorEvent, EditorPrefabPath};
|
||||
use wgpu::Extent3d;
|
||||
|
||||
use crate::{
|
||||
anim::timeline_animation::{Keyframe, TimelineInteractionEvent},
|
||||
bvh::{BvhAnimCompleteEvent, TestBvh},
|
||||
scene::{SceneState, SkyboxHolder},
|
||||
screenshot::{ScreenCapture, ScreenShotPlugin},
|
||||
BvhPlugin, CommonPlugins, LoadLibraryObjectEvent, LoadSceneEvent, MainCamera,
|
||||
MixamoImportEvent, MixamoPlugin, SkyboxId, StudioMode, WorldGridPlugin,
|
||||
MixamoImportEvent, MixamoPlugin, RemoteAssetPlugin, SkyboxId, StudioMode, WorldGridPlugin,
|
||||
};
|
||||
|
||||
#[derive(Debug, Resource, Clone)]
|
||||
@@ -24,6 +26,14 @@ pub enum HeadlessMode {
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub enum CameraMotion {
|
||||
Orbit(f32),
|
||||
Pan(f32),
|
||||
Zoom(f32),
|
||||
Static,
|
||||
}
|
||||
|
||||
pub fn start_headless(
|
||||
mode: HeadlessMode,
|
||||
fps: u32,
|
||||
@@ -39,10 +49,14 @@ pub fn start_headless(
|
||||
max_frames: frames_to_capture,
|
||||
output_dims: image_res,
|
||||
output_dir: outout_dir.to_string(),
|
||||
fps: fps as f32,
|
||||
frame_delta: 1.0 / (fps as f32),
|
||||
});
|
||||
|
||||
app.add_plugins((
|
||||
ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0 / fps as f64)),
|
||||
WebAssetPlugin,
|
||||
RemoteAssetPlugin,
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: None,
|
||||
exit_condition: bevy::window::ExitCondition::DontExit,
|
||||
@@ -84,7 +98,8 @@ pub fn start_headless(
|
||||
|
||||
ew.send(EditorEvent::Load(EditorPrefabPath::File(scene.clone())));
|
||||
},
|
||||
);
|
||||
)
|
||||
.add_systems(OnEnter(SceneState::Active), start_timeline_animation);
|
||||
}
|
||||
HeadlessMode::Mixamo(mixamo_path, skybox) => {
|
||||
app.add_plugins(MixamoPlugin);
|
||||
@@ -124,6 +139,8 @@ pub struct HeadlessSettings {
|
||||
pub output_dims: Extent3d,
|
||||
pub output_dir: String,
|
||||
pub max_frames: u32,
|
||||
pub fps: f32,
|
||||
pub frame_delta: f32,
|
||||
}
|
||||
|
||||
impl Default for HeadlessSettings {
|
||||
@@ -136,6 +153,8 @@ impl Default for HeadlessSettings {
|
||||
},
|
||||
output_dir: "./screen_shots/".to_string(),
|
||||
max_frames: u32::MAX,
|
||||
fps: 24.0,
|
||||
frame_delta: 1.0 / 24.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,3 +195,30 @@ fn check_for_mixamo_capture_complete(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_timeline_animation(
|
||||
mut headless_settings: ResMut<HeadlessSettings>,
|
||||
keyframes: Query<&Keyframe>,
|
||||
camera: Query<Entity, With<MainCamera>>,
|
||||
mut commands: Commands,
|
||||
mut ev: EventWriter<TimelineInteractionEvent>,
|
||||
) {
|
||||
if keyframes.is_empty() {
|
||||
return;
|
||||
}
|
||||
commands.remove_resource::<CameraMotion>();
|
||||
let total_time = keyframes
|
||||
.iter()
|
||||
.fold(0f32, |previous, keyframe| previous.max(keyframe.time))
|
||||
.max(3.0);
|
||||
|
||||
let frames = (total_time * headless_settings.fps).ceil() as u32;
|
||||
headless_settings.max_frames = frames;
|
||||
ev.send(TimelineInteractionEvent::Play);
|
||||
let camera = camera.single();
|
||||
commands.entity(camera).insert(
|
||||
ScreenCapture::from_out(&headless_settings.output_dir)
|
||||
//Part of quick hack, see screenshot.rs#261
|
||||
.with_max_frames(headless_settings.max_frames + 3),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,9 +35,7 @@ pub(crate) fn nearest_common_ancestor(
|
||||
}
|
||||
|
||||
// Find the lowest depth of all the entities
|
||||
let Some(lowest_depth) = depths.values().copied().reduce(usize::min) else {
|
||||
return None;
|
||||
};
|
||||
let lowest_depth = depths.values().copied().reduce(usize::min)?;
|
||||
|
||||
// Traverse up the tree from each entity until we find each one's ancestor
|
||||
// at the level of lowest_depth
|
||||
|
||||
+67
-6
@@ -39,27 +39,88 @@ impl Plugin for StudioInputPlugin {
|
||||
.after(bevy::input::InputSystem)
|
||||
.after(read_mouse_delta),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(apply_deferred, log_input_actions).after(StudioInputSystem),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event, Clone, Copy)]
|
||||
/// Logs all `InputAction`s emitted, along with the input state at the time they
|
||||
/// were fired, with `debug` verbosity.
|
||||
fn log_input_actions(
|
||||
ri_keyboard: Res<Input<KeyCode>>,
|
||||
ri_mouse_buttons: Res<Input<MouseButton>>,
|
||||
mut er_input_actions: EventReader<InputAction>,
|
||||
) {
|
||||
if er_input_actions.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let lmb = if ri_mouse_buttons.pressed(MouseButton::Left) {
|
||||
'X'
|
||||
} else {
|
||||
'-'
|
||||
};
|
||||
let mmb = if ri_mouse_buttons.pressed(MouseButton::Middle) {
|
||||
'X'
|
||||
} else {
|
||||
'-'
|
||||
};
|
||||
let rmb = if ri_mouse_buttons.pressed(MouseButton::Right) {
|
||||
'X'
|
||||
} else {
|
||||
'-'
|
||||
};
|
||||
|
||||
use KeyCode::*;
|
||||
let mod_keys = [AltLeft, AltRight, ShiftLeft, ControlLeft, ControlRight]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
let action_keys = [W, A, S, D, E, Q, Escape, Grave, Delete, F]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let keys_pressed = ri_keyboard.get_pressed().copied().collect::<HashSet<_>>();
|
||||
let keys_display = keys_pressed
|
||||
.intersection(&mod_keys)
|
||||
.map(|key| format!("{key:?}"))
|
||||
.chain(
|
||||
keys_pressed
|
||||
.intersection(&action_keys)
|
||||
.map(|key| format!("{key:?}")),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
.join("+");
|
||||
|
||||
for action in er_input_actions.read() {
|
||||
debug!(
|
||||
"Mouse: [{lmb}{mmb}{rmb}] \
|
||||
Keyboard: [{keys_display}] \
|
||||
Action: [{action:.2?}]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event, Clone, Copy, Debug)]
|
||||
pub struct InputAction(pub Lifecycle, pub Action);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Lifecycle {
|
||||
Start,
|
||||
Ongoing,
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Action {
|
||||
Camera(CameraMotion),
|
||||
Selection(Selection),
|
||||
ToggleDebug,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CameraMotion {
|
||||
Translate(Vec3),
|
||||
Pan(Vec2),
|
||||
@@ -73,7 +134,7 @@ impl From<CameraMotion> for Action {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Selection {
|
||||
/// Clear the current selection
|
||||
Clear,
|
||||
@@ -212,7 +273,7 @@ fn translate_dcc_camera_events(
|
||||
CameraMotion::Rotate(Vec2::ZERO).into(),
|
||||
));
|
||||
}
|
||||
// Alt Camera Rotation (designed for laptop)
|
||||
// Alternative Camera Rotation (designed for laptop)
|
||||
else if keyboard.pressed(KeyCode::ShiftLeft) {
|
||||
let lifecycle = if keyboard.just_pressed(KeyCode::ShiftLeft) {
|
||||
Lifecycle::Start
|
||||
|
||||
@@ -27,6 +27,7 @@ impl Plugin for SelectionPlugin {
|
||||
PostUpdate,
|
||||
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
|
||||
)
|
||||
.add_systems(First, finalize_delete)
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
(apply_deferred, tag_scene_elements)
|
||||
@@ -48,60 +49,73 @@ impl Plugin for SelectionPlugin {
|
||||
.run_if(resource_changed_or_removed::<Selection>()),
|
||||
);
|
||||
|
||||
app.add_systems(Update, sync_editor_selected);
|
||||
app.add_systems(PostUpdate, propagate_space_editor_to_studio);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Copy)]
|
||||
#[derive(Component, Clone, Copy, Reflect, Default)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct Selectable;
|
||||
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct Selected;
|
||||
|
||||
#[derive(Resource, Deref, DerefMut, Debug)]
|
||||
pub struct Selection(Entity);
|
||||
|
||||
fn sync_editor_selected(
|
||||
fn propagate_space_editor_to_studio(
|
||||
mut cmd: Commands,
|
||||
q_new_selected: Query<Entity, (Added<Selected>, Without<space_editor::prelude::Selected>)>,
|
||||
q_editor_selected: Query<Entity, With<space_editor::prelude::Selected>>,
|
||||
q_selected: Query<Entity, With<Selected>>,
|
||||
q_added_editor_selected: Query<
|
||||
Entity,
|
||||
(Without<Selected>, Added<space_editor::prelude::Selected>),
|
||||
>,
|
||||
r_selection: Option<Res<Selection>>,
|
||||
q_new_selected: Query<Entity, Added<space_editor::prelude::Selected>>,
|
||||
mut er_deselected: RemovedComponents<space_editor::prelude::Selected>,
|
||||
q_parents: Query<&Parent>,
|
||||
q_selectable: Query<&Selectable>,
|
||||
) {
|
||||
for ent in q_new_selected.iter() {
|
||||
for ed_ent in q_editor_selected.iter() {
|
||||
cmd.entity(ed_ent)
|
||||
.remove::<space_editor::prelude::Selected>();
|
||||
let studio_selection_deselected_in_space_editor = r_selection
|
||||
.as_ref()
|
||||
.map(|selection| {
|
||||
er_deselected
|
||||
.read()
|
||||
.any(|deselected| deselected == ***selection)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if q_new_selected.is_empty() && studio_selection_deselected_in_space_editor {
|
||||
cmd.remove_resource::<Selection>();
|
||||
} else {
|
||||
let mut new_selected_iter = q_new_selected.iter();
|
||||
let Some(selected) = new_selected_iter.next().and_then(|se_selected| {
|
||||
nearest_selectable_parent(se_selected, &q_parents, &q_selectable)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if r_selection.is_none() || **r_selection.unwrap() != selected {
|
||||
cmd.insert_resource(Selection(selected));
|
||||
}
|
||||
cmd.entity(ent).insert(space_editor::prelude::Selected);
|
||||
}
|
||||
|
||||
for ent in q_added_editor_selected.iter() {
|
||||
cmd.entity(ent).insert(Selected);
|
||||
cmd.insert_resource(Selection(ent));
|
||||
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
if new_selected_iter.next().is_some() {
|
||||
warn!(
|
||||
"Multiple new entities were selected in Space Editor, but \
|
||||
Storyteller Studio doesn't currently support multiple selections"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_scene_elements(
|
||||
mut cmd: Commands,
|
||||
q_scene_elements: Query<(Entity, &Children), (With<SceneElement>, Without<Selectable>)>,
|
||||
q_scene_elements: Query<(Entity, Option<&Children>), (With<SceneElement>, Without<Selectable>)>,
|
||||
q_primitives: Query<&Handle<Mesh>>,
|
||||
) {
|
||||
for (ent, children) in q_scene_elements.iter() {
|
||||
cmd.entity(ent).insert(Selectable);
|
||||
cmd.entity(ent)
|
||||
.insert((Selectable, PickableBundle::default()));
|
||||
|
||||
for &ent_child in children
|
||||
.iter()
|
||||
.filter(|child| q_primitives.contains(**child))
|
||||
{
|
||||
cmd.entity(ent_child).insert(PickableBundle::default());
|
||||
if let Some(children) = children {
|
||||
for &ent_child in children
|
||||
.iter()
|
||||
.filter(|child| q_primitives.contains(**child))
|
||||
{
|
||||
cmd.entity(ent_child).insert(PickableBundle::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,31 +130,11 @@ fn update_selection(
|
||||
mut ev_input: EventReader<InputAction>,
|
||||
q_parents: Query<&Parent>,
|
||||
q_selectable: Query<&Selectable>,
|
||||
q_selected: Query<Entity, With<Selected>>,
|
||||
) {
|
||||
if let Some(event) = ev_select.read().last() {
|
||||
// Clear any existing `Selected` components
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
}
|
||||
|
||||
// Find the nearest Selectable parent of the target entity
|
||||
let mut ent = event.target;
|
||||
let ent_selected = loop {
|
||||
if q_selectable.get(ent).is_ok() {
|
||||
break Some(ent);
|
||||
}
|
||||
|
||||
if let Ok(parent) = q_parents.get(ent) {
|
||||
ent = parent.get();
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ent) = ent_selected {
|
||||
if let Some(ent) = nearest_selectable_parent(event.target, &q_parents, &q_selectable) {
|
||||
cmd.insert_resource(Selection(ent));
|
||||
cmd.entity(ent).insert(Selected);
|
||||
} else {
|
||||
cmd.remove_resource::<Selection>();
|
||||
}
|
||||
@@ -151,13 +145,30 @@ fn update_selection(
|
||||
)
|
||||
}) {
|
||||
cmd.remove_resource::<Selection>();
|
||||
}
|
||||
}
|
||||
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
fn nearest_selectable_parent(
|
||||
mut target: Entity,
|
||||
q_parents: &Query<&Parent>,
|
||||
q_selectable: &Query<&Selectable>,
|
||||
) -> Option<Entity> {
|
||||
loop {
|
||||
if q_selectable.get(target).is_ok() {
|
||||
break Some(target);
|
||||
}
|
||||
|
||||
if let Ok(parent) = q_parents.get(target) {
|
||||
target = parent.get();
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MarkedForDeletion;
|
||||
|
||||
fn process_delete_inputs(
|
||||
mut cmd: Commands,
|
||||
mut er_input: EventReader<InputAction>,
|
||||
@@ -172,7 +183,7 @@ fn process_delete_inputs(
|
||||
}) {
|
||||
let entity = **r_selection;
|
||||
|
||||
cmd.entity(entity).despawn_recursive();
|
||||
cmd.entity(entity).insert(MarkedForDeletion);
|
||||
cmd.remove_resource::<Selection>();
|
||||
ew_changes.send(NewChange {
|
||||
change: Arc::new(RemovedEntity { entity }),
|
||||
@@ -180,9 +191,16 @@ fn process_delete_inputs(
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_delete(mut cmd: Commands, q: Query<Entity, With<MarkedForDeletion>>) {
|
||||
for entity in &q {
|
||||
cmd.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_selection_outline(
|
||||
mut cmd: Commands,
|
||||
q_selected: Query<&Children, With<Selected>>,
|
||||
r_selection: Option<Res<Selection>>,
|
||||
q_children: Query<&Children>,
|
||||
q_meshes: Query<
|
||||
Entity,
|
||||
(
|
||||
@@ -199,19 +217,30 @@ fn draw_selection_outline(
|
||||
cmd.entity(ent).remove::<OutlineBundle>();
|
||||
}
|
||||
|
||||
for children in q_selected.iter() {
|
||||
for &child in children.iter() {
|
||||
if q_meshes.contains(child) {
|
||||
cmd.entity(child).insert(OutlineBundle {
|
||||
outline: OutlineVolume {
|
||||
visible: true,
|
||||
width: 4.,
|
||||
colour: Color::YELLOW,
|
||||
},
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
||||
let Some(selection) = r_selection else {
|
||||
return;
|
||||
};
|
||||
|
||||
let target = if q_meshes.contains(**selection) {
|
||||
Some(**selection)
|
||||
} else {
|
||||
q_children.get(**selection).ok().and_then(|children| {
|
||||
children
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&child| q_meshes.contains(child))
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(target) = target {
|
||||
cmd.entity(target).insert(OutlineBundle {
|
||||
outline: OutlineVolume {
|
||||
visible: true,
|
||||
width: 4.,
|
||||
colour: Color::YELLOW,
|
||||
},
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +252,7 @@ mod wasm {
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use super::{Selected, Selection};
|
||||
use super::Selection;
|
||||
use crate::wasm::{self, FromStr, StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
@@ -263,7 +292,6 @@ declare global {
|
||||
pub(super) fn update_selection_from_frontend(mut cmd: Commands) {
|
||||
if let Some(entity) = SELECTION_REQUEST.take_message() {
|
||||
cmd.insert_resource(Selection(entity));
|
||||
cmd.entity(entity).insert(Selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ mod mixamo;
|
||||
mod path;
|
||||
mod persistance;
|
||||
mod remote;
|
||||
mod scene;
|
||||
pub mod scene;
|
||||
pub mod screenshot;
|
||||
mod skybox;
|
||||
mod ui;
|
||||
|
||||
+88
-32
@@ -1,6 +1,9 @@
|
||||
use bevy::{prelude::*, render::render_resource::Extent3d, transform::components::Transform};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use studio::headless::{start_headless, HeadlessMode};
|
||||
use studio::{
|
||||
headless::{start_headless, CameraMotion, HeadlessMode},
|
||||
scene::SceneState,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "StorytellerStudioHeadless")]
|
||||
@@ -20,11 +23,9 @@ enum Subcommands {
|
||||
Scene {
|
||||
scene_dir: String,
|
||||
#[arg(long)]
|
||||
orbit: bool,
|
||||
#[arg(long, value_parser = clap::value_parser!(f32))]
|
||||
orbit_radius: Option<f32>,
|
||||
#[arg(long, value_parser = clap::value_parser!(f32))]
|
||||
orbit_speed: Option<f32>,
|
||||
camera_motion: Option<String>,
|
||||
#[arg(long)]
|
||||
camera_motion_speed: Option<f32>,
|
||||
#[arg(long, short)]
|
||||
output_dir: Option<String>,
|
||||
#[arg(long)]
|
||||
@@ -73,9 +74,8 @@ fn main() {
|
||||
let mut app = match args.subcommand {
|
||||
Subcommands::Scene {
|
||||
scene_dir,
|
||||
orbit,
|
||||
orbit_speed,
|
||||
orbit_radius,
|
||||
camera_motion,
|
||||
camera_motion_speed,
|
||||
skybox,
|
||||
output_dir,
|
||||
output_resolution,
|
||||
@@ -98,15 +98,29 @@ fn main() {
|
||||
frames,
|
||||
output_res,
|
||||
);
|
||||
let orbit_speed = orbit_speed.unwrap_or(1.0);
|
||||
let orbit_radius = orbit_radius.unwrap_or(8.0);
|
||||
|
||||
if orbit {
|
||||
app.insert_resource(OrbitSettings {
|
||||
radius: orbit_radius,
|
||||
speed: orbit_speed,
|
||||
})
|
||||
.add_systems(Update, orbit_scene);
|
||||
if let Some(camera_motion) = camera_motion {
|
||||
let camera_motion_speed = camera_motion_speed.unwrap_or(1.0);
|
||||
match camera_motion.as_str() {
|
||||
"orbit" => {
|
||||
app.insert_resource(CameraMotion::Orbit(camera_motion_speed));
|
||||
}
|
||||
"pan" => {
|
||||
app.insert_resource(CameraMotion::Pan(camera_motion_speed));
|
||||
}
|
||||
"zoom" => {
|
||||
app.insert_resource(CameraMotion::Zoom(camera_motion_speed));
|
||||
}
|
||||
"static" => {
|
||||
app.insert_resource(CameraMotion::Static);
|
||||
}
|
||||
_ => panic!("Unsupported camera motion type."),
|
||||
}
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
update_camera_motion.run_if(in_state(SceneState::Active)),
|
||||
);
|
||||
}
|
||||
|
||||
app
|
||||
@@ -166,27 +180,69 @@ fn main() {
|
||||
app.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct OrbitSettings {
|
||||
radius: f32,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
fn orbit_scene(
|
||||
mut camera: Query<&mut Transform, With<Camera>>,
|
||||
orbit_settings: Res<OrbitSettings>,
|
||||
pub(crate) fn update_camera_motion(
|
||||
mut camera: Query<&mut Transform, (With<Camera>, Without<Parent>)>,
|
||||
camera_motion: Option<Res<CameraMotion>>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let Some(camera_motion) = camera_motion else {
|
||||
return;
|
||||
};
|
||||
println!("{}", camera.iter().count());
|
||||
let Ok(mut main_cam_transform) = camera.get_single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
main_cam_transform.translation = Vec3::new(
|
||||
orbit_settings.radius * (time.elapsed_seconds() * orbit_settings.speed).cos(),
|
||||
6.0,
|
||||
orbit_settings.radius * (time.elapsed_seconds() * orbit_settings.speed).sin(),
|
||||
);
|
||||
main_cam_transform.look_at(Vec3::ZERO, Vec3::Y);
|
||||
println!("Updating camera motion");
|
||||
|
||||
match *camera_motion {
|
||||
CameraMotion::Orbit(speed) => {
|
||||
orbit_camera(&mut main_cam_transform, &time, speed);
|
||||
}
|
||||
CameraMotion::Pan(speed) => {
|
||||
pan_camera(&mut main_cam_transform, &time, speed);
|
||||
}
|
||||
CameraMotion::Zoom(speed) => {
|
||||
zoom_camera(&mut main_cam_transform, &time, speed);
|
||||
}
|
||||
CameraMotion::Static => {
|
||||
main_cam_transform.translation = Vec3::new(0.340, 1.1, -0.5);
|
||||
main_cam_transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
// main_cam_transform.look_at(Vec3::ZERO, Vec3::Y);
|
||||
}
|
||||
|
||||
fn zoom_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed: f32) {
|
||||
let scaled_time = time.elapsed().as_secs_f32() * speed;
|
||||
let normalized_cos = (-scaled_time.cos() + 1.0) / 2.0;
|
||||
let start_pos = Vec3::new(0.366, 1.1, 2.5);
|
||||
let end_pos = Vec3::new(0.366, 1.1, -0.5);
|
||||
|
||||
main_cam_transform.translation = start_pos.lerp(end_pos, normalized_cos);
|
||||
main_cam_transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
fn pan_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed: f32) {
|
||||
let scaled_time = time.elapsed().as_secs_f32() * speed;
|
||||
let normalized_cos = (-scaled_time.cos() + 1.0) / 2.0;
|
||||
let start_pos = Vec3::new(0.676, 1.1, 0.119);
|
||||
let end_pos = Vec3::new(0.067, 1.1, 0.121);
|
||||
|
||||
main_cam_transform.translation = start_pos.lerp(end_pos, normalized_cos);
|
||||
main_cam_transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
fn orbit_camera(main_cam_transform: &mut Mut<'_, Transform>, time: &Res<'_, Time>, speed: f32) {
|
||||
let scaled_time = time.elapsed().as_secs_f32() * speed;
|
||||
let max_angle = std::f32::consts::PI / 2.0;
|
||||
let normalized_cos = (scaled_time.cos() + 1.0) / 2.0;
|
||||
let angle = max_angle * normalized_cos;
|
||||
|
||||
// let t = (time.elapsed().as_secs_f32() / 5.0) % 1.0;
|
||||
|
||||
main_cam_transform.translation = Vec3::new(-2.2 * angle.cos(), 1.0, 2.5 * angle.sin());
|
||||
main_cam_transform.look_at(Vec3::new(0.347, 0.556, -1.0), Vec3::Y);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ use bevy::{
|
||||
window::{close_on_esc, Window},
|
||||
};
|
||||
use clap::Parser;
|
||||
use studio::{screenshot::ScreenShotPlugin, InitAsset, InitParams, StudioMode};
|
||||
use studio::{InitAsset, InitParams, StudioMode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "StorytellerStudio")]
|
||||
@@ -19,7 +19,7 @@ struct StudioArgs {
|
||||
fn main() {
|
||||
let args = StudioArgs::parse();
|
||||
|
||||
let object_id = args.model.unwrap_or("Mannequin.gltf".to_string());
|
||||
let object_id = args.model.unwrap_or("sample-room.gltf".to_string());
|
||||
let skybox_id = args.skybox.unwrap_or("gum_trees_4k".to_string());
|
||||
|
||||
// Edit the arguments here to change the default startup scene or skybox
|
||||
@@ -37,7 +37,7 @@ fn main() {
|
||||
|
||||
// Add any additional systems, resources, etc. for local debugging
|
||||
app.add_systems(Update, close_on_esc);
|
||||
app.add_plugins(ScreenShotPlugin);
|
||||
// app.add_plugins(ScreenShotPlugin);
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
+73
-61
@@ -1,5 +1,6 @@
|
||||
use std::{io::ErrorKind, path::Path, sync::Arc};
|
||||
|
||||
use async_compat::Compat;
|
||||
use bevy::{
|
||||
asset::io::{AssetReader, AssetReaderError, PathStream, Reader, VecReader},
|
||||
prelude::*,
|
||||
@@ -15,6 +16,68 @@ pub struct RemoteAssetReader;
|
||||
impl RemoteAssetReader {
|
||||
const API_BASE_URL: &'static str = "https://api.fakeyou.com/v1/media_files/file";
|
||||
const BUCKET_BASE_URL: &'static str = "https://storage.googleapis.com/vocodes-public";
|
||||
|
||||
async fn load<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
|
||||
let token = path
|
||||
.ext_file_prefix()
|
||||
.ok_or_else(|| {
|
||||
AssetReaderError::Io(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed to parse token from '{path:?}'"),
|
||||
))
|
||||
})?
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
info!("Loading asset from media token: '{token}'");
|
||||
|
||||
let response = reqwest::get(format!("{}/{token}", &Self::API_BASE_URL))
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into)?;
|
||||
|
||||
let response = match response.status() {
|
||||
StatusCode::OK => response
|
||||
.json::<GetMediaFileSuccessResponse>()
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into),
|
||||
other => Err(AssetReaderError::Io(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Status Code from endpoint: {other}"),
|
||||
))),
|
||||
}?;
|
||||
|
||||
let url = format!(
|
||||
"{}{}",
|
||||
&Self::BUCKET_BASE_URL,
|
||||
&response.media_file.public_bucket_path
|
||||
);
|
||||
|
||||
let response = reqwest::get(url)
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into)?;
|
||||
|
||||
let bytes = match response.status() {
|
||||
// TODO: This is not thread-safe, but some variation of this
|
||||
// would be better than the current solution once Bevy
|
||||
// WASM becomes multi-threaded:
|
||||
// StatusCode::OK => Ok(response.bytes_stream().map(|result| {
|
||||
// result.map_err(|err| std::io::Error::new(ErrorKind::Other, format!("{err}")))
|
||||
// })),
|
||||
StatusCode::OK => Ok(response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into)?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()),
|
||||
other => Err(AssetReaderError::Io(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Status Code from bucket URL: {other}"),
|
||||
))),
|
||||
}?;
|
||||
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
|
||||
Ok(reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetReader for RemoteAssetReader {
|
||||
@@ -22,67 +85,16 @@ impl AssetReader for RemoteAssetReader {
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let token = path
|
||||
.ext_file_prefix()
|
||||
.ok_or_else(|| {
|
||||
AssetReaderError::Io(Arc::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed to parse token from '{path:?}'"),
|
||||
)))
|
||||
})?
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
info!("Loading asset from media token: '{token}'");
|
||||
|
||||
let response = reqwest::get(format!("{}/{token}", &Self::API_BASE_URL))
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into)?;
|
||||
|
||||
let response = match response.status() {
|
||||
StatusCode::OK => response
|
||||
.json::<GetMediaFileSuccessResponse>()
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into),
|
||||
other => Err(AssetReaderError::Io(Arc::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Status Code from endpoint: {other}"),
|
||||
)))),
|
||||
}?;
|
||||
|
||||
let url = format!(
|
||||
"{}{}",
|
||||
&Self::BUCKET_BASE_URL,
|
||||
&response.media_file.public_bucket_path
|
||||
);
|
||||
|
||||
let response = reqwest::get(url)
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into)?;
|
||||
|
||||
let bytes = match response.status() {
|
||||
// TODO: This is not thread-safe, but some variation of this
|
||||
// would be better than the current solution once Bevy
|
||||
// WASM becomes multi-threaded:
|
||||
// StatusCode::OK => Ok(response.bytes_stream().map(|result| {
|
||||
// result.map_err(|err| std::io::Error::new(ErrorKind::Other, format!("{err}")))
|
||||
// })),
|
||||
StatusCode::OK => Ok(response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(IntoError::<AssetReaderError>::into)?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()),
|
||||
other => Err(AssetReaderError::Io(Arc::new(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Status Code from bucket URL: {other}"),
|
||||
)))),
|
||||
}?;
|
||||
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
|
||||
Ok(reader)
|
||||
})
|
||||
let unboxed = async move { self.load(path).await };
|
||||
if cfg!(not(feature = "wasm")) {
|
||||
info!("wasm mode not detected");
|
||||
// NB: Headless mode (server rendering) is running without a Tokio runtime.
|
||||
// The reqwest HTTP client requires async-compat to function at runtime.
|
||||
Box::pin(Compat::new(unboxed))
|
||||
} else {
|
||||
info!("wasm mode");
|
||||
Box::pin(unboxed)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
|
||||
+105
-15
@@ -12,22 +12,28 @@ use bevy::{
|
||||
prelude::*,
|
||||
render::{camera::RenderTarget, primitives::Aabb},
|
||||
scene::InstanceId,
|
||||
utils::{EntityHashSet, HashSet},
|
||||
};
|
||||
#[cfg(feature = "wasm")]
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use space_editor::{
|
||||
prelude::{EditorEvent, EditorPrefabPath, PrefabMarker},
|
||||
space_prefab::component::SceneAutoChild,
|
||||
space_prefab::{
|
||||
component::{GltfPrefab, SceneAutoChild},
|
||||
editor_registry::EditorRegistryExt,
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "wasm")]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wgpu::{TextureDescriptor, TextureUsages};
|
||||
|
||||
use crate::{
|
||||
anim::timeline_animation::AnimationTarget,
|
||||
camera_controller::{CameraController, CameraControllerBundle},
|
||||
change_tracking::{ChangeTracker, ChangeTrackingPlugin},
|
||||
debug::DebugPoint,
|
||||
headless::{is_headless, is_not_headless, HeadlessSettings},
|
||||
is_editor,
|
||||
math::{Combinable, Extrema},
|
||||
screenshot::ScreenCapture,
|
||||
MainCamera,
|
||||
@@ -57,21 +63,28 @@ impl Plugin for ScenePlugin {
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(init_object_loading, init_scene_import, init_scene_loading)
|
||||
(
|
||||
init_object_loading,
|
||||
init_scene_import,
|
||||
init_scene_loading,
|
||||
start_capture.run_if(is_headless),
|
||||
)
|
||||
.in_set(SceneSpawnerSystem),
|
||||
)
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
(
|
||||
tag_scene_elements,
|
||||
frame_scene_in_view,
|
||||
start_capture.run_if(is_headless),
|
||||
)
|
||||
(tag_scene_elements, frame_scene_in_view)
|
||||
.chain()
|
||||
.in_set(SceneSpawnerSystem),
|
||||
);
|
||||
|
||||
app.add_systems(Update, tag_scene_elements);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
tag_scene_elements,
|
||||
autoplay_animations.run_if(not(is_editor)),
|
||||
),
|
||||
);
|
||||
|
||||
app.init_resource::<SkyboxHolder>();
|
||||
app.add_systems(Update, attach_skybox);
|
||||
@@ -103,6 +116,8 @@ impl Plugin for ScenePlugin {
|
||||
wasm::notify_loading_queue_changes.map(bevy::utils::error),
|
||||
),
|
||||
);
|
||||
|
||||
app.editor_registry::<SceneElement>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +141,7 @@ fn attach_skybox(
|
||||
fn minimal_scene_setup(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Directional Light"),
|
||||
AnimationTarget::new(),
|
||||
DirectionalLightBundle {
|
||||
directional_light: DirectionalLight {
|
||||
shadows_enabled: false,
|
||||
@@ -164,8 +180,19 @@ pub struct LoadingQueue {
|
||||
fn check_loading(
|
||||
mut loading_queue: ResMut<LoadingQueue>,
|
||||
mut st_next: ResMut<NextState<SceneState>>,
|
||||
r_scene_state: Res<State<SceneState>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
// Avoid transitioning from `SceneState::None` directly to `SceneState::Active`.
|
||||
// We will always be initializing the scene with _something_, so if the
|
||||
// `LoadingQueue` is empty when we're in `SceneState::None`, it just means
|
||||
// our startup routine hasn't yet gotten to the point of processing any
|
||||
// `Handle<T>`s.
|
||||
if loading_queue.queue.is_empty() && **r_scene_state == SceneState::Loading {
|
||||
st_next.set(SceneState::Active);
|
||||
return;
|
||||
}
|
||||
|
||||
if !loading_queue.queue.is_empty() {
|
||||
//filter queue to only include notloaded assets
|
||||
|
||||
@@ -200,8 +227,6 @@ fn check_loading(
|
||||
} else {
|
||||
st_next.set(SceneState::Loading);
|
||||
}
|
||||
} else {
|
||||
st_next.set(SceneState::Active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,9 +281,11 @@ pub struct ImportSceneEvent(pub String);
|
||||
wasm_bindgen,
|
||||
derive(Serialize_repr, Deserialize_repr)
|
||||
)]
|
||||
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect, Default)]
|
||||
#[reflect(Component, Default)]
|
||||
#[allow(dead_code)] // Some variants only used in `wasm`, some planned for later use
|
||||
pub enum SceneElement {
|
||||
#[default]
|
||||
Generic,
|
||||
Mesh,
|
||||
Skeleton,
|
||||
@@ -283,6 +310,7 @@ fn initialize(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget::MainCamera,
|
||||
ChangeTracker,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
@@ -339,6 +367,7 @@ fn initialiaze_headless(
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget::MainCamera,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
@@ -350,8 +379,6 @@ fn initialiaze_headless(
|
||||
..default()
|
||||
},
|
||||
CameraControllerBundle::new(focus, camera_xform.translation),
|
||||
ScreenCapture::from_out(&headless_settings.output_dir)
|
||||
.with_max_frames(headless_settings.max_frames),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -371,7 +398,11 @@ fn init_object_loading(mut cmd: Commands, mut er: EventReader<LoadLibraryObjectE
|
||||
}
|
||||
}
|
||||
|
||||
fn init_scene_import(mut cmd: Commands, mut er: EventReader<ImportSceneEvent>) {
|
||||
fn init_scene_import(
|
||||
mut cmd: Commands,
|
||||
r_assets: Res<AssetServer>,
|
||||
mut er: EventReader<ImportSceneEvent>,
|
||||
) {
|
||||
if let Some(path) = er.read().last() {
|
||||
info!("Importing asset: {}", path.0);
|
||||
cmd.spawn((
|
||||
@@ -381,6 +412,7 @@ fn init_scene_import(mut cmd: Commands, mut er: EventReader<ImportSceneEvent>) {
|
||||
path: path.0.clone(),
|
||||
scene: "Scene0".to_string(),
|
||||
},
|
||||
r_assets.load::<Gltf>(&path.0),
|
||||
PrefabMarker,
|
||||
SceneRoot,
|
||||
));
|
||||
@@ -426,6 +458,64 @@ fn tag_scene_elements(
|
||||
}
|
||||
}
|
||||
|
||||
fn autoplay_animations(
|
||||
mut cmd: Commands,
|
||||
mut l_done: Local<HashSet<String>>,
|
||||
mut l_animating: Local<EntityHashSet<Entity>>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
ra_anims: Res<Assets<AnimationClip>>,
|
||||
q_prefabs: Query<(&GltfPrefab, &Handle<Gltf>)>,
|
||||
q_named_ents: Query<(Entity, &Name)>,
|
||||
mut q_players: Query<&mut AnimationPlayer>,
|
||||
) {
|
||||
for (prefab, handle) in q_prefabs.iter() {
|
||||
if l_done.contains(&prefab.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(gltf) = ra_gltfs.get(handle) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut playing_anims = 0_usize;
|
||||
for (anim_name, anim_handle) in gltf.named_animations.iter() {
|
||||
let Some(anim) = ra_anims.get(anim_handle.clone()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some((target, name)) = q_named_ents
|
||||
.iter()
|
||||
.find(|(_, name)| anim.compatible_with(name))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !l_animating.contains(&target) {
|
||||
info!(
|
||||
"Playing animation '{}' on entity {:?} '{}'",
|
||||
anim_name, target, name,
|
||||
);
|
||||
if let Ok(mut player) = q_players.get_mut(target) {
|
||||
if !player.is_playing_clip(anim_handle) {
|
||||
player.play(anim_handle.clone()).repeat();
|
||||
}
|
||||
} else {
|
||||
let mut player = AnimationPlayer::default();
|
||||
player.play(anim_handle.clone()).repeat();
|
||||
cmd.entity(target).insert(player);
|
||||
}
|
||||
l_animating.insert(target);
|
||||
}
|
||||
|
||||
playing_anims += 1;
|
||||
}
|
||||
|
||||
if playing_anims == gltf.named_animations.len() {
|
||||
l_done.insert(prefab.path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_scene_in_view(
|
||||
mut cmd: Commands,
|
||||
q_aabbs: Query<(&Aabb, &GlobalTransform), With<SceneAutoChild>>,
|
||||
@@ -529,7 +619,7 @@ fn frame_scene_in_view(
|
||||
cam_xform.translation = cam.focus + cam_xform.back() * radius;
|
||||
}
|
||||
|
||||
fn start_capture(mut captures: Query<&mut ScreenCapture>) {
|
||||
fn start_capture(mut captures: Query<&mut ScreenCapture, Added<ScreenCapture>>) {
|
||||
for mut capture in captures.iter_mut() {
|
||||
capture.start_capturing();
|
||||
}
|
||||
|
||||
@@ -147,7 +147,11 @@ fn update_screen_capture_on_extract_step(
|
||||
fn extract_screen_capture_components(
|
||||
mut commands: Commands,
|
||||
screen_capure_comps: Extract<Query<(Entity, &ScreenCapture), With<Camera>>>,
|
||||
scene_state: Extract<Res<State<SceneState>>>,
|
||||
) {
|
||||
if scene_state.get() != &SceneState::Active {
|
||||
return;
|
||||
};
|
||||
for (entity, screen_capture) in screen_capure_comps.iter() {
|
||||
if screen_capture.should_capture() {
|
||||
info!(
|
||||
@@ -249,10 +253,24 @@ impl ViewNode for ScreenCaptureNode {
|
||||
//==============================================================================
|
||||
|
||||
fn save_buffer_to_file(
|
||||
mut frames_to_skip: Local<Option<u32>>,
|
||||
buffers: Query<(&ScreenCapture, &ScreenCaptureBuffers)>,
|
||||
device: Res<RenderDevice>,
|
||||
) {
|
||||
for (screen_capture, buffers) in buffers.iter() {
|
||||
// ALERT!! ALERT!! Filthy Hack in effect
|
||||
// Engine does not load scene in time for the first 3 frames
|
||||
// So we skip them. Oops :(
|
||||
// TODO: Fix the Race Condition and properly take screenshots.
|
||||
let frames_skipped = frames_to_skip.unwrap_or(3);
|
||||
|
||||
if frames_skipped > 0 {
|
||||
println!("Skipping Frame");
|
||||
*frames_to_skip = Some(frames_skipped - 1);
|
||||
return;
|
||||
}
|
||||
// Filthy Hack Ends
|
||||
|
||||
if buffers.main_bytes_per_row != buffers.main_padded_bytes_per_row {
|
||||
println!("These don't match!");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_mod_picking::PickableBundle;
|
||||
use space_editor::prelude::{
|
||||
space_undo::{AddedEntity, NewChange},
|
||||
*,
|
||||
@@ -9,7 +8,6 @@ use space_editor::prelude::{
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
interaction::Selectable,
|
||||
scene::SceneElement,
|
||||
wasm::{StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
@@ -27,13 +25,8 @@ fn read_command_system(mut commands: Commands, mut change_event: EventWriter<New
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_entity = commands.spawn((
|
||||
SpatialBundle::default(),
|
||||
PrefabMarker,
|
||||
Selectable,
|
||||
SceneElement::Mesh,
|
||||
PickableBundle::default(),
|
||||
));
|
||||
let mut new_entity =
|
||||
commands.spawn((SpatialBundle::default(), PrefabMarker, SceneElement::Mesh));
|
||||
|
||||
match cmd {
|
||||
BlockingCommand::Cube => {
|
||||
|
||||
+356
-105
@@ -8,18 +8,20 @@ use bevy::{
|
||||
window::PrimaryWindow,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::*;
|
||||
use serde_json::{self, json, Map as JsonMap, Value as JsonValue};
|
||||
use space_editor::{
|
||||
space_editor_ui::{
|
||||
editor_tab::{EditorTab, EditorTabName},
|
||||
ui_plugin::EditorUiAppExt,
|
||||
EditorUiAppExt,
|
||||
},
|
||||
space_prefab::{
|
||||
component::MeshPrimitivePrefab, editor_registry::EditorRegistryExt, save::SaveState,
|
||||
},
|
||||
space_prefab::component::MeshPrimitive3dPrefab,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
interaction::Selected,
|
||||
interaction::Selection,
|
||||
wasm::{self, StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
|
||||
@@ -48,7 +50,11 @@ impl Plugin for InspectorPlugin {
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
update_field_by_ui.in_set(InspectorSystem::WriteChanges),
|
||||
(
|
||||
update_field_by_ui.map(bevy::utils::error),
|
||||
update_local_rotation_by_ui,
|
||||
)
|
||||
.in_set(InspectorSystem::WriteChanges),
|
||||
);
|
||||
|
||||
app.editor_tab_by_trait(
|
||||
@@ -60,16 +66,17 @@ impl Plugin for InspectorPlugin {
|
||||
|
||||
app.register_inspect::<Transform>();
|
||||
app.register_inspect::<Name>();
|
||||
app.register_inspect::<MeshPrimitive3dPrefab>();
|
||||
app.register_inspect::<MeshPrimitivePrefab>();
|
||||
|
||||
//register standart material
|
||||
{
|
||||
app.register_type::<ShortMaterial>()
|
||||
app.editor_registry::<ShortMaterial>()
|
||||
.add_event::<UpdateInspect<Handle<StandardMaterial>>>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_inspect_by_select_change::<Handle<StandardMaterial>>,
|
||||
update_inspect_by_select_change::<Handle<StandardMaterial>>
|
||||
.run_if(resource_changed_or_removed::<Selection>()),
|
||||
update_inspect_by_remove::<Handle<StandardMaterial>>,
|
||||
update_inspect_by_change::<Handle<StandardMaterial>>,
|
||||
notify_asset_change_system::<StandardMaterial>,
|
||||
@@ -81,32 +88,90 @@ impl Plugin for InspectorPlugin {
|
||||
serialize_standart_materal.in_set(InspectorSystem::PerComponentCollect),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
OnEnter(SaveState::Save),
|
||||
(create_short_materials, apply_deferred)
|
||||
.chain()
|
||||
.before(space_editor::space_prefab::sub_scene::prepare_auto_scene),
|
||||
);
|
||||
app.add_systems(PostUpdate, apply_short_materials);
|
||||
|
||||
app.world.resource_mut::<InspectGetters>().getters.insert(
|
||||
StandardMaterial::get_type_registration()
|
||||
.type_info()
|
||||
.type_path()
|
||||
.to_string(),
|
||||
Arc::new(|entity, path, world, f| unsafe {
|
||||
let handle = entity.get::<Handle<StandardMaterial>>().unwrap();
|
||||
let handle = entity.get::<Handle<StandardMaterial>>().ok_or_else(|| {
|
||||
format!(
|
||||
"Failed to get StandardMaterial for entity {:?}",
|
||||
entity.id()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut assets = world
|
||||
.get_resource_mut::<Assets<StandardMaterial>>()
|
||||
.unwrap();
|
||||
let material = assets.get_mut(handle).unwrap();
|
||||
.ok_or_else(|| {
|
||||
"Failed to get `Assets<StandadrMaterial>` resource".to_string()
|
||||
})?;
|
||||
|
||||
let reflect_field = material.reflect_path_mut(path).unwrap();
|
||||
(f)(reflect_field);
|
||||
let material = assets.get_mut(handle).ok_or_else(|| {
|
||||
format!("Failed to retrieve material from handle {handle:?}")
|
||||
})?;
|
||||
|
||||
let reflect_field = material
|
||||
.reflect_path_mut(path)
|
||||
.map_err(|err| format!("{err}"))?;
|
||||
|
||||
(f)(reflect_field)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_short_materials(
|
||||
mut commands: Commands,
|
||||
q_mats: Query<(Entity, &Handle<StandardMaterial>)>,
|
||||
materials: Res<Assets<StandardMaterial>>,
|
||||
) {
|
||||
for (entity, handle) in q_mats.iter() {
|
||||
if let Some(material) = materials.get(handle) {
|
||||
commands.entity(entity).insert(ShortMaterial::new(material));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_short_materials(
|
||||
mut commands: Commands,
|
||||
q_mats: Query<(Entity, &Handle<StandardMaterial>, &ShortMaterial)>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
for (entity, handle, short) in q_mats.iter() {
|
||||
let Some(material) = materials.get_mut(handle) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
material.base_color = short.base_color;
|
||||
material.perceptual_roughness = short.perceptual_roughness;
|
||||
material.metallic = short.metallic;
|
||||
material.reflectance = short.reflectance;
|
||||
|
||||
commands.entity(entity).remove::<ShortMaterial>();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct InspectGetters {
|
||||
getters: HashMap<
|
||||
String,
|
||||
Arc<
|
||||
dyn Fn(&mut EntityWorldMut, &str, &mut UnsafeWorldCell, &dyn Fn(&mut dyn Reflect))
|
||||
dyn Fn(
|
||||
&mut EntityWorldMut,
|
||||
&str,
|
||||
&mut UnsafeWorldCell,
|
||||
&dyn Fn(&mut dyn Reflect) -> Result<(), String>,
|
||||
) -> Result<(), String>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>,
|
||||
@@ -125,7 +190,8 @@ impl AppInspectable for App {
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_inspect_by_select_change::<T>,
|
||||
update_inspect_by_select_change::<T>
|
||||
.run_if(resource_changed_or_removed::<Selection>()),
|
||||
update_inspect_by_remove::<T>,
|
||||
update_inspect_by_change::<T>,
|
||||
)
|
||||
@@ -146,7 +212,7 @@ impl AppInspectable for App {
|
||||
Arc::new(|entity, path, _, f| {
|
||||
let mut component = entity.get_mut::<T>().unwrap();
|
||||
let reflect_field = component.reflect_path_mut(path).unwrap();
|
||||
(f)(reflect_field);
|
||||
(f)(reflect_field)
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -158,7 +224,8 @@ impl AppInspectable for App {
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_inspect_by_select_change::<Handle<T>>,
|
||||
update_inspect_by_select_change::<Handle<T>>
|
||||
.run_if(resource_changed_or_removed::<Selection>()),
|
||||
update_inspect_by_remove::<Handle<T>>,
|
||||
update_inspect_by_change::<Handle<T>>,
|
||||
notify_asset_change_system::<T>,
|
||||
@@ -186,7 +253,7 @@ impl EditorTab for TestInspectorTab {
|
||||
CHANGE_FROM_UI.write_message(UpdateField {
|
||||
component: "bevy_transform::components::transform::Transform".to_string(),
|
||||
reflect_path: ".translation.x".to_string(),
|
||||
value: serde_json::Value::from(0.0),
|
||||
value: JsonValue::from(0.0),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -220,116 +287,181 @@ impl<T> Default for UpdateInspect<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_field_by_ui(world: &mut World) {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Rgba {
|
||||
red: f32,
|
||||
green: f32,
|
||||
blue: f32,
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
fn update_field_by_ui(world: &mut World) -> Result<(), String> {
|
||||
unsafe {
|
||||
let mut unsafe_world = world.as_unsafe_world_cell();
|
||||
let Some(msg) = CHANGE_FROM_UI.take_message() else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
info!("Processing update: {msg:?}");
|
||||
|
||||
let mut selected_query = unsafe_world
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<Selected>>();
|
||||
let Ok(entity) = selected_query.get_single(unsafe_world.world_mut()) else {
|
||||
return;
|
||||
let Some(selection) = unsafe_world.world_mut().get_resource::<Selection>() else {
|
||||
return Ok(());
|
||||
};
|
||||
let entity = **selection;
|
||||
|
||||
let getters = unsafe_world.get_resource_mut::<InspectGetters>().unwrap();
|
||||
let getter = getters
|
||||
.getters
|
||||
.get(&msg.component)
|
||||
.ok_or_else(|| format!("Failed to find getter for component: {}", msg.component))?;
|
||||
|
||||
let mut entity_mut = unsafe_world.world_mut().entity_mut(entity);
|
||||
// TODO: This is pretty ugly
|
||||
let target = 'find_target: {
|
||||
if &msg.component[..] == "bevy_pbr::pbr_material::StandardMaterial" {
|
||||
let mut q_handles = unsafe_world
|
||||
.world_mut()
|
||||
.query::<&Handle<StandardMaterial>>();
|
||||
|
||||
let Some(getter) = getters.getters.get(&msg.component) else {
|
||||
error!("Failed to find getter for component: {}", msg.component);
|
||||
return;
|
||||
if q_handles.get(unsafe_world.world(), entity).is_ok() {
|
||||
break 'find_target Some(entity);
|
||||
}
|
||||
|
||||
let mut q_children = unsafe_world.world_mut().query::<&Children>();
|
||||
let Ok(ent_children) = q_children.get(unsafe_world.world(), entity) else {
|
||||
break 'find_target None;
|
||||
};
|
||||
|
||||
break 'find_target ent_children
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&child| q_handles.get(unsafe_world.world(), child).is_ok());
|
||||
} else {
|
||||
break 'find_target Some(entity);
|
||||
}
|
||||
};
|
||||
|
||||
let Some(target) = target else {
|
||||
return Err(format!(
|
||||
"Failed to find target for component {}",
|
||||
&msg.component
|
||||
));
|
||||
};
|
||||
|
||||
info!("Applying to entity: {target:?}");
|
||||
|
||||
let mut entity_mut = unsafe_world.world_mut().entity_mut(target);
|
||||
|
||||
getter.as_ref()(
|
||||
&mut entity_mut,
|
||||
&msg.reflect_path,
|
||||
&mut unsafe_world,
|
||||
&|reflect_field| {
|
||||
let fmt_err = |err: serde_json::Error| {
|
||||
format!(
|
||||
"Failed to parse value {} for path {}: {err}",
|
||||
msg.value, &msg.reflect_path,
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(val) = reflect_field.downcast_mut::<f32>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<f64>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<u32>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<u64>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<i32>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<i64>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<bool>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<String>() {
|
||||
*val = msg.value.as_str().unwrap().to_string();
|
||||
let msg_value = msg
|
||||
.value
|
||||
.as_str()
|
||||
.ok_or_else(|| format!("Failed to parse value as string: {}", msg.value))?;
|
||||
*val = msg_value.to_string();
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<Vec3>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
*val = msg_value;
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<Quat>() {
|
||||
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
|
||||
error!("Failed to parse value: {}", msg.value);
|
||||
return;
|
||||
};
|
||||
*val = msg_value;
|
||||
#[derive(Deserialize)]
|
||||
struct JsonQuat {
|
||||
w: f32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
}
|
||||
|
||||
let JsonQuat { w, x, y, z } =
|
||||
serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
|
||||
*val = Quat::from_xyzw(x, y, z, w);
|
||||
Ok(())
|
||||
} else if let Some(val) = reflect_field.downcast_mut::<Color>() {
|
||||
let Rgba {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
alpha,
|
||||
} = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
|
||||
|
||||
match *val {
|
||||
Color::RgbaLinear { .. } => {
|
||||
*val = Color::RgbaLinear {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
alpha,
|
||||
};
|
||||
}
|
||||
Color::Rgba { .. } => {
|
||||
*val = Color::Rgba {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
alpha,
|
||||
};
|
||||
}
|
||||
other => {
|
||||
return Err(format!("Unsupported color format: {other:?}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
error!("Unsupported type {}!", &msg.reflect_path);
|
||||
return;
|
||||
Err(format!("Unsupported type {}!", &msg.reflect_path))
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Serialize, Deserialize, Default, Clone)]
|
||||
pub struct EntityDescription {
|
||||
pub components: HashMap<String, Value>,
|
||||
pub components: HashMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
fn update_inspect_by_select_change<T: Component + Reflect + GetTypeRegistration>(
|
||||
mut ev_inspect: EventWriter<UpdateInspect<T>>,
|
||||
q_added: Query<(), Added<Selected>>,
|
||||
q_removed: RemovedComponents<Selected>,
|
||||
) {
|
||||
if !q_added.is_empty() {
|
||||
ev_inspect.send(UpdateInspect::<T>::default());
|
||||
}
|
||||
|
||||
if !q_removed.is_empty() {
|
||||
ev_inspect.send(UpdateInspect::<T>::default());
|
||||
}
|
||||
ev_inspect.send(UpdateInspect::<T>::default());
|
||||
}
|
||||
|
||||
fn update_inspect_by_remove<T: Component + Reflect + GetTypeRegistration>(
|
||||
@@ -342,16 +474,20 @@ fn update_inspect_by_remove<T: Component + Reflect + GetTypeRegistration>(
|
||||
}
|
||||
|
||||
fn update_inspect_by_change<T: Component + Reflect + GetTypeRegistration>(
|
||||
r_selection: Option<Res<Selection>>,
|
||||
mut ev_inspect: EventWriter<UpdateInspect<T>>,
|
||||
q_t_changed: Query<&T, (With<Selected>, Changed<T>)>,
|
||||
q_t_changed: Query<&T, Changed<T>>,
|
||||
) {
|
||||
if q_t_changed.iter().next().is_some() {
|
||||
ev_inspect.send(UpdateInspect::<T>::default());
|
||||
if let Some(selection) = r_selection {
|
||||
if q_t_changed.contains(**selection) {
|
||||
ev_inspect.send(UpdateInspect::<T>::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
|
||||
q_t: Query<&T, With<Selected>>,
|
||||
r_selection: Option<Res<Selection>>,
|
||||
q_t: Query<&T>,
|
||||
mut description: ResMut<EntityDescription>,
|
||||
registry: Res<AppTypeRegistry>,
|
||||
mut ev_inspect: EventReader<UpdateInspect<T>>,
|
||||
@@ -363,7 +499,7 @@ fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
|
||||
|
||||
ev_inspect.clear();
|
||||
|
||||
if let Some(t) = q_t.iter().next() {
|
||||
if let Some(t) = r_selection.and_then(|selection| q_t.get(**selection).ok()) {
|
||||
let registry = registry.read();
|
||||
let json = serde_json::to_value(ReflectSerializer::new(t, ®istry)).unwrap();
|
||||
info!("json: {:?}", json.to_string());
|
||||
@@ -384,12 +520,20 @@ fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
|
||||
}
|
||||
|
||||
fn notify_asset_change_system<T: Asset>(
|
||||
r_selection: Option<Res<Selection>>,
|
||||
mut ev_inspect: EventWriter<UpdateInspect<Handle<T>>>,
|
||||
mut asset_changing: EventReader<AssetEvent<T>>,
|
||||
mut selected: Query<&Handle<T>, With<Selected>>,
|
||||
q_handles: Query<&Handle<T>>,
|
||||
q_children: Query<&Children>,
|
||||
) {
|
||||
for ev in asset_changing.read() {
|
||||
for handle in selected.iter_mut() {
|
||||
if let Some(handle) = r_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| {
|
||||
find_handle_in_self_or_children(***selection, &q_children, &q_handles)
|
||||
})
|
||||
.and_then(|target| q_handles.get(target).ok())
|
||||
{
|
||||
if ev.is_modified(handle.id()) {
|
||||
ev_inspect.send(UpdateInspect::<Handle<T>>::default());
|
||||
}
|
||||
@@ -398,7 +542,9 @@ fn notify_asset_change_system<T: Asset>(
|
||||
}
|
||||
|
||||
fn auto_serialize_asset<T: Asset + Reflect + GetTypeRegistration>(
|
||||
q_t: Query<&Handle<T>, With<Selected>>,
|
||||
r_selection: Option<Res<Selection>>,
|
||||
q_t: Query<&Handle<T>>,
|
||||
q_children: Query<&Children>,
|
||||
mut description: ResMut<EntityDescription>,
|
||||
registry: Res<AppTypeRegistry>,
|
||||
mut ev_inspect: EventReader<UpdateInspect<Handle<T>>>,
|
||||
@@ -411,7 +557,11 @@ fn auto_serialize_asset<T: Asset + Reflect + GetTypeRegistration>(
|
||||
|
||||
ev_inspect.clear();
|
||||
|
||||
if let Some(handle) = q_t.iter().next() {
|
||||
if let Some(handle) = r_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| find_handle_in_self_or_children(***selection, &q_children, &q_t))
|
||||
.and_then(|target| q_t.get(target).ok())
|
||||
{
|
||||
if let Some(asset) = assets.get(handle) {
|
||||
let registry = registry.read();
|
||||
info!(
|
||||
@@ -441,22 +591,30 @@ fn auto_serialize_asset<T: Asset + Reflect + GetTypeRegistration>(
|
||||
ev_update_description.send(UpdateEntityDescription);
|
||||
}
|
||||
|
||||
#[derive(Reflect, Default)]
|
||||
#[reflect(Default)]
|
||||
#[derive(Reflect, Default, Clone, Component)]
|
||||
#[reflect(Default, Component)]
|
||||
struct ShortMaterial {
|
||||
pub base_color: Color,
|
||||
pub perceptual_roughness: f32,
|
||||
pub metallic: f32,
|
||||
pub reflectance: f32,
|
||||
}
|
||||
|
||||
impl ShortMaterial {
|
||||
pub fn new(material: &StandardMaterial) -> Self {
|
||||
Self {
|
||||
base_color: material.base_color,
|
||||
perceptual_roughness: material.perceptual_roughness,
|
||||
metallic: material.metallic,
|
||||
reflectance: material.reflectance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_standart_materal(
|
||||
r_selection: Option<Res<Selection>>,
|
||||
q_m: Query<&Handle<StandardMaterial>>,
|
||||
q_children: Query<&Children>,
|
||||
mut description: ResMut<EntityDescription>,
|
||||
registry: Res<AppTypeRegistry>,
|
||||
mut ev_inspect: EventReader<UpdateInspect<Handle<StandardMaterial>>>,
|
||||
@@ -469,8 +627,12 @@ fn serialize_standart_materal(
|
||||
|
||||
ev_inspect.clear();
|
||||
|
||||
if let Some(handle) = q_m.iter().next() {
|
||||
if let Some(material) = assets.get(handle) {
|
||||
if let Some((target, handle)) = r_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| find_handle_in_self_or_children(***selection, &q_children, &q_m))
|
||||
.and_then(|target| q_m.get(target).ok().map(|h| (target, h.clone())))
|
||||
{
|
||||
if let Some(material) = assets.get(handle.clone()) {
|
||||
let registry = registry.read();
|
||||
let short_material = ShortMaterial::new(material);
|
||||
let json =
|
||||
@@ -522,11 +684,11 @@ fn update_description(
|
||||
.components
|
||||
.iter()
|
||||
.map(|(name, v)| {
|
||||
let mut value = Value::Object(Map::new());
|
||||
let mut value = JsonValue::Object(JsonMap::new());
|
||||
value
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("name".to_string(), Value::String(name.to_string()));
|
||||
.insert("name".to_string(), JsonValue::String(name.to_string()));
|
||||
value
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
@@ -543,25 +705,54 @@ fn update_description(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
/// Find a `Handle<T>` component on this entity or one of its direct children
|
||||
fn find_handle_in_self_or_children<T: Asset>(
|
||||
target: Entity,
|
||||
q_children: &Query<&Children>,
|
||||
q_handles: &Query<&Handle<T>>,
|
||||
) -> Option<Entity> {
|
||||
if q_handles.contains(target) {
|
||||
return Some(target);
|
||||
}
|
||||
|
||||
let Ok(children) = q_children.get(target) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
children
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&child| q_handles.contains(child))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateField {
|
||||
pub component: String,
|
||||
pub reflect_path: String,
|
||||
pub value: serde_json::Value,
|
||||
pub value: JsonValue,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const EVENT_DECLARATION: &str = r#"
|
||||
|
||||
export interface InspectorChanged extends CustomEvent {
|
||||
export interface InspectorChangedEvent extends CustomEvent {
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface HTMLElementEventMap {
|
||||
"inspector-changed": InspectorChanged;
|
||||
export interface HTMLElementEventMap {
|
||||
"inspector-changed": InspectorChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateField<T> {
|
||||
component: string;
|
||||
reflectPath: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export function updateComponentField<T>(params: UpdateField<T>): void;
|
||||
"#;
|
||||
|
||||
pub(super) fn notify_changed(q_window: &Window, msg: String) {
|
||||
@@ -573,11 +764,71 @@ pub(super) fn notify_changed(q_window: &Window, msg: String) {
|
||||
|
||||
static CHANGE_FROM_UI: WasmMessageBuffer<UpdateField> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = changeValue)]
|
||||
pub fn change_value(msg: JsValue) {
|
||||
let Ok(msg) = serde_wasm_bindgen::from_value(msg) else {
|
||||
error!("Failed to parse message");
|
||||
#[wasm_bindgen(js_name = updateComponentField, skip_typescript)]
|
||||
pub fn update_component_field(msg: JsValue) -> Result<(), JsValue> {
|
||||
let msg = serde_wasm_bindgen::from_value(msg)?;
|
||||
info!("Received update: {msg:?}");
|
||||
CHANGE_FROM_UI.write_message(msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle transform rotation updates from the UI as a special case.
|
||||
///
|
||||
/// It's impossible to reliably compute a new quaternion for a desired Euler-
|
||||
/// axes or axis-angle rotation from the frontend, because the calculation is
|
||||
/// dependent on the parent's orientation. To deal with this, we provide a
|
||||
/// separate `rotate_local_transform` method for this operation and handle it
|
||||
/// with a separate Bevy system.
|
||||
static ROTATE_LOCAL_XFORM: WasmMessageBuffer<(RotationAxis, f32)> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub enum RotationAxis {
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = rotateLocalTransform)]
|
||||
pub fn rotate_local_transform(axis: RotationAxis, theta: f32) {
|
||||
ROTATE_LOCAL_XFORM.write_message((axis, theta));
|
||||
}
|
||||
|
||||
fn update_local_rotation_by_ui(
|
||||
r_selection: Option<Res<Selection>>,
|
||||
q_parents: Query<&Parent>,
|
||||
q_global_xforms: Query<&GlobalTransform>,
|
||||
mut q_xforms: Query<&mut Transform>,
|
||||
) {
|
||||
let Some((axis, theta)) = ROTATE_LOCAL_XFORM.take_message() else {
|
||||
return;
|
||||
};
|
||||
CHANGE_FROM_UI.write_message(msg);
|
||||
|
||||
let Some(selection) = r_selection.map(|selection| **selection) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(target_global_xform) = q_global_xforms.get(selection).map(|xform| xform.affine()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent_global_xform = q_parents
|
||||
.get(selection)
|
||||
.and_then(|parent| q_global_xforms.get(**parent))
|
||||
.unwrap_or(&GlobalTransform::IDENTITY)
|
||||
.affine();
|
||||
|
||||
let axis_ws = match axis {
|
||||
RotationAxis::X => target_global_xform.matrix3.x_axis,
|
||||
RotationAxis::Y => target_global_xform.matrix3.y_axis,
|
||||
RotationAxis::Z => target_global_xform.matrix3.z_axis,
|
||||
};
|
||||
|
||||
let axis_parent_space = parent_global_xform
|
||||
.inverse()
|
||||
.transform_vector3a(axis_ws)
|
||||
.normalize();
|
||||
|
||||
if let Ok(mut xform) = q_xforms.get_mut(selection) {
|
||||
xform.rotate_axis(axis_parent_space.into(), theta);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user