import { RenameAssetCommand, ResetAssetPropertyCommand, SetAssetPropertyCommand, } from '../../core/commands/assetCommands'; import { nextVariableName } from '../../core/commands/naming'; import { AddVariableCommand, RemoveVariableCommand, RenameVariableCommand, } from '../../core/commands/variableCommands'; import type { Asset, BehaviorAsset } from '../../core/model/Asset'; import { findAsset, type Project } from '../../core/model/Project'; import { COMMON_PROPS, type PropertyDef, schemaForAsset, VARIABLE_KINDS, type VariableKind, } from '../../core/properties/PropertySchema'; import { isModified, resolveAssetProps } from '../../core/properties/resolve'; import { Button, Dropdown, HStack, Label, Menu, Spacer, TextField, Theme } from '../../ui'; import { createField, createFieldRow, type FieldHandlers, labelledRow } from './fieldFactory'; import { addNote, addTitle, type InspectorHost } from './host'; import { previewingHandlers } from './previewHandlers'; const VARIABLE_KIND_LABELS: Record = { number: 'Number', boolean: 'Boolean', text: 'Text', }; // Keyed by the wide `PropertyDef['kind']` because `variables` is a `PropertyDef[]`, but only the // three VariableKinds ever reach it – the '?' fallback is a safety net, not a case to fill in. const VARIABLE_KIND_MARKS: Partial> = { number: '#', boolean: '✓', text: 'Aa', }; /** Structure-defining facts of an asset selection: the schema keys and kinds. */ export function assetSignature(project: Project, assetId: string): string { const asset = findAsset(project, assetId); return asset ? schemaForAsset(asset) .map((d) => `${d.key}:${d.kind}`) .join(',') : 'missing'; } /** Name, properties (asset values marked when they differ from the default) and, for behaviors, the variables. */ export function buildAssetSection(host: InspectorHost, assetId: string): void { const asset = findAsset(host.state.project, assetId); if (!asset) return; addTitle(host, `${asset.type.toUpperCase()} ASSET`); host.nameField = new TextField(asset.name, { onChange: (name) => { if (name.trim()) host.state.execute(new RenameAssetCommand(assetId, name.trim())); else host.nameField?.setValue(asset.name); }, }); const { row } = labelledRow('Name'); row.add(host.nameField, 1); host.body.add(row); if (asset.type === 'behavior') { buildProps(host, asset, COMMON_PROPS); buildVariables(host, asset); } else { buildProps(host, asset, schemaForAsset(asset)); } } /** Pushes the current asset values into the rows. False when the asset is gone (rebuild). */ export function refreshAssetSection(host: InspectorHost, assetId: string): boolean { const asset = findAsset(host.state.project, assetId); if (!asset) return false; host.nameField?.setValue(asset.name); const values = resolveAssetProps(asset); for (const [key, row] of host.fields) { row.setValue(values[key] ?? 0); row.setModified(isModified(asset, key)); } return true; } function buildProps(host: InspectorHost, asset: Asset, schema: readonly PropertyDef[]): void { const values = resolveAssetProps(asset); for (const def of schema) { const row = createFieldRow( host, def, values[def.key] ?? def.default, assetHandlers(host, asset.id, def), () => { const target = findAsset(host.state.project, asset.id); if (!target || !isModified(target, def.key)) return []; return [ { label: 'Reset to default', onSelect: () => host.state.execute(new ResetAssetPropertyCommand(asset.id, def.key)), }, ]; }, ); row.setModified(isModified(asset, def.key)); host.fields.set(def.key, row); host.body.add(row.row); } } /** Previews straight into the asset while editing; the command records the value from before the preview. */ function assetHandlers(host: InspectorHost, assetId: string, def: PropertyDef): FieldHandlers { return previewingHandlers(host, { target: () => findAsset(host.state.project, assetId), read: (asset) => asset.props[def.key], write: (asset, value) => (asset.props[def.key] = value), // No `remove`: an asset bag is dense – loading fills the defaults, a new variable brings its own. commit: (value) => new SetAssetPropertyCommand(assetId, def.key, value), }); } /** Variables of a behavior: kind mark, name, default value, remove – plus an "add" menu. */ function buildVariables(host: InspectorHost, asset: BehaviorAsset): void { const header = new HStack({ gap: Theme.gap, align: 'center' }); header.add(new Label('VARIABLES', { style: 'title' }), 1); const add = new Dropdown('+ Variable', { variant: 'ghost' }); add.onClick = () => { const items = VARIABLE_KINDS.map((kind) => ({ label: VARIABLE_KIND_LABELS[kind], onSelect: () => { const current = findAsset(host.state.project, asset.id); if (current?.type === 'behavior') host.state.execute(new AddVariableCommand(asset.id, nextVariableName(current), kind)); }, })); new Menu(items).openBelow(add); }; header.add(add); host.body.add(header); if (asset.variables.length === 0) addNote(host, 'No variables yet.'); const values = resolveAssetProps(asset); for (const def of asset.variables) { const row = new HStack({ gap: Theme.gap, align: 'center' }); row.minHeight = Theme.rowHeight; const mark = new Label(VARIABLE_KIND_MARKS[def.kind] ?? '?', { style: 'small', align: 'center' }); mark.minWidth = 18; row.add(mark); row.add( new TextField(def.key, { width: 90, onChange: (next) => { host.state.execute(new RenameVariableCommand(asset.id, def.key, next.trim())); }, }), ); const field = createField(host, def, values[def.key] ?? def.default, assetHandlers(host, asset.id, def)); row.add(field.widget, field.grow ? 1 : 0); if (!field.grow) row.add(new Spacer(), 1); row.add( new Button('×', { variant: 'ghost', width: Theme.controlHeight, onClick: () => host.state.execute(new RemoveVariableCommand(asset.id, def.key)), }), ); host.fields.set(def.key, { row, setValue: field.setValue, setModified: () => {} }); host.body.add(row); } }