mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Merge branch 'main' into update-version
This commit is contained in:
@@ -47,11 +47,15 @@ futures-lite = "2.2.0"
|
||||
futures = "0.3.30"
|
||||
image = "0.25.0"
|
||||
bytemuck = "1.14.1"
|
||||
|
||||
space_editor = {git = "https://github.com/rewin123/space_editor", rev = "7b93e21ee1eb8a42418890b6cd525eaa7db43b95"}
|
||||
anyhow.workspace = true
|
||||
serde_repr = "0.1.18"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
async-compat = "0.2"
|
||||
uuid = { version = "1", features = ["v4", "serde"]}
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12.2"
|
||||
features = [
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -41,7 +41,7 @@
|
||||
"headless": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "cargo run -p studio --bin studio-headless -- {args}"
|
||||
"command": "cargo run -p studio --bin studio-headless --"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
|
||||
@@ -2,11 +2,12 @@ import fs from "node:fs";
|
||||
import url from "node:url";
|
||||
import path from "node:path";
|
||||
import { rimraf } from "rimraf";
|
||||
import which from "which";
|
||||
|
||||
import copyAssets from "./copy-assets.mjs";
|
||||
import handleError from "./handle-error.mjs";
|
||||
import run from "./run.mjs";
|
||||
import run, { wasmBindgenCliVersion } from "./run.mjs";
|
||||
|
||||
const WASM_BINDGEN_CLI_VERSION = "0.2.92";
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
@@ -39,11 +40,11 @@ async function main() {
|
||||
stdio,
|
||||
});
|
||||
|
||||
const wasmBindgenInstalled = Boolean(await which("wasm-bindgen", { nothrow: true }));
|
||||
if (!wasmBindgenInstalled) {
|
||||
const wbVersion = await wasmBindgenCliVersion();
|
||||
if (!wbVersion || !wbVersion.includes(WASM_BINDGEN_CLI_VERSION)) {
|
||||
await run("cargo", [
|
||||
"install", "-f", "wasm-bindgen-cli",
|
||||
"--version", "0.2.88",
|
||||
"--version", WASM_BINDGEN_CLI_VERSION,
|
||||
], {
|
||||
shell: true,
|
||||
cwd: workspaceRoot,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import chalk from "chalk";
|
||||
import cp from "node:child_process";
|
||||
import which from "which";
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
@@ -25,3 +26,35 @@ export default function run(command, args, options) {
|
||||
.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
export async function wasmBindgenCliVersion() {
|
||||
const exePath = await which("wasm-bindgen", { nothrow: true });
|
||||
if (!exePath) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
const finish = code => {
|
||||
if (code) reject(new Error(stderr));
|
||||
else resolve(stdout);
|
||||
}
|
||||
|
||||
const proc = cp.spawn(exePath, ["--version"], {
|
||||
shell: true,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
.on("exit", finish)
|
||||
.on("close", finish)
|
||||
.on("error", reject);
|
||||
|
||||
proc.stdout.setEncoding("utf-8");
|
||||
proc.stdout.on("data", chunk => stdout += chunk);
|
||||
|
||||
proc.stderr.setEncoding("utf-8");
|
||||
proc.stderr.on("data", chunk => stderr += chunk);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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