A sound asset now opens in a tab that shows its file as a waveform. Two handles set where the sound starts and ends in the file, and that stretch is the sample: Play sound, a song item and an instrument's Sample all play it and nothing else. The file itself is never changed. - The asset has three new rows: Start (s), End (s) and Edge fade (ms). End 0 is the end of the file, the fade is 10 ms by default. Older assets have no values and play the whole file as before. - Every way of playing asks playedSoundOf() for a name instead of the file: the file, or <file>#<start>~<end>~<fade> for a cut. - SoundLibrary makes the cut from the decoded file the first time that name plays and ramps each edge inside the file over the fade. Length and peaks of a cut are read from the file without a copy. - Cut buffers can be made again at any time, so the library lets the oldest go past 64 Mi samples. A voice holds its buffers itself. - The tab draws min/max per pixel from 256-sample block peaks, and the samples as a line when zoomed in far. Wheel zooms, the middle button pans, as in the music tabs. Wave, cut and ruler sit under a mask, so a handle panned out of view does not draw over the panels beside. - Dragging a handle moves that edge, dragging elsewhere spans a new stretch. One drag is one undo step for both edges. Whole file puts the handles back, Play or Space plays the stretch with a playhead. - The inspector preview and the Sound FX preview play the cut too, and the song lanes draw and repeat it. Tests: - Unit: sounds 20, wave-view 8 (new), sound-library 68, plus 258 in the touched areas (instrument, song, pattern, inspector, tabs) - e2e 34 on melektaus-atx: ALL PASSED, 57 checks (7 new) - Gate on melektaus-atx: CHECK EXIT 0, 253 files, 3936 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
497 lines
22 KiB
TypeScript
497 lines
22 KiB
TypeScript
import {
|
||
RenameAssetCommand,
|
||
ResetAssetPropertyCommand,
|
||
SetAssetPropertyCommand,
|
||
SetMachineScopeCommand,
|
||
SetVariablesPersistentCommand,
|
||
SetVariablesScopeCommand,
|
||
} from '../../core/commands/assetCommands';
|
||
import { bagWithoutObjects } from '../../core/graph/liveObjects';
|
||
import { isPersistent } from '../../core/graph/savedValues';
|
||
import { nextFieldName, nextVariableName } from '../../core/commands/naming';
|
||
import {
|
||
AddStructFieldCommand,
|
||
AddVariableCommand,
|
||
MoveStructFieldCommand,
|
||
MoveVariableCommand,
|
||
RemoveStructFieldCommand,
|
||
RemoveVariableCommand,
|
||
RenameStructFieldCommand,
|
||
RenameVariableCommand,
|
||
SetVariableKeptCommand,
|
||
SetVariablePrivateCommand,
|
||
} from '../../core/commands/variableCommands';
|
||
import type { Command } from '../../core/commands/Command';
|
||
import {
|
||
type Asset,
|
||
isKeptVariable,
|
||
isPrivateVariable,
|
||
isVariableOwner,
|
||
type StateMachineAsset,
|
||
type VariableOwnerAsset,
|
||
type VariablesAsset,
|
||
} from '../../core/model/Asset';
|
||
import { RENDER_LAYER_KEY } from '../../core/model/renderLayers';
|
||
import {
|
||
MakeListItemTypeCommand,
|
||
MakeStructTypeCommand,
|
||
SetListItemTypeCommand,
|
||
SetStructTypeCommand,
|
||
WithStructTypes,
|
||
} from '../../core/commands/structTypeCommands';
|
||
import { containsType, type FieldRename, structAssetOf } from '../../core/model/structTypes';
|
||
import { findAsset, type Project } from '../../core/model/Project';
|
||
import type { PropertyBag, PropertyValue } from '../../core/model/Properties';
|
||
import { readScope, readSharedScope, type Scope, scopeOf, sharedScopeOf } from '../../core/model/Scope';
|
||
import { type NestedKind, VARIABLE_KINDS } from '../../core/properties/kinds';
|
||
import {
|
||
COMMON_PROPS,
|
||
isRowShown,
|
||
type PropertyDef,
|
||
schemaForAsset,
|
||
type StructFieldDef,
|
||
} from '../../core/properties/PropertySchema';
|
||
import { markedAssetSchema } from '../../core/properties/deviation';
|
||
import { isModified, resolveAssetProps } from '../../core/properties/resolve';
|
||
import { Button, Dropdown, Label, Menu, type MenuEntry, type SortBlock, sortBlocks } from '../../ui';
|
||
import { FontPreview } from '../font/FontPreview';
|
||
import { clearSaved, hasSaved } from '../saves';
|
||
import { SoundPreview } from '../sound/SoundPreview';
|
||
import type { VariablesOwner } from '../state';
|
||
import { aspectActions, sizeCommand } from './aspectAction';
|
||
import { buildCutRows, CUT_ROW_KEYS, refreshCutRows } from './cutSection';
|
||
import { actionIf, createFieldRow, type FieldHandlers } from './fieldFactory';
|
||
import { addedRows, addNameField, addNote, addTitle, type InspectorHost, sectionHeader } from './host';
|
||
import { buildShapeListSection, shapeListSignature } from './shapeSection';
|
||
import { buildVariable, kindMenu, type StructTyping, type VariableEditing } from './variableRows';
|
||
import { listSource, previewingHandlers, structFields } from './previewHandlers';
|
||
import { addSpritePreview } from './SpritePreview';
|
||
import { assetValue } from './valueSpecs';
|
||
|
||
/** A struct field declares its own fields, starting with none. */
|
||
const newField = (key: string, kind: NestedKind): StructFieldDef =>
|
||
kind === 'struct' ? { key, kind, fields: [] } : { key, kind };
|
||
|
||
/**
|
||
* Structure-defining facts of an asset selection: the schema keys and kinds of the rows shown, plus
|
||
* the fields of a struct – those are rows of their own, so adding one has to rebuild the section.
|
||
*/
|
||
export function assetSignature(project: Project, assetId: string): string {
|
||
const asset = findAsset(project, assetId);
|
||
if (!asset) return 'missing';
|
||
// A machine's own rows are none, so its scope has to be named here or switching it rebuilds nothing.
|
||
const machine = asset.type === 'statemachine' ? `|${scopeOf(asset)}` : '';
|
||
// The same for the switch that keeps shared values between runs and for the scope beside it: the
|
||
// notes, the button and whether the switch is shown at all depend on the two.
|
||
const kept = asset.type === 'variables' ? `|${isPersistent(asset)}|${sharedScopeOf(asset)}` : '';
|
||
// A behavior's Keep and Private buttons are drawn once per build and read their switch from it.
|
||
const keeps =
|
||
asset.type === 'behavior'
|
||
? `|keep:${[...(asset.keptVariables ?? [])].sort().join(',')}` +
|
||
`|private:${[...(asset.privateVariables ?? [])].sort().join(',')}`
|
||
: '';
|
||
// Vector art shows its shapes under its rows (#129), so a shape added or renamed is a new structure.
|
||
const shapes = asset.type === 'vectorart' ? `|${shapeListSignature(asset)}` : '';
|
||
const values = resolveAssetProps(project, asset);
|
||
const rows = schemaForAsset(project, asset).filter((def) => isRowShown(def, values));
|
||
// A Type row shows the name of its Struct asset, or "Own fields" once that asset is gone (#253).
|
||
const types = `|types:${typeNames(project, rows)}`;
|
||
return rows.map(defSignature).join(',') + machine + kept + keeps + shapes + types;
|
||
}
|
||
|
||
function typeNames(project: Project, rows: readonly (PropertyDef | StructFieldDef)[]): string {
|
||
return rows
|
||
.flatMap((def): string[] =>
|
||
def.kind === 'struct'
|
||
? [structAssetOf(project, def.structType)?.name ?? '', typeNames(project, def.fields)]
|
||
: def.kind === 'list'
|
||
? [structAssetOf(project, def.itemType)?.name ?? '']
|
||
: [],
|
||
)
|
||
.join(',');
|
||
}
|
||
|
||
export function defSignature(def: PropertyDef | StructFieldDef): string {
|
||
// Only a struct has rows of its own, at any depth; a list's items rebuild themselves (#252).
|
||
const fields = def.kind === 'struct' ? def.fields.map(defSignature).join('+') : '';
|
||
// A type decides which of those rows may be changed, and names itself in a row (#253) – so does
|
||
// the item type of a list (#254).
|
||
const type =
|
||
def.kind === 'struct' && def.structType
|
||
? `@${def.structType}`
|
||
: def.kind === 'list' && def.itemType
|
||
? `@${def.itemType}`
|
||
: '';
|
||
return `${def.key}:${def.kind}${type}${fields ? `(${fields})` : ''}`;
|
||
}
|
||
|
||
/** 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`);
|
||
addNameField(host, asset.name, {
|
||
onChange: (name) => {
|
||
if (name.trim()) host.state.execute(new RenameAssetCommand(assetId, name.trim()));
|
||
else host.nameField?.setValue(asset.name);
|
||
},
|
||
});
|
||
|
||
// The switches in a group of their own, as on a placed object (#290). A behavior's rows are all
|
||
// switches: what it declares is listed under VARIABLES.
|
||
const schema = markedAssetSchema(host.state.project, asset);
|
||
const switches = schema.filter((def) => asset.type === 'behavior' || isSwitch(def.key));
|
||
buildProps(host, asset, switches, 'SWITCHES');
|
||
buildProps(
|
||
host,
|
||
asset,
|
||
schema.filter((def) => !switches.includes(def)),
|
||
'PROPERTIES',
|
||
);
|
||
if (isVariableOwner(asset)) buildVariables(host, asset);
|
||
if (asset.type === 'variables') {
|
||
addTitle(host, 'SCOPE');
|
||
buildVariablesScope(host, asset);
|
||
buildPersistence(host, asset);
|
||
}
|
||
if (asset.type === 'statemachine') {
|
||
addTitle(host, 'SCOPE');
|
||
buildMachineScope(host, asset);
|
||
}
|
||
// A drawing is sliced and sorted as a whole, with the rows a texture frame has (#308).
|
||
if (isDrawing(asset)) buildCutRows(host, { assetId }, 'CANVAS PIXELS');
|
||
// The drawing itself: its shapes are picked here as a texture's frames are picked in its tab (#129).
|
||
if (asset.type === 'vectorart') buildShapeListSection(host, asset);
|
||
// A font has no properties at all, so without this its inspector would be the name and nothing.
|
||
if (asset.type === 'font')
|
||
host.body.add(
|
||
asset.file ? new FontPreview(asset.file) : new Label('Not uploaded.', { style: 'warning' }),
|
||
);
|
||
// A sound has its rows above this; the preview is the quickest way to find out which sound it is.
|
||
if (asset.type === 'sound')
|
||
host.body.add(
|
||
asset.file ? new SoundPreview(host.state, assetId) : new Label('Not uploaded.', { style: 'warning' }),
|
||
);
|
||
if (asset.type === 'sprite') addSpritePreview(host, { kind: 'asset', assetId });
|
||
}
|
||
|
||
/** 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 project = host.state.project;
|
||
const values = shownValues(host, asset);
|
||
const drawing = isDrawing(asset);
|
||
for (const [key, row] of host.fields) {
|
||
if (drawing && CUT_ROW_KEYS.has(key)) continue;
|
||
row.setValue(values[key] ?? 0);
|
||
row.setModified(isModified(project, asset, key));
|
||
}
|
||
if (drawing) refreshCutRows(host, { assetId });
|
||
return true;
|
||
}
|
||
|
||
const isDrawing = (asset: Asset): boolean => asset.type === 'pixelart' || asset.type === 'vectorart';
|
||
|
||
/** The switches, and the render layer (#302): both say whether the object is seen at all. */
|
||
const isSwitch = (key: string): boolean =>
|
||
key === RENDER_LAYER_KEY || COMMON_PROPS.some((def) => def.key === key);
|
||
|
||
/** The rows of `schema` that are shown, under `title` – which is left out with them. */
|
||
function buildProps(host: InspectorHost, asset: Asset, schema: readonly PropertyDef[], title: string): void {
|
||
const project = () => host.state.project;
|
||
const values = resolveAssetProps(project(), asset);
|
||
const shown = schema.filter((d) => isRowShown(d, values));
|
||
if (shown.length > 0) addTitle(host, title);
|
||
for (const def of shown) {
|
||
const row = createFieldRow(
|
||
host,
|
||
def,
|
||
values[def.key] ?? def.default,
|
||
assetHandlers(host, asset.id, def),
|
||
{
|
||
extra: aspectActions(
|
||
host,
|
||
asset,
|
||
def,
|
||
() => {
|
||
const target = findAsset(host.state.project, asset.id);
|
||
return target ? resolveAssetProps(project(), target) : null;
|
||
},
|
||
(values) =>
|
||
host.state.execute(
|
||
sizeCommand(values, (axis, value) => new SetAssetPropertyCommand(asset.id, axis, value)),
|
||
),
|
||
),
|
||
reset: () => {
|
||
const target = findAsset(host.state.project, asset.id);
|
||
return actionIf(target && isModified(project(), target, def.key), 'Reset to default', () =>
|
||
host.state.execute(new ResetAssetPropertyCommand(asset.id, def.key)),
|
||
);
|
||
},
|
||
},
|
||
);
|
||
row.setModified(isModified(project(), 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 {
|
||
const spec = assetValue(host.state, assetId, def.key);
|
||
return {
|
||
...previewingHandlers<Asset>(host, spec),
|
||
...listSource(host, def, () => ({ kind: 'asset', assetId, key: def.key })),
|
||
...structFields<Asset>(host, def, spec, (path) => ({ kind: 'asset', assetId, key: def.key, path })),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* What the rows show: the values of the run while one is on, else what the document declares – so a
|
||
* value a graph counts up is visible without ever being saved.
|
||
*/
|
||
export function shownValues(host: InspectorHost, asset: Asset): PropertyBag {
|
||
const values = resolveAssetProps(host.state.project, asset);
|
||
const live = asset.type === 'variables' ? host.liveVariables(asset.id) : null;
|
||
return live ? { ...values, ...bagWithoutObjects(live) } : values;
|
||
}
|
||
|
||
/** The fields declared at `path` inside a struct declaration; none where the path leads nowhere. */
|
||
function fieldsAt(fields: readonly StructFieldDef[], path: readonly string[]): readonly StructFieldDef[] {
|
||
const [step, ...rest] = path;
|
||
if (step === undefined) return fields;
|
||
const field = fields.find((f) => f.key === step);
|
||
return field?.kind === 'struct' ? fieldsAt(field.fields, rest) : [];
|
||
}
|
||
|
||
/**
|
||
* Declared variables, each with its declaration editable – plus an "add" menu, and in the inspector
|
||
* the way to the same rows in a Variables tab (#252). A Struct asset's fields are declared the same
|
||
* way (#253); every change to them is carried on to the places of that type.
|
||
*/
|
||
export function buildVariables(host: InspectorHost, asset: VariableOwnerAsset, inTab = false): void {
|
||
const isType = asset.type === 'struct';
|
||
const header = sectionHeader(host, isType ? 'FIELDS' : 'VARIABLES');
|
||
const add = new Dropdown(isType ? '+ Field' : '+ Variable', { variant: 'ghost' });
|
||
const run = (command: Command, rename?: FieldRename): void =>
|
||
void host.state.execute(isType ? new WithStructTypes(command, rename) : command);
|
||
add.onClick = () => {
|
||
const items = kindMenu(VARIABLE_KINDS, (kind) => {
|
||
const current = findAsset(host.state.project, asset.id);
|
||
if (isVariableOwner(current))
|
||
run(new AddVariableCommand(asset.id, nextVariableName(current, isType ? 'field' : 'var'), kind));
|
||
});
|
||
new Menu(items).openBelow(add);
|
||
};
|
||
header.add(add);
|
||
if (!inTab) header.add(openVariablesButton(host, { kind: 'asset', assetId: asset.id }));
|
||
host.body.add(header);
|
||
|
||
if (asset.variables.length === 0) addNote(host, isType ? 'No fields yet.' : 'No variables yet.');
|
||
const values = shownValues(host, asset);
|
||
const renamed = (path: readonly string[], from: string, to: string): FieldRename | undefined =>
|
||
isType ? { structType: asset.id, path, from, to } : undefined;
|
||
const blocks: SortBlock[] = [];
|
||
for (const def of asset.variables) {
|
||
const edit: VariableEditing = {
|
||
rename: (next) => run(new RenameVariableCommand(asset.id, def.key, next), renamed([], def.key, next)),
|
||
remove: () => run(new RemoveVariableCommand(asset.id, def.key)),
|
||
renameField: (path, field, next) =>
|
||
run(
|
||
new RenameStructFieldCommand(asset.id, def.key, field.key, next, path),
|
||
renamed([def.key, ...path], field.key, next),
|
||
),
|
||
removeField: (path, field) => run(new RemoveStructFieldCommand(asset.id, def.key, field.key, path)),
|
||
addField: (path, kind) =>
|
||
run(
|
||
new AddStructFieldCommand(
|
||
asset.id,
|
||
def.key,
|
||
newField(nextFieldName(def.kind === 'struct' ? fieldsAt(def.fields, path) : []), kind),
|
||
path,
|
||
),
|
||
),
|
||
moveField: (path, from, to) => run(new MoveStructFieldCommand(asset.id, def.key, from, to, path)),
|
||
};
|
||
const behavior = asset.type === 'behavior';
|
||
const keep = behavior
|
||
? {
|
||
on: isKeptVariable(asset, def.key),
|
||
set: (on: boolean) => run(new SetVariableKeptCommand(asset.id, def.key, on)),
|
||
}
|
||
: undefined;
|
||
const hidden = behavior
|
||
? {
|
||
on: isPrivateVariable(asset, def.key),
|
||
set: (on: boolean) => run(new SetVariablePrivateCommand(asset.id, def.key, on)),
|
||
}
|
||
: undefined;
|
||
const value = values[def.key] ?? def.default;
|
||
const typing = structTyping(host, asset, def.key);
|
||
const { rows, built } = addedRows(host, () =>
|
||
buildVariable(host, def, value, assetHandlers(host, asset.id, def), { edit, typing, keep, hidden }),
|
||
);
|
||
host.fields.set(def.key, built);
|
||
blocks.push({ grip: built.grip, rows });
|
||
}
|
||
sortBlocks(blocks, (from, to) => run(new MoveVariableCommand(asset.id, from, to)));
|
||
}
|
||
|
||
/**
|
||
* The type picker of a struct variable and of its struct fields (#253): own fields, a Struct asset,
|
||
* or a new Struct asset made from the fields it holds now. Inside a Struct asset the types that
|
||
* hold that asset are not offered – the default of such a field would never end.
|
||
*/
|
||
function structTyping(host: InspectorHost, owner: VariableOwnerAsset, key: string): StructTyping {
|
||
const project = () => host.state.project;
|
||
const run = (command: Command): void => void host.state.execute(command);
|
||
return {
|
||
typeName: (structType) => structAssetOf(project(), structType)?.name ?? null,
|
||
menu: (path, structType) => {
|
||
const current = structAssetOf(project(), structType);
|
||
const target = { kind: 'variable', ownerId: owner.id, key, path } as const;
|
||
const types = project().assets.filter(
|
||
(a) => a.type === 'struct' && !(owner.type === 'struct' && containsType(project(), a.id, owner.id)),
|
||
);
|
||
const entries: MenuEntry[] = [
|
||
{ label: 'Own fields', current: !current, onSelect: () => run(new SetStructTypeCommand(target, '')) },
|
||
...types.map((type) => ({
|
||
label: type.name,
|
||
current: type.id === current?.id,
|
||
onSelect: () => run(new SetStructTypeCommand(target, type.id)),
|
||
})),
|
||
{ separator: true },
|
||
{
|
||
label: 'Make struct type',
|
||
disabled: current !== undefined,
|
||
onSelect: () => run(new MakeStructTypeCommand(owner.id, key, path)),
|
||
},
|
||
];
|
||
return entries;
|
||
},
|
||
listMenu: (path, itemType) => {
|
||
const current = structAssetOf(project(), itemType);
|
||
const target = { ownerId: owner.id, key, path };
|
||
// A list starts empty, so unlike a field a list may hold the type it sits in.
|
||
const types = project().assets.filter((a) => a.type === 'struct');
|
||
return [
|
||
{ label: 'Mixed', current: !current, onSelect: () => run(new SetListItemTypeCommand(target, '')) },
|
||
...types.map((type) => ({
|
||
label: type.name,
|
||
current: type.id === current?.id,
|
||
onSelect: () => run(new SetListItemTypeCommand(target, type.id)),
|
||
})),
|
||
{ separator: true },
|
||
{
|
||
label: 'Make struct type',
|
||
disabled: current !== undefined,
|
||
onSelect: () => run(new MakeListItemTypeCommand(target)),
|
||
},
|
||
];
|
||
},
|
||
};
|
||
}
|
||
|
||
/** Opens the variables of `owner` in a Variables tab of their own, or brings up the one they have. */
|
||
export function openVariablesButton(host: InspectorHost, owner: VariablesOwner): Button {
|
||
return new Button('Open', {
|
||
variant: 'ghost',
|
||
onClick: () => host.state.openValues({ kind: 'variables', owner }),
|
||
});
|
||
}
|
||
|
||
/** Not in the schema table either: a row there would share the bag with the declared values. */
|
||
const PERSISTENT: PropertyDef = { key: 'persistent', label: 'Persistent', kind: 'boolean', default: false };
|
||
|
||
/** Whether the values outlive the run (#166), what that means, and the way back to a first start. */
|
||
function buildPersistence(host: InspectorHost, asset: VariablesAsset): void {
|
||
const set = (value: PropertyValue): void =>
|
||
void host.state.execute(new SetVariablesPersistentCommand(asset.id, value === true));
|
||
// Nothing to keep between runs when every scene throws the values away – the switch is not shown.
|
||
if (sharedScopeOf(asset) === 'scene') {
|
||
addNote(host, 'One value each, built again for', 'small');
|
||
addNote(host, 'every scene that is loaded.', 'small');
|
||
return;
|
||
}
|
||
const row = createFieldRow(host, PERSISTENT, isPersistent(asset), { onInput: set, onChange: set });
|
||
host.body.add(row.row);
|
||
// Two lines rather than one: the panel is narrow enough to cut a longer note off mid-sentence.
|
||
if (!isPersistent(asset)) {
|
||
addNote(host, 'One value each for the whole run.', 'small');
|
||
addNote(host, 'Every graph pointing here shares them.', 'small');
|
||
return;
|
||
}
|
||
addNote(host, 'Kept between runs, in this browser.', 'small');
|
||
addNote(host, 'The next run starts where one ended.', 'small');
|
||
// An object is an instance of this run only: the next run has new ones, so there is nothing to keep.
|
||
if (asset.variables.some((def) => def.kind === 'object')) {
|
||
addNote(host, 'Object variables are not kept,', 'warning');
|
||
addNote(host, 'every run starts them at (null).', 'warning');
|
||
}
|
||
host.body.add(
|
||
new Button('Clear saved values', {
|
||
variant: 'ghost',
|
||
onClick: () => {
|
||
const projectId = host.state.project.id;
|
||
const had = hasSaved(projectId, asset.id);
|
||
clearSaved(projectId, asset.id);
|
||
const name = findAsset(host.state.project, asset.id)?.name ?? asset.name;
|
||
host.state.setNotice(had ? `Saved values of ${name} cleared` : 'Nothing saved yet');
|
||
},
|
||
}),
|
||
);
|
||
}
|
||
|
||
/** Not in the schema table: the scope lives on the asset itself, not in its property bag. */
|
||
const MACHINE_SCOPE: PropertyDef = {
|
||
key: 'scope',
|
||
label: 'Scope',
|
||
kind: 'enum',
|
||
default: 'local',
|
||
options: [
|
||
{ value: 'local', label: 'Local' },
|
||
{ value: 'global', label: 'Global' },
|
||
{ value: 'scene', label: 'Scene' },
|
||
],
|
||
};
|
||
|
||
const MACHINE_SCOPE_NOTES: Record<Scope, string[]> = {
|
||
local: ['Each behavior instance keeps its own current state.'],
|
||
global: ['Every behavior using this machine shares its', 'current state, for the whole run.'],
|
||
scene: ['Shared like global, but it starts over', 'whenever a scene is loaded.'],
|
||
};
|
||
|
||
/** A state machine has no properties of its own, so the inspector shows just its name and this. */
|
||
function buildMachineScope(host: InspectorHost, asset: StateMachineAsset): void {
|
||
const set = (value: PropertyValue): void =>
|
||
void host.state.execute(new SetMachineScopeCommand(asset.id, readScope(value)));
|
||
const row = createFieldRow(host, MACHINE_SCOPE, scopeOf(asset), { onInput: set, onChange: set });
|
||
host.body.add(row.row);
|
||
// Short lines rather than one: the panel is narrow enough to cut a longer note off mid-sentence.
|
||
for (const line of MACHINE_SCOPE_NOTES[scopeOf(asset)]) addNote(host, line, 'small');
|
||
}
|
||
|
||
/** The same switch for a Variables asset, which has no `local`: the asset is the shared thing. */
|
||
const VARIABLES_SCOPE: PropertyDef = {
|
||
key: 'scope',
|
||
label: 'Scope',
|
||
kind: 'enum',
|
||
default: 'global',
|
||
options: [
|
||
{ value: 'global', label: 'Global' },
|
||
{ value: 'scene', label: 'Scene' },
|
||
],
|
||
};
|
||
|
||
function buildVariablesScope(host: InspectorHost, asset: VariablesAsset): void {
|
||
const set = (value: PropertyValue): void =>
|
||
void host.state.execute(new SetVariablesScopeCommand(asset.id, readSharedScope(value)));
|
||
const row = createFieldRow(host, VARIABLES_SCOPE, sharedScopeOf(asset), {
|
||
onInput: set,
|
||
onChange: set,
|
||
});
|
||
host.body.add(row.row);
|
||
}
|