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(); } }