diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 624eb0e..292fe49 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.76.0" +channel = "stable" diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 0ef0494..622c554 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -40,6 +40,13 @@ export class AppElement extends LitElement { return this; } + @on("inspector-changed") + onInspectorChanged(event: studio.InspectorChanged): void { + const data = event.detail ? JSON.parse(event.detail) : null; + console.log("Inspector changed:", data); + } + + @on("scene-saved") async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { const file = new File([scene], "scene.scn.ron", { type: "text/ron" }); diff --git a/studio-frontend/src/main.ts b/studio-frontend/src/main.ts index cde5d45..ba385d9 100644 --- a/studio-frontend/src/main.ts +++ b/studio-frontend/src/main.ts @@ -1 +1 @@ -import "./app/app.element"; +import "./app/app.element"; \ No newline at end of file diff --git a/studio/src/ui/inspector.rs b/studio/src/ui/inspector.rs new file mode 100644 index 0000000..e1851ab --- /dev/null +++ b/studio/src/ui/inspector.rs @@ -0,0 +1,216 @@ +use std::marker::PhantomData; + +use bevy::{ + prelude::*, + reflect::{serde::ReflectSerializer, GetTypeRegistration}, + utils::HashMap, + window::PrimaryWindow, +}; +use serde::{Deserialize, Serialize}; +use serde_json::*; +use space_editor::space_prefab::component::MeshPrimitivePrefab; +use wasm_bindgen::prelude::*; + +use crate::{interaction::Selected, wasm}; + +pub struct InspectorPlugin; + +impl Plugin for InspectorPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.add_event::(); + + app.configure_sets( + Update, + ( + InspectorSystem::DetectChanges, + InspectorSystem::PerComponentCollect, + InspectorSystem::CollectChanges, + ) + .chain(), + ); + + app.add_systems( + Update, + update_description.in_set(InspectorSystem::CollectChanges), + ); + + app.register_inspect::(); + app.register_inspect::(); + app.register_inspect::(); + } +} + +trait AppInspectable { + fn register_inspect(&mut self); +} + +impl AppInspectable for App { + fn register_inspect(&mut self) { + self.register_type::() + .add_event::>() + .add_systems( + Update, + ( + update_inspect_by_select_change::, + update_inspect_by_remove::, + update_inspect_by_change::, + ) + .in_set(InspectorSystem::DetectChanges), + ) + .add_systems( + Update, + auto_serialize_system::.in_set(InspectorSystem::PerComponentCollect), + ); + } +} + +#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum InspectorSystem { + DetectChanges, + PerComponentCollect, + CollectChanges, +} + +#[derive(Event, Clone, Copy)] +pub struct UpdateInspect { + _phantom: PhantomData, +} + +#[derive(Event)] +pub struct UpdateEntityDescription; + +impl Default for UpdateInspect { + fn default() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +#[derive(Resource, Serialize, Deserialize, Default, Clone)] +pub struct EntityDescription { + pub components: HashMap, +} + +fn update_inspect_by_select_change( + mut ev_inspect: EventWriter>, + q_added: Query<(), Added>, + q_removed: RemovedComponents, +) { + if !q_added.is_empty() { + ev_inspect.send(UpdateInspect::::default()); + } + + if !q_removed.is_empty() { + ev_inspect.send(UpdateInspect::::default()); + } +} + +fn update_inspect_by_remove( + mut ev_inspect: EventWriter>, + mut q_t_removed: RemovedComponents, +) { + if let Some(_t) = q_t_removed.read().next() { + ev_inspect.send(UpdateInspect::::default()); + } +} + +fn update_inspect_by_change( + mut ev_inspect: EventWriter>, + q_t_changed: Query<&T, (With, Changed)>, +) { + if q_t_changed.iter().next().is_some() { + ev_inspect.send(UpdateInspect::::default()); + } +} + +fn auto_serialize_system( + q_t: Query<&T, With>, + mut description: ResMut, + registry: Res, + mut ev_inspect: EventReader>, + mut ev_update_description: EventWriter, +) { + if ev_inspect.is_empty() { + return; + } + + ev_inspect.clear(); + + if let Some(t) = q_t.iter().next() { + let registry = registry.read(); + let json = serde_json::to_value(ReflectSerializer::new(t, ®istry)).unwrap(); + info!("json: {:?}", json.to_string()); + description.components.insert( + T::get_type_registration() + .type_info() + .type_path() + .to_string(), + json, + ); + } else { + description + .components + .remove(T::get_type_registration().type_info().type_path()); + } + + ev_update_description.send(UpdateEntityDescription); +} + +fn update_description( + description: ResMut, + mut ev_update_description: EventReader, + mut q_windows: Query<&Window, With>, +) { + if ev_update_description.is_empty() { + return; + } + + ev_update_description.clear(); + + let values = description + .components + .iter() + .map(|(name, v)| { + let mut value = Value::Object(Map::new()); + value + .as_object_mut() + .unwrap() + .insert("name".to_string(), Value::String(name.to_string())); + value + .as_object_mut() + .unwrap() + .insert("value".to_string(), v[name].clone()); + value + }) + .collect::>(); + + //Write pretty json + // info!("description: {}", serde_json::to_string_pretty(&values).unwrap()); + + if let Some(window) = q_windows.iter_mut().next() { + notify_changed(window, serde_json::to_string_pretty(&values).unwrap()); + } +} + +#[wasm_bindgen(typescript_custom_section)] +const EVENT_DECLARATION: &str = r#" + +export interface InspectorChanged extends CustomEvent { + detail: string | null; +} + +declare global { +export interface HTMLElementEventMap { + "inspector-changed": InspectorChanged; +} +} +"#; + +pub(super) fn notify_changed(q_window: &Window, msg: String) { + let res = wasm::dispatch_to_js(q_window, "inspector-changed", &msg); + if let Err(e) = res { + error!("Failed to dispatch inspector-changed: {}", e); + } +} diff --git a/studio/src/ui/mod.rs b/studio/src/ui/mod.rs index 227e76b..d3f0385 100644 --- a/studio/src/ui/mod.rs +++ b/studio/src/ui/mod.rs @@ -4,10 +4,16 @@ use self::fps::FpsCounterPlugin; mod fps; +#[cfg(feature = "wasm")] +pub mod inspector; + pub struct UiLayerPlugin; impl Plugin for UiLayerPlugin { fn build(&self, app: &mut App) { app.add_plugins(FpsCounterPlugin); + + #[cfg(feature = "wasm")] + app.add_plugins(inspector::InspectorPlugin); } } diff --git a/studio/src/wasm/conv.rs b/studio/src/wasm/conv.rs index b7c4af3..679ed17 100644 --- a/studio/src/wasm/conv.rs +++ b/studio/src/wasm/conv.rs @@ -22,6 +22,8 @@ export interface ITransform { } export type Transform = BevyObject<"Transform", ITransform>; export type GlobalTransform = BevyObject<"GlobalTransform", ITransform>; + + "#; pub(crate) const SERIALIZER: Serializer = Serializer::new()