Inspector: component view part (#55)

This PR allow to send entity components data to typescript with
inspector-dom event

Event data

![image](https://github.com/storytold/storyteller-bevy/assets/10954133/f4b5a88e-0a8d-4d56-ade2-d0102a6c08f7)

Component register example

![image](https://github.com/storytold/storyteller-bevy/assets/10954133/f9480f0a-a657-4445-a0d4-0984e2f69900)

Read event example

![image](https://github.com/storytold/storyteller-bevy/assets/10954133/2b6480f0-d1fb-42f1-bd6e-dfbc3d4676a2)


This PR is the first step of new inspector backend logic
Next parts is:
- Allow to edit components fields 
- Allow to show asset data (like show Handle<StandartMaterial> data)
- Allow to edit asset fields
This commit is contained in:
rewin
2024-02-29 23:17:51 +03:00
committed by GitHub
parent f31cf35da1
commit ae88696213
6 changed files with 233 additions and 2 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.76.0"
channel = "stable"
+7
View File
@@ -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<void> {
const file = new File([scene], "scene.scn.ron", { type: "text/ron" });
+1 -1
View File
@@ -1 +1 @@
import "./app/app.element";
import "./app/app.element";
+216
View File
@@ -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::<EntityDescription>();
app.add_event::<UpdateEntityDescription>();
app.configure_sets(
Update,
(
InspectorSystem::DetectChanges,
InspectorSystem::PerComponentCollect,
InspectorSystem::CollectChanges,
)
.chain(),
);
app.add_systems(
Update,
update_description.in_set(InspectorSystem::CollectChanges),
);
app.register_inspect::<Transform>();
app.register_inspect::<Name>();
app.register_inspect::<MeshPrimitivePrefab>();
}
}
trait AppInspectable {
fn register_inspect<T: Component + Reflect + GetTypeRegistration>(&mut self);
}
impl AppInspectable for App {
fn register_inspect<T: Component + Reflect + GetTypeRegistration>(&mut self) {
self.register_type::<T>()
.add_event::<UpdateInspect<T>>()
.add_systems(
Update,
(
update_inspect_by_select_change::<T>,
update_inspect_by_remove::<T>,
update_inspect_by_change::<T>,
)
.in_set(InspectorSystem::DetectChanges),
)
.add_systems(
Update,
auto_serialize_system::<T>.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<T> {
_phantom: PhantomData<T>,
}
#[derive(Event)]
pub struct UpdateEntityDescription;
impl<T> Default for UpdateInspect<T> {
fn default() -> Self {
Self {
_phantom: PhantomData,
}
}
}
#[derive(Resource, Serialize, Deserialize, Default, Clone)]
pub struct EntityDescription {
pub components: HashMap<String, Value>,
}
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());
}
}
fn update_inspect_by_remove<T: Component + Reflect + GetTypeRegistration>(
mut ev_inspect: EventWriter<UpdateInspect<T>>,
mut q_t_removed: RemovedComponents<T>,
) {
if let Some(_t) = q_t_removed.read().next() {
ev_inspect.send(UpdateInspect::<T>::default());
}
}
fn update_inspect_by_change<T: Component + Reflect + GetTypeRegistration>(
mut ev_inspect: EventWriter<UpdateInspect<T>>,
q_t_changed: Query<&T, (With<Selected>, Changed<T>)>,
) {
if q_t_changed.iter().next().is_some() {
ev_inspect.send(UpdateInspect::<T>::default());
}
}
fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
q_t: Query<&T, With<Selected>>,
mut description: ResMut<EntityDescription>,
registry: Res<AppTypeRegistry>,
mut ev_inspect: EventReader<UpdateInspect<T>>,
mut ev_update_description: EventWriter<UpdateEntityDescription>,
) {
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, &registry)).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<EntityDescription>,
mut ev_update_description: EventReader<UpdateEntityDescription>,
mut q_windows: Query<&Window, With<PrimaryWindow>>,
) {
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::<Vec<_>>();
//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);
}
}
+6
View File
@@ -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);
}
}
+2
View File
@@ -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()