Remove comments that only restate the code, keeping the section banners and the notes that carry a reason, a measured fact or an external contract. ButtonOptions.width claimed to be a fixed width but only set minWidth, so the Graph and Texture pickers grew with the name they showed. It is now what it promises, with a separate minWidth for the buttons that have two forms. Narrow buttons shrink their padding, otherwise a 24px icon button had 8px left for its glyph and truncate dropped it entirely. __gm.labelsDropped() guards that class: the overflow guard only saw text that was too wide, never a label truncated to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
6.3 KiB
TypeScript
171 lines
6.3 KiB
TypeScript
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<VariableKind, string> = {
|
||
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<Record<PropertyDef['kind'], string>> = {
|
||
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<Asset>(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);
|
||
}
|
||
}
|