From 03303720f69f555b134fdc4298e6b08b28c5d53e Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Tue, 14 Nov 2023 20:19:30 -0500 Subject: [PATCH] POC scene serialization --- studio-frontend/src/app/app.element.ts | 19 +- studio/src/{scene.rs => scene/mod.rs} | 266 +------------------- studio/src/scene/wasm.rs | 326 +++++++++++++++++++++++++ studio/src/wasm/mod.rs | 8 + 4 files changed, 357 insertions(+), 262 deletions(-) rename studio/src/{scene.rs => scene/mod.rs} (52%) create mode 100644 studio/src/scene/wasm.rs diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 7ea4197..c96d5b3 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -5,6 +5,7 @@ import { type EntityMultiSpawnEvent, type EntitySelectEvent, type EntitySpawnEvent, + type SceneDownloadReadyEvent, TransformSpace, TransformType, ViewportSize, @@ -107,12 +108,28 @@ export class AppElement extends LitElement { this.#updateHierarchy(); } + @on("window:scene-download-ready") + onSceneDownloadReady({ detail: content }: SceneDownloadReadyEvent): void { + const fileBlob = new Blob([content], { type: "text/ron" }); + const url = URL.createObjectURL(fileBlob); + + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "scene.ron"; + anchor.click(); + + setTimeout(() => { + URL.revokeObjectURL(url); + anchor.remove(); + }); + } + toggleHelp(): void { this.showHelp = !this.showHelp; } saveScene(): void { - // TODO + studio.saveScene(); } onTransformSpaceChange(event: InputEvent): void { diff --git a/studio/src/scene.rs b/studio/src/scene/mod.rs similarity index 52% rename from studio/src/scene.rs rename to studio/src/scene/mod.rs index a31c510..c14b634 100644 --- a/studio/src/scene.rs +++ b/studio/src/scene/mod.rs @@ -22,6 +22,9 @@ use crate::{ MainCamera, }; +#[cfg(feature = "wasm")] +mod wasm; + pub struct ScenePlugin; impl Plugin for ScenePlugin { @@ -41,7 +44,8 @@ impl Plugin for ScenePlugin { .add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem)); #[cfg(feature = "wasm")] - app.add_systems(PostUpdate, wasm::notify_entity_spawn_changes.pipe(error)); + app.add_systems(PostUpdate, wasm::notify_entity_spawn_changes.pipe(error)) + .add_systems(Update, wasm::save_scene_system); } } @@ -255,7 +259,6 @@ fn mark_scene_objects( }) .collect::>(); - // If the sets match, this is the entity that represents the glTF mesh if prims_from_gltf == prims_from_ecs { cmd.entity(ent_parent).insert(Selectable); @@ -296,262 +299,3 @@ impl Material for ProceduralGridMaterial { AlphaMode::Blend } } - -#[cfg(feature = "wasm")] -mod wasm { - use std::collections::VecDeque; - - use bevy::{prelude::*, utils::HashSet, window::PrimaryWindow}; - use serde::Serialize; - use wasm_bindgen::prelude::*; - - use crate::wasm::{self, AsJson}; - - use super::Inspectable; - - #[wasm_bindgen(typescript_custom_section)] - const TYPE_DECLARATIONS: &str = r#" -export interface SceneObject { - /** The entity ID. */ - id: string; - /** A user-friendly name for displaying in the UI */ - name: string; - /** The IDs of this object's children in the hierarchy */ - children: string[]; - /** The ID of this object's parent in the hierarchy, if it has one */ - parent: string | null; - /** The local-space transform */ - transform: Transform; - /** The world-space transform */ - globalTransform: GlobalTransform; -} - -/** - * An event indicating that a new entity has spawned. - */ -export interface EntitySpawnEvent extends CustomEvent { - type: "entity-spawn"; - detail: SceneObject; -} - -/** - * An event indicating that an entity has been de-spawned. - */ -export interface EntityDespawnEvent extends CustomEvent { - type: "entity-despawn"; - /** The entity ID */ - detail: string; -} - -/** - * An event indicating that multiple entities have spawned. - */ -export interface EntityMultiSpawnEvent extends CustomEvent { - type: "entity-multi-spawn"; - detail: SceneObject[]; -} - -declare global { - export interface HTMLElementEventMap { - "entity-spawn": EntitySpawnEvent; - "entity-despawn": EntityDespawnEvent; - "entity-multi-spawn": EntityMultiSpawnEvent; - } -} - "#; - - #[derive(Serialize)] - pub(super) struct SceneObject { - id: String, - name: String, - children: Vec, - parent: Option, - transform: serde_json::Value, - #[serde(rename(serialize = "globalTransform"))] - global_transform: serde_json::Value, - } - - struct SceneObjectBuilder<'a> { - id: Entity, - name: Option, - children: Option>, - parent: Option, - transform: Option<&'a Transform>, - global_transform: Option<&'a GlobalTransform>, - } - - impl<'a> SceneObjectBuilder<'a> { - fn new(id: Entity) -> Self { - Self { - id, - name: None, - children: None, - parent: None, - transform: None, - global_transform: None, - } - } - fn finish(self) -> SceneObject { - SceneObject { - id: format!("{:?}", self.id), - name: self.name.unwrap(), - children: self - .children - .map(|children| children.iter().map(|ent| format!("{ent:?}")).collect()) - .unwrap_or_default(), - parent: self.parent.map(|ent| format!("{ent:?}")), - transform: self.transform.unwrap().as_json(), - global_transform: self.global_transform.unwrap().as_json(), - } - } - fn with_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } - fn with_children(mut self, children: Vec) -> Self { - self.children = Some(children); - self - } - fn with_parent(mut self, parent: Entity) -> Self { - self.parent = Some(parent); - self - } - fn with_transform(mut self, transform: &'a Transform) -> Self { - self.transform = Some(transform); - self - } - fn with_global_transform(mut self, global_transform: &'a GlobalTransform) -> Self { - self.global_transform = Some(global_transform); - self - } - } - - impl<'a> SceneObject { - fn build(id: Entity) -> SceneObjectBuilder<'a> { - SceneObjectBuilder::new(id) - } - } - - pub(super) fn notify_entity_spawn_changes( - mut l_notified: Local>, - // Note: We use locals for these data structures purely to reuse - // existing allocations across multiple invocations of this system - mut l_new_objects: Local>, - mut l_despawned_entities: Local>, - mut l_missing_parents: Local>, - q_window: Query<&Window, With>, - q_inspectables: Query<(Entity, &Transform, &GlobalTransform), With>, - q_others: Query<(Entity, &Transform, &GlobalTransform), Without>, - q_names: Query<&Name>, - q_parents: Query<&Children>, - q_children: Query<&Parent>, - ) -> Result<(), String> { - l_missing_parents.clear(); - - l_new_objects.clear(); - l_new_objects.extend( - q_inspectables - .iter() - .filter_map(|(ent, xform, world_xform)| { - build_scene_object( - &mut l_notified, - &mut l_missing_parents, - ent, - xform, - world_xform, - &q_inspectables, - &q_names, - &q_parents, - &q_children, - ) - }), - ); - - while let Some(parent) = l_missing_parents.pop_front() { - let Ok((ent, xform, world_xform)) = q_others.get(parent) else { - error!("Failed to find missing parent: {parent:?}"); - l_notified.insert(parent); - - continue; - }; - - if let Some(object) = build_scene_object( - &mut l_notified, - &mut l_missing_parents, - ent, - xform, - world_xform, - &q_inspectables, - &q_names, - &q_parents, - &q_children, - ) { - l_new_objects.push(object); - } - } - - l_despawned_entities.clear(); - l_despawned_entities.extend( - l_notified - .iter() - .filter(|ent| !q_inspectables.contains(**ent) && !q_others.contains(**ent)) - .cloned(), - ); - - let win = q_window.single(); - - for ent in l_despawned_entities.iter() { - // TODO: Make this take an array like the multi-spawn event - wasm::dispatch_to_js(win, "entity-despawn", ent)?; - l_notified.remove(ent); - } - - if !l_new_objects.is_empty() { - // TODO: Remove the single-spawn event - wasm::dispatch_to_js(win, "entity-multi-spawn", &*l_new_objects)?; - } - - Ok(()) - } - - fn build_scene_object( - notified: &mut HashSet, - missing_parents: &mut VecDeque, - ent: Entity, - xform: &Transform, - world_xform: &GlobalTransform, - q_inspectables: &Query<(Entity, &Transform, &GlobalTransform), With>, - q_names: &Query<&Name>, - q_parents: &Query<&Children>, - q_children: &Query<&Parent>, - ) -> Option { - if notified.contains(&ent) { - return None; - } - - notified.insert(ent); - - let mut result = SceneObject::build(ent) - .with_transform(xform) - .with_global_transform(world_xform); - - if let Ok(children) = q_parents.get(ent) { - result = result.with_children(children.iter().cloned().collect()); - } - if let Ok(parent) = q_children.get(ent) { - let parent_ent = **parent; - result = result.with_parent(parent_ent); - - if !notified.contains(&parent_ent) && !q_inspectables.contains(parent_ent) { - missing_parents.push_back(parent_ent); - } - } - - result = result.with_name(match q_names.get(ent) { - Ok(name) => name.into(), - Err(_) => format!("{ent:?}"), - }); - - Some(result.finish()) - } -} diff --git a/studio/src/scene/wasm.rs b/studio/src/scene/wasm.rs new file mode 100644 index 0000000..cb4d51f --- /dev/null +++ b/studio/src/scene/wasm.rs @@ -0,0 +1,326 @@ +use std::collections::VecDeque; + +use bevy::{ + prelude::*, reflect::serde::TypedReflectSerializer, utils::HashSet, window::PrimaryWindow, +}; +use serde::Serialize; +use wasm_bindgen::prelude::*; + +use crate::wasm::{self, AsJson, WasmMessageBuffer}; + +use super::Inspectable; + +static SCENE_REQUEST: WasmMessageBuffer<()> = WasmMessageBuffer::new(); + +#[wasm_bindgen(js_name = saveScene)] +pub fn save_scene() { + wasm::write_message!(SCENE_REQUEST, ()); +} + +pub(super) fn save_scene_system(world: &mut World) { + wasm::read_buffer!(SCENE_REQUEST, |_| { + let mut scene = DynamicScene::from_world(world); + let type_registry = world.resource::(); + + #[allow(clippy::borrowed_box)] + let is_serializable = |reflect: &Box| -> bool { + TypedReflectSerializer::new(&**reflect, &type_registry.read()) + .serialize(&wasm::SERIALIZER) + .is_ok() + }; + + // FIXME: This includes some stuff we don't want, including (most + // importantly) the _window_. We probably want to grab just the + // main camera, the skybox, the glTF scene instance and its + // children. + scene.resources = scene + .resources + .into_iter() + .filter(is_serializable) + .collect(); + + scene.entities = scene + .entities + .into_iter() + .map(|mut ent| { + ent.components = ent.components.into_iter().filter(is_serializable).collect(); + ent + }) + .collect(); + + match scene.serialize_ron(type_registry) { + Ok(serialized) => { + let Some(window) = world.iter_entities().find_map(|ent| { + if ent.contains::() { + ent.get::() + } else { + None + } + }) else { + warn!("Failed to get window from &mut World"); + return; + }; + + if let Err(err) = wasm::dispatch_to_js(window, "scene-download-ready", &serialized) + { + error!("{err}"); + } + } + Err(err) => { + error!("{err}"); + } + } + }); +} + +#[wasm_bindgen(typescript_custom_section)] +const TYPE_DECLARATIONS: &str = r#" +export interface SceneDownloadReadyEvent extends CustomEvent { + type: "scene-download-ready"; + detail: string; +} + +export interface SceneObject { + /** The entity ID. */ + id: string; + /** A user-friendly name for displaying in the UI */ + name: string; + /** The IDs of this object's children in the hierarchy */ + children: string[]; + /** The ID of this object's parent in the hierarchy, if it has one */ + parent: string | null; + /** The local-space transform */ + transform: Transform; + /** The world-space transform */ + globalTransform: GlobalTransform; +} + +/** + * An event indicating that a new entity has spawned. + */ +export interface EntitySpawnEvent extends CustomEvent { + type: "entity-spawn"; + detail: SceneObject; +} + +/** + * An event indicating that an entity has been de-spawned. + */ +export interface EntityDespawnEvent extends CustomEvent { + type: "entity-despawn"; + /** The entity ID */ + detail: string; +} + +/** + * An event indicating that multiple entities have spawned. + */ +export interface EntityMultiSpawnEvent extends CustomEvent { + type: "entity-multi-spawn"; + detail: SceneObject[]; +} + +declare global { + export interface HTMLElementEventMap { + "scene-download-ready": + "entity-spawn": EntitySpawnEvent; + "entity-despawn": EntityDespawnEvent; + "entity-multi-spawn": EntityMultiSpawnEvent; + } +} + "#; + +#[derive(Serialize)] +pub(super) struct SceneObject { + id: String, + name: String, + children: Vec, + parent: Option, + transform: serde_json::Value, + #[serde(rename(serialize = "globalTransform"))] + global_transform: serde_json::Value, +} + +struct SceneObjectBuilder<'a> { + id: Entity, + name: Option, + children: Option>, + parent: Option, + transform: Option<&'a Transform>, + global_transform: Option<&'a GlobalTransform>, +} + +impl<'a> SceneObjectBuilder<'a> { + fn new(id: Entity) -> Self { + Self { + id, + name: None, + children: None, + parent: None, + transform: None, + global_transform: None, + } + } + fn finish(self) -> SceneObject { + SceneObject { + id: format!("{:?}", self.id), + name: self.name.unwrap(), + children: self + .children + .map(|children| children.iter().map(|ent| format!("{ent:?}")).collect()) + .unwrap_or_default(), + parent: self.parent.map(|ent| format!("{ent:?}")), + transform: self.transform.unwrap().as_json(), + global_transform: self.global_transform.unwrap().as_json(), + } + } + fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + fn with_children(mut self, children: Vec) -> Self { + self.children = Some(children); + self + } + fn with_parent(mut self, parent: Entity) -> Self { + self.parent = Some(parent); + self + } + fn with_transform(mut self, transform: &'a Transform) -> Self { + self.transform = Some(transform); + self + } + fn with_global_transform(mut self, global_transform: &'a GlobalTransform) -> Self { + self.global_transform = Some(global_transform); + self + } +} + +impl<'a> SceneObject { + fn build(id: Entity) -> SceneObjectBuilder<'a> { + SceneObjectBuilder::new(id) + } +} + +pub(super) fn notify_entity_spawn_changes( + mut l_notified: Local>, + // Note: We use locals for these data structures purely to reuse + // existing allocations across multiple invocations of this system + mut l_new_objects: Local>, + mut l_despawned_entities: Local>, + mut l_missing_parents: Local>, + q_window: Query<&Window, With>, + q_inspectables: Query<(Entity, &Transform, &GlobalTransform), With>, + q_others: Query<(Entity, &Transform, &GlobalTransform), Without>, + q_names: Query<&Name>, + q_parents: Query<&Children>, + q_children: Query<&Parent>, +) -> Result<(), String> { + l_missing_parents.clear(); + + l_new_objects.clear(); + l_new_objects.extend( + q_inspectables + .iter() + .filter_map(|(ent, xform, world_xform)| { + build_scene_object( + &mut l_notified, + &mut l_missing_parents, + ent, + xform, + world_xform, + &q_inspectables, + &q_names, + &q_parents, + &q_children, + ) + }), + ); + + while let Some(parent) = l_missing_parents.pop_front() { + let Ok((ent, xform, world_xform)) = q_others.get(parent) else { + error!("Failed to find missing parent: {parent:?}"); + l_notified.insert(parent); + + continue; + }; + + if let Some(object) = build_scene_object( + &mut l_notified, + &mut l_missing_parents, + ent, + xform, + world_xform, + &q_inspectables, + &q_names, + &q_parents, + &q_children, + ) { + l_new_objects.push(object); + } + } + + l_despawned_entities.clear(); + l_despawned_entities.extend( + l_notified + .iter() + .filter(|ent| !q_inspectables.contains(**ent) && !q_others.contains(**ent)) + .cloned(), + ); + + let win = q_window.single(); + + for ent in l_despawned_entities.iter() { + // TODO: Make this take an array like the multi-spawn event + wasm::dispatch_to_js(win, "entity-despawn", ent)?; + l_notified.remove(ent); + } + + if !l_new_objects.is_empty() { + // TODO: Remove the single-spawn event + wasm::dispatch_to_js(win, "entity-multi-spawn", &*l_new_objects)?; + } + + Ok(()) +} + +fn build_scene_object( + notified: &mut HashSet, + missing_parents: &mut VecDeque, + ent: Entity, + xform: &Transform, + world_xform: &GlobalTransform, + q_inspectables: &Query<(Entity, &Transform, &GlobalTransform), With>, + q_names: &Query<&Name>, + q_parents: &Query<&Children>, + q_children: &Query<&Parent>, +) -> Option { + if notified.contains(&ent) { + return None; + } + + notified.insert(ent); + + let mut result = SceneObject::build(ent) + .with_transform(xform) + .with_global_transform(world_xform); + + if let Ok(children) = q_parents.get(ent) { + result = result.with_children(children.iter().cloned().collect()); + } + if let Ok(parent) = q_children.get(ent) { + let parent_ent = **parent; + result = result.with_parent(parent_ent); + + if !notified.contains(&parent_ent) && !q_inspectables.contains(parent_ent) { + missing_parents.push_back(parent_ent); + } + } + + result = result.with_name(match q_names.get(ent) { + Ok(name) => name.into(), + Err(_) => format!("{ent:?}"), + }); + + Some(result.finish()) +} diff --git a/studio/src/wasm/mod.rs b/studio/src/wasm/mod.rs index fc3f8b0..a2a141c 100644 --- a/studio/src/wasm/mod.rs +++ b/studio/src/wasm/mod.rs @@ -83,6 +83,14 @@ macro_rules! read_buffer { } } }}; + ($buffer:ident, |_| $stmt:stmt) => {{ + let buffer = crate::wasm::get_buffer!($buffer); + if let Some(lock) = buffer.try_lock() { + if lock.borrow_mut().take().is_some() { + $stmt + } + } + }}; } pub(crate) use get_buffer;