89 lines
3.9 KiB
TypeScript
89 lines
3.9 KiB
TypeScript
import type { Container } from 'pixi.js';
|
||
import { Label, Panel, ScrollContainer, Theme, ui, VStack } from '../../ui';
|
||
import { type EditorState, sameSelection, type Selection } from '../state';
|
||
import { assetSignature, buildAssetSection, refreshAssetSection } from './assetSection';
|
||
import type { InspectorHost } from './host';
|
||
import { buildNodeSection, nodeSignature, refreshNodeSection } from './nodeSection';
|
||
import { buildObjectSection, buildObjectsSection, objectSignature, refreshObjectSection } from './objectSection';
|
||
|
||
/**
|
||
* Shows the properties of the current selection. The rows are generated from the
|
||
* PropertySchema by one section per selection kind (asset, scene object, graph node); the
|
||
* panel only decides when a section has to be rebuilt and when its values merely refresh.
|
||
*/
|
||
export class Inspector extends Panel {
|
||
private readonly scroll = new ScrollContainer();
|
||
private host: InspectorHost;
|
||
private builtFor: Selection = null;
|
||
/** Describes the structure the rows were built for; a change means a rebuild. */
|
||
private signature = '';
|
||
/** Last live refresh (play mode), in ms – the world ticks far more often than the eye needs. */
|
||
private lastLiveRefresh = 0;
|
||
|
||
constructor(private readonly state: EditorState) {
|
||
super({ title: 'Inspector', padding: 0, gap: 0 });
|
||
this.host = this.emptyHost();
|
||
this.scroll.setContent(this.host.body);
|
||
this.content.add(this.scroll, 1);
|
||
state.on('selection', () => this.rebuild());
|
||
state.on('load', () => this.rebuild());
|
||
state.on('project', () => this.refreshValues());
|
||
state.on('runtime', () => this.refreshLive());
|
||
this.rebuild();
|
||
}
|
||
|
||
private emptyHost(): InspectorHost {
|
||
return { state: this.state, body: new VStack({ padding: Theme.padding, gap: Theme.gap }), fields: new Map(), nameField: null };
|
||
}
|
||
|
||
/**
|
||
* Play mode: show what the behaviors are doing to the selected object, ~10× a second.
|
||
* Skipped while a field in here is being edited so the typed text is not overwritten.
|
||
*/
|
||
private refreshLive(): void {
|
||
const now = performance.now();
|
||
if (now - this.lastLiveRefresh < 100) return;
|
||
for (let node: Container | null = ui().focus.focused; node; node = node.parent) if (node === this) return;
|
||
this.lastLiveRefresh = now;
|
||
this.refreshValues();
|
||
}
|
||
|
||
private rebuild(): void {
|
||
this.builtFor = this.state.selection;
|
||
this.host.body.destroy({ children: true });
|
||
this.host = this.emptyHost();
|
||
this.signature = this.currentSignature();
|
||
|
||
const s = this.state.selection;
|
||
if (!s) this.host.body.add(new Label('Select an asset, a scene object or a node.', { style: 'muted' }));
|
||
else if (s.kind === 'asset') buildAssetSection(this.host, s.assetId);
|
||
else if (s.kind === 'node') buildNodeSection(this.host, s.assetId, s.nodeId);
|
||
else if (s.objectIds.length > 1) buildObjectsSection(this.host, s.sceneId, s.objectIds);
|
||
else buildObjectSection(this.host, s.sceneId, s.objectId);
|
||
this.scroll.setContent(this.host.body);
|
||
}
|
||
|
||
private currentSignature(): string {
|
||
const s = this.state.selection;
|
||
if (!s) return '';
|
||
if (s.kind === 'asset') return assetSignature(this.state.project, s.assetId);
|
||
if (s.kind === 'node') return nodeSignature(this.state.project, s.assetId, s.nodeId);
|
||
return objectSignature(this.state.project, s.sceneId, s.objectIds, s.objectId);
|
||
}
|
||
|
||
/** Pushes current model values into the widgets (after undo, drag in viewport, …). */
|
||
private refreshValues(): void {
|
||
const s = this.state.selection;
|
||
if (!sameSelection(s, this.builtFor) || this.currentSignature() !== this.signature) {
|
||
this.rebuild();
|
||
return;
|
||
}
|
||
if (!s) return;
|
||
const ok =
|
||
s.kind === 'asset' ? refreshAssetSection(this.host, s.assetId)
|
||
: s.kind === 'node' ? refreshNodeSection(this.host, s.assetId, s.nodeId)
|
||
: s.objectIds.length > 1 || refreshObjectSection(this.host, s.sceneId, s.objectId);
|
||
if (!ok) this.rebuild();
|
||
}
|
||
}
|