mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-25 08:30:24 +00:00
Inspector editing (#96)
This PR adds Material component inspection to the frontend Entity Inspector, and adds functional editing capabilities for the Transform and Material inspectors. ### Changes * Added a `ScalarFieldElement` for numeric form fields, with a hybrid slider/text-input UX inspired by Blender, Unreal Engine, etc. * Added `MaterialInspectorElement` for inspecting the Bevy `StandardMaterial` component * Wired up the `TransformInspectorElement` and `MaterialInspectorElement` to apply changes made in the UI to the Bevy scene * Refactored the `studio/ui/inspector` module to output `Result::Err`s to the dev console * Refactored the `studio/interaction/selection` module to use only the `Selection` resource, removing the redundant `Selected` component which added some unnecessary complexity. This fixes #39 * Fixed primitive shapes not receiving a selection outline when selected ### To-Do These tasks will be addressed in a forthcoming PR: * The **Transform** > **Rotation** section in the inspector is still not displaying sensible values. Currently it's just displaying the raw quaternion `xyz` vector rather than displaying the orientation in human-readable terms (e.g., as Euler angles). * We're currently ignoring the Primitive Shape component, which would allow the user to configure some additional geometric properties of the shape or replace it with a different one. --------- Co-authored-by: rewin <rewin1996@gmail.com>
This commit is contained in:
+281
-103
@@ -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},
|
||||
EditorUiAppExt,
|
||||
},
|
||||
space_prefab::component::MeshPrimitivePrefab,
|
||||
space_prefab::{
|
||||
component::MeshPrimitivePrefab, editor_registry::EditorRegistryExt, save::SaveState,
|
||||
},
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
interaction::Selected,
|
||||
interaction::Selection,
|
||||
wasm::{self, StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
|
||||
@@ -48,7 +50,9 @@ impl Plugin for InspectorPlugin {
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
update_field_by_ui.in_set(InspectorSystem::WriteChanges),
|
||||
update_field_by_ui
|
||||
.map(bevy::utils::error)
|
||||
.in_set(InspectorSystem::WriteChanges),
|
||||
);
|
||||
|
||||
app.editor_tab_by_trait(
|
||||
@@ -64,12 +68,13 @@ impl Plugin for InspectorPlugin {
|
||||
|
||||
//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 +86,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 +188,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 +210,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 +222,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 +251,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 +285,171 @@ 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;
|
||||
};
|
||||
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::<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 +462,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 +487,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 +508,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 +530,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 +545,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 +579,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 +615,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 +672,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 +693,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 +752,10 @@ 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");
|
||||
return;
|
||||
};
|
||||
#[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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user