Proof-of-concept inspector mutations

This commit is contained in:
Danny McGee
2024-03-21 06:15:09 -04:00
parent 534101fa60
commit 57904cecb5
7 changed files with 145 additions and 47 deletions
@@ -24,6 +24,10 @@
} }
} }
*, *::before, *::after {
box-sizing: border-box;
}
[part^="icon-"] { [part^="icon-"] {
color: #FFF6; color: #FFF6;
} }
@@ -46,6 +46,7 @@ export class TextFieldElement extends FormFieldBaseElement {
` : nothing} ` : nothing}
<input <input
class="input" class="input"
part="input"
.type=${this.fieldType} .type=${this.fieldType}
placeholder=${ifDefined(this.placeholder)} placeholder=${ifDefined(this.placeholder)}
.value=${this.value} .value=${this.value}
@@ -1,3 +1,4 @@
import * as studio from "@storyteller/studio";
import { bind } from "@storyteller/framework"; import { bind } from "@storyteller/framework";
import { LitElement, PropertyValues, type TemplateResult, html, unsafeCSS } from "lit"; import { LitElement, PropertyValues, type TemplateResult, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
@@ -13,8 +14,20 @@ export class NameInspectorElement extends LitElement {
@property() value = ""; @property() value = "";
protected override updated(changes: PropertyValues<this>): void { protected override updated(changes: PropertyValues<this>): void {
if (changes.has("value")) // FIXME: `Name` component needs special handling due to:
console.log("New value:", this.value); // 1. Its `hash` field, which doesn't get updated by just directly
// mutating the `name` field
// 2. Its use in our hierarchy. We'll need to add some logic to check
// for changed names on existing entities and notify the frontend so
// the hierarchy can reflect the new value.
// if (changes.has("value")) {
// studio.updateComponentField({
// component: "bevy_core::name::Name",
// reflectPath: "name",
// value: this.value,
// });
// }
super.updated(changes); super.updated(changes);
} }
@@ -17,7 +17,17 @@ h4 {
margin: 0 0 0.25rem; margin: 0 0 0.25rem;
} }
fieldset { .fieldset {
position: relative;
padding-block-start: math.div(14, 16) + 0.333rem !important;
legend {
position: absolute;
inset-block-start: 0;
}
}
fieldset, .fieldset {
display: flex; display: flex;
width: 100%; width: 100%;
padding: 0; padding: 0;
@@ -33,14 +43,10 @@ fieldset {
input { input {
appearance: textfield; appearance: textfield;
width: math.div(1, 3) * 100%;
padding: 0.5rem;
border-radius: 0.25rem; border-radius: 0.25rem;
background: #0004; background: #0004;
border: 1px solid #FFF2 { border: 1px solid #FFF2;
left: 4px solid #FFF2;
}
color: #FFFC; color: #FFFC;
transition: { transition: {
property: background-color, border-color, color; property: background-color, border-color, color;
@@ -60,6 +66,29 @@ fieldset {
color: #FFFF; color: #FFFF;
} }
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
sts-text-field::part(input) {
flex: unset;
width: 100%;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
input, sts-text-field {
padding: 0.5rem;
width: math.div(1, 3) * 100%;
border-left-width: 4px;
&:nth-of-type(1) { &:nth-of-type(1) {
border-left-color: rgb(220, 53, 69); border-left-color: rgb(220, 53, 69);
&:focus { &:focus {
@@ -78,11 +107,5 @@ fieldset {
border-left-color: rgb(47, 131, 255); border-left-color: rgb(47, 131, 255);
} }
} }
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
} }
} }
@@ -1,3 +1,4 @@
import * as studio from "@storyteller/studio"
import { LitElement, html, unsafeCSS } from "lit"; import { LitElement, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { type Quat, type Vec3 } from "./inspector.types"; import { type Quat, type Vec3 } from "./inspector.types";
@@ -17,27 +18,76 @@ export class TransformInspectorElement extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
scale: Vec3 = { x: 1, y: 1, z: 1 }; scale: Vec3 = { x: 1, y: 1, z: 1 };
updateField(reflectPath: `${"translation"|"rotation"|"scale"}.${"x"|"y"|"z"}`) {
return ({ detail }: CustomEvent<string>) => {
const value = parseFloat(detail);
if (Number.isFinite(value) && !Number.isNaN(value)) {
studio.updateComponentField({
component: "bevy_transform::components::transform::Transform",
reflectPath,
value,
});
}
}
}
protected override render = () => html` protected override render = () => html`
<!-- TODO -->
<h4>Transform</h4> <h4>Transform</h4>
<fieldset> <div class="fieldset" role="group">
<legend>Translation</legend> <legend>Translation</legend>
<input type="number" value=${this.translation.x.toFixed(3)} /> <sts-text-field
<input type="number" value=${this.translation.y.toFixed(3)} /> type="number"
<input type="number" value=${this.translation.z.toFixed(3)} /> .value=${this.translation.x.toFixed(3)}
</fieldset> @value-change=${this.updateField("translation.x")}
<fieldset> ></sts-text-field>
<sts-text-field
type="number"
.value=${this.translation.y.toFixed(3)}
@value-change=${this.updateField("translation.y")}
></sts-text-field>
<sts-text-field
type="number"
.value=${this.translation.z.toFixed(3)}
@value-change=${this.updateField("translation.z")}
></sts-text-field>
</div>
<div class="fieldset" role="group">
<legend>Rotation</legend> <legend>Rotation</legend>
<input type="number" value=${this.rotation.x.toFixed(3)} /> <!-- TODO: This is not how quaternions work lol -->
<input type="number" value=${this.rotation.y.toFixed(3)} /> <sts-text-field
<input type="number" value=${this.rotation.z.toFixed(3)} /> type="number"
.value=${this.rotation.x.toFixed(3)}
@value-change=${this.updateField("rotation.x")}
></sts-text-field>
<sts-text-field
type="number"
.value=${this.rotation.y.toFixed(3)}
@value-change=${this.updateField("rotation.y")}
></sts-text-field>
<sts-text-field
type="number"
.value=${this.rotation.z.toFixed(3)}
@value-change=${this.updateField("rotation.z")}
></sts-text-field>
<!-- <input type="number" value=${this.rotation.w.toFixed(3)} /> --> <!-- <input type="number" value=${this.rotation.w.toFixed(3)} /> -->
</fieldset> </div>
<fieldset> <div class="fieldset" role="group">
<legend>Scale</legend> <legend>Scale</legend>
<input type="number" value=${this.scale.x.toFixed(3)} /> <sts-text-field
<input type="number" value=${this.scale.y.toFixed(3)} /> type="number"
<input type="number" value=${this.scale.z.toFixed(3)} /> .value=${this.scale.x.toFixed(3)}
</fieldset> @value-change=${this.updateField("scale.x")}
></sts-text-field>
<sts-text-field
type="number"
.value=${this.scale.y.toFixed(3)}
@value-change=${this.updateField("scale.y")}
></sts-text-field>
<sts-text-field
type="number"
.value=${this.scale.z.toFixed(3)}
@value-change=${this.updateField("scale.z")}
></sts-text-field>
</div>
`; `;
} }
@@ -6,7 +6,7 @@ import {
type EntityMultiSpawnEvent, type EntityMultiSpawnEvent,
type EntitySelectEvent, type EntitySelectEvent,
type EntitySpawnEvent, type EntitySpawnEvent,
type InspectorChanged, type InspectorChangedEvent,
} from "@storyteller/studio"; } from "@storyteller/studio";
import { TREE_SELECTION_PROVIDER, TreeSelectionProvider } from "@storyteller/studio-ui/tree"; import { TREE_SELECTION_PROVIDER, TreeSelectionProvider } from "@storyteller/studio-ui/tree";
import { match } from "@storyteller/utility"; import { match } from "@storyteller/utility";
@@ -109,7 +109,7 @@ export class SceneDataProviderElement
} }
@on("window:inspector-changed") @on("window:inspector-changed")
onInspectorChanged({ detail: json }: InspectorChanged): void { onInspectorChanged({ detail: json }: InspectorChangedEvent): void {
this._components = json ? JSON.parse(json) : []; this._components = json ? JSON.parse(json) : [];
} }
+22 -15
View File
@@ -8,7 +8,7 @@ use bevy::{
window::PrimaryWindow, window::PrimaryWindow,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::*; use serde_json::{self, json, Map as JsonMap, Value as JsonValue};
use space_editor::{ use space_editor::{
space_editor_ui::{ space_editor_ui::{
editor_tab::{EditorTab, EditorTabName}, editor_tab::{EditorTab, EditorTabName},
@@ -189,7 +189,7 @@ impl EditorTab for TestInspectorTab {
CHANGE_FROM_UI.write_message(UpdateField { CHANGE_FROM_UI.write_message(UpdateField {
component: "bevy_transform::components::transform::Transform".to_string(), component: "bevy_transform::components::transform::Transform".to_string(),
reflect_path: ".translation.x".to_string(), reflect_path: ".translation.x".to_string(),
value: serde_json::Value::from(0.0), value: JsonValue::from(0.0),
}); });
} }
} }
@@ -316,7 +316,7 @@ fn update_field_by_ui(world: &mut World) {
#[derive(Resource, Serialize, Deserialize, Default, Clone)] #[derive(Resource, Serialize, Deserialize, Default, Clone)]
pub struct EntityDescription { pub struct EntityDescription {
pub components: HashMap<String, Value>, pub components: HashMap<String, JsonValue>,
} }
fn update_inspect_by_select_change<T: Component + Reflect + GetTypeRegistration>( fn update_inspect_by_select_change<T: Component + Reflect + GetTypeRegistration>(
@@ -524,11 +524,11 @@ fn update_description(
.components .components
.iter() .iter()
.map(|(name, v)| { .map(|(name, v)| {
let mut value = Value::Object(Map::new()); let mut value = JsonValue::Object(JsonMap::new());
value value
.as_object_mut() .as_object_mut()
.unwrap() .unwrap()
.insert("name".to_string(), Value::String(name.to_string())); .insert("name".to_string(), JsonValue::String(name.to_string()));
value value
.as_object_mut() .as_object_mut()
.unwrap() .unwrap()
@@ -546,24 +546,33 @@ fn update_description(
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateField { pub struct UpdateField {
pub component: String, pub component: String,
pub reflect_path: String, pub reflect_path: String,
pub value: serde_json::Value, pub value: JsonValue,
} }
#[wasm_bindgen(typescript_custom_section)] #[wasm_bindgen(typescript_custom_section)]
const EVENT_DECLARATION: &str = r#" const EVENT_DECLARATION: &str = r#"
export interface InspectorChanged extends CustomEvent { export interface InspectorChangedEvent extends CustomEvent {
detail: string | null; detail: string | null;
} }
declare global { declare global {
export interface HTMLElementEventMap { export interface HTMLElementEventMap {
"inspector-changed": InspectorChanged; "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) { pub(super) fn notify_changed(q_window: &Window, msg: String) {
@@ -575,11 +584,9 @@ pub(super) fn notify_changed(q_window: &Window, msg: String) {
static CHANGE_FROM_UI: WasmMessageBuffer<UpdateField> = WasmMessageBuffer::new(); static CHANGE_FROM_UI: WasmMessageBuffer<UpdateField> = WasmMessageBuffer::new();
#[wasm_bindgen(js_name = changeValue)] #[wasm_bindgen(js_name = updateComponentField, skip_typescript)]
pub fn change_value(msg: JsValue) { pub fn update_component_field(msg: JsValue) -> Result<(), JsValue> {
let Ok(msg) = serde_wasm_bindgen::from_value(msg) else { let msg = serde_wasm_bindgen::from_value(msg)?;
error!("Failed to parse message");
return;
};
CHANGE_FROM_UI.write_message(msg); CHANGE_FROM_UI.write_message(msg);
Ok(())
} }