mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-28 08:58:07 +00:00
Inspector frontend UI (#68)
This PR begins fleshing out the front-end UI for inspecting the selected entity's components. ### Implemented * Support for the `Name` component * Placeholder/WIP support for the `Transform` component ### To-do * Support for sending changes made in the inspector back to Bevy * A slider/field combo widget for numeric values (can be adapted from [this StackBlitz](https://stackblitz.com/edit/angular-wfikuy?file=src%2Ffloat-input%2Ffloat-input.component.ts)) * Quat -> Euler angles conversion ([example math here](https://github.com/dannymcgee/3DGameMath/blob/5cb7a8ff9310565329e57751cd8b9c33e9bd4cb7/libs/Math/include/math/quat.inl.hpp#L56-L78), or use a 3rd-party math library) ### Known issues * Selecting entities from the hierarchy doesn't always update the components list. I suspect this is the same issue as #39
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { DynamicProvider, UniqueToken, on, provide } from "@storyteller/framework";
|
||||
import * as studio from "@storyteller/studio";
|
||||
import {
|
||||
SceneElement,
|
||||
type EntityDespawnEvent,
|
||||
type EntityMultiSpawnEvent,
|
||||
type EntitySelectEvent,
|
||||
type EntitySpawnEvent,
|
||||
type InspectorChanged,
|
||||
} from "@storyteller/studio";
|
||||
import { TREE_SELECTION_PROVIDER, TreeSelectionProvider } from "@storyteller/studio-ui/tree";
|
||||
import { match } from "@storyteller/utility";
|
||||
import { LitElement, type PropertyValues, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { InspectorComponent } from "./inspector/inspector.types";
|
||||
|
||||
export interface SceneDataProvider extends DynamicProvider<SceneDataProvider> {
|
||||
readonly tree: readonly SceneObject[];
|
||||
readonly entityMap: ReadonlyMap<string, SceneObject>;
|
||||
readonly selectedEntity: string | null;
|
||||
readonly selectedObject: SceneObject | null;
|
||||
readonly selectionComponents: readonly InspectorComponent[];
|
||||
}
|
||||
|
||||
export const SCENE_DATA_PROVIDER = UniqueToken.create<SceneDataProvider>();
|
||||
|
||||
export interface SceneObject extends Omit<studio.SceneObject, "children"> {
|
||||
icon?: string;
|
||||
children: SceneObject[];
|
||||
}
|
||||
|
||||
@customElement("sts-scene-data-provider")
|
||||
@provide(SCENE_DATA_PROVIDER)
|
||||
@provide(TREE_SELECTION_PROVIDER)
|
||||
export class SceneDataProviderElement
|
||||
extends LitElement
|
||||
implements SceneDataProvider, TreeSelectionProvider
|
||||
{
|
||||
get tree() { return this._tree; }
|
||||
get entityMap() { return this.#entityMap; }
|
||||
|
||||
get selectedEntity() { return this._selection; }
|
||||
get selectedId() { return this._selection; }
|
||||
|
||||
get selectedObject() {
|
||||
if (!this._selection) return null;
|
||||
return this.#entityMap.get(this._selection) ?? null;
|
||||
}
|
||||
|
||||
get selectionComponents() { return this._components; }
|
||||
|
||||
@state() _tree: SceneObject[] = [];
|
||||
#sceneObjectsFlat: studio.SceneObject[] = [];
|
||||
#entityMap = new Map<string, SceneObject>();
|
||||
|
||||
@state() _selection: string | null = null;
|
||||
@state() _components: InspectorComponent[] = [];
|
||||
|
||||
// TODO: This workflow allows dependents of this provider to listen for the
|
||||
// "property-changes" event and manually trigger updates when relevant
|
||||
// properties change, but it's pretty fiddly to set up. Probably not
|
||||
// worth fixing before we migrate over to Bevy-native UI.
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
const downstreamChanges: PropertyValues<SceneDataProvider & TreeSelectionProvider>
|
||||
= new Map();
|
||||
|
||||
if (changes.has("_tree")) {
|
||||
downstreamChanges.set("tree", changes.get("_tree"));
|
||||
downstreamChanges.set("entityMap", new Map());
|
||||
}
|
||||
|
||||
if (changes.has("_selection")) {
|
||||
const prevSelection = changes.get("_selection");
|
||||
downstreamChanges.set("selectedEntity", prevSelection);
|
||||
downstreamChanges.set("selectedId", prevSelection);
|
||||
downstreamChanges.set(
|
||||
"selectedObject",
|
||||
prevSelection
|
||||
? this.#entityMap.get(prevSelection) ?? null
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
if (changes.has("_components"))
|
||||
downstreamChanges.set("selectionComponents", changes.get("_components"));
|
||||
|
||||
this.dispatchEvent(new CustomEvent("property-changes", { detail: downstreamChanges }));
|
||||
|
||||
super.update(changes);
|
||||
}
|
||||
|
||||
@on("window:entity-spawn")
|
||||
@on("window:entity-multi-spawn")
|
||||
_onEntitiesSpawned({ detail: entities }: EntitySpawnEvent | EntityMultiSpawnEvent): void {
|
||||
this.#sceneObjectsFlat = this.#sceneObjectsFlat.concat(entities);
|
||||
this.#rebuildTree();
|
||||
}
|
||||
|
||||
@on("window:entity-despawn")
|
||||
_onEntityDespawn({ detail: entity }: EntityDespawnEvent): void {
|
||||
this.#sceneObjectsFlat = this.#sceneObjectsFlat.filter(({ id }) => id !== entity);
|
||||
this.#rebuildTree();
|
||||
}
|
||||
|
||||
@on("window:entity-select")
|
||||
onEntitySelect({ detail: entity }: EntitySelectEvent): void {
|
||||
this._selection = entity;
|
||||
}
|
||||
|
||||
@on("window:inspector-changed")
|
||||
onInspectorChanged({ detail: json }: InspectorChanged): void {
|
||||
this._components = json ? JSON.parse(json) : [];
|
||||
}
|
||||
|
||||
#rebuildTree(): void {
|
||||
this.#entityMap.clear();
|
||||
|
||||
for (let obj of this.#sceneObjectsFlat) {
|
||||
this.#entityMap.set(obj.id, {
|
||||
...obj,
|
||||
icon: match (obj.type, {
|
||||
[SceneElement.Generic]: () => "cube",
|
||||
[SceneElement.Mesh]: () => "cube", // TODO ?
|
||||
[SceneElement.Skeleton]: () => "skeleton",
|
||||
[SceneElement.Bone]: () => "bone",
|
||||
[SceneElement.Light]: () => "light",
|
||||
[SceneElement.Camera]: () => "video",
|
||||
}),
|
||||
children: []
|
||||
});
|
||||
}
|
||||
|
||||
for (let obj of this.#entityMap.values()) {
|
||||
if (obj.parent != null) {
|
||||
const parent = this.#entityMap.get(obj.parent);
|
||||
if (!parent)
|
||||
throw new Error(`Missing parent: ${obj.parent}`);
|
||||
|
||||
parent.children.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
for (let obj of this.#entityMap.values())
|
||||
obj.children.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
this._tree = Array
|
||||
.from(this.#entityMap.values())
|
||||
.filter(obj => obj.parent == null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user