Describe every value kind in one table instead of spreading it over the code
A kind like colour or texture used to be spelled out in five parallel lists, its port type, its empty value, its glyph, its label and its list tag - 22 literal places for texture alone. core/properties/kinds.ts now holds one row per kind and the rest is derived from it, so adding a kind is one row. The five literal nodes collapse into literals.ts, the six kind menus into one helper. Number, Text, Boolean is now the one order everywhere; the variable and port menus used to list Boolean before Text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a7185948d
commit
3ed8b68ecf
39 files changed
+699
-565
No files matched your search
@@ -6,9 +6,10 @@ import {
|
||||
assetRefLabel,
|
||||
NO_ASSET_LABEL,
|
||||
} from '../../core/properties/assetRef';
|
||||
import { isKindWith, type ReferenceKind } from '../../core/properties/kinds';
|
||||
import {
|
||||
emptyFieldValue,
|
||||
type FieldKind,
|
||||
flatDef,
|
||||
type PropertyDef,
|
||||
type StructFieldDef,
|
||||
} from '../../core/properties/PropertySchema';
|
||||
@@ -202,29 +203,70 @@ export function removeButton(onClick: () => void): Button {
|
||||
|
||||
export const READ_ONLY: FieldHandlers = { onInput: () => {}, onChange: () => {} };
|
||||
|
||||
/** A primitive as a property definition, so one widget set serves a list item and a struct field. */
|
||||
export function primitiveDef(key: string, kind: FieldKind, label = key): PrimitiveDef {
|
||||
switch (kind) {
|
||||
case 'number':
|
||||
return { key, label, kind: 'number', default: 0 };
|
||||
case 'text':
|
||||
return { key, label, kind: 'text', default: '' };
|
||||
case 'boolean':
|
||||
return { key, label, kind: 'boolean', default: false };
|
||||
case 'color':
|
||||
return { key, label, kind: 'color', default: 0 };
|
||||
case 'texture':
|
||||
return { key, label, kind: 'texture', default: '' };
|
||||
}
|
||||
}
|
||||
|
||||
export type PrimitiveDef = Extract<PropertyDef, { kind: FieldKind }>;
|
||||
|
||||
/** A struct field as a property definition – a list field becomes the same row a list property has. */
|
||||
export function fieldDef(field: StructFieldDef, label = field.key): PropertyDef {
|
||||
return field.kind === 'list'
|
||||
? { key: field.key, label, kind: 'list', default: [] }
|
||||
: primitiveDef(field.key, field.kind, label);
|
||||
: flatDef(field.key, field.kind, { label });
|
||||
}
|
||||
|
||||
/** What a reference kind names in the project: how an id reads, and what its picker may offer. */
|
||||
interface Reference {
|
||||
none: string;
|
||||
label(project: Project | null, id: string): string;
|
||||
choices(project: Project, def: PropertyDef): { id: string; label: string }[];
|
||||
}
|
||||
|
||||
const REFERENCES: Record<ReferenceKind, Reference> = {
|
||||
asset: {
|
||||
none: NO_ASSET_LABEL,
|
||||
label: assetRefLabel,
|
||||
choices: (project, def) =>
|
||||
assetRefChoices(project, def.kind === 'asset' ? def.assetTypes : undefined).map((asset) => ({
|
||||
id: asset.id,
|
||||
label: assetChoiceLabel(asset),
|
||||
})),
|
||||
},
|
||||
texture: { none: NO_TEXTURE_LABEL, label: textureRefLabel, choices: textureRefChoices },
|
||||
};
|
||||
|
||||
/** An id in the words of its reference kind; nothing for a kind that names nothing. */
|
||||
export function referenceLabel(def: PropertyDef, project: Project | null, id: string): string | undefined {
|
||||
return isKindWith('reference', def.kind) ? REFERENCES[def.kind].label(project, id) : undefined;
|
||||
}
|
||||
|
||||
function referenceField(
|
||||
ctx: FieldContext,
|
||||
def: PropertyDef,
|
||||
ref: Reference,
|
||||
value: PropertyValue,
|
||||
handlers: FieldHandlers,
|
||||
): Field {
|
||||
// The current id is kept here rather than read back off the label: the label is a name, two
|
||||
// assets may share one (a frame's label repeats its sheet's), and a dangling reference has none.
|
||||
let current = textOf(value);
|
||||
const button = new Dropdown(ref.label(ctx.project(), current));
|
||||
button.onClick = () => {
|
||||
const mark = (id: string, text: string) => (id === current ? `● ${text}` : text);
|
||||
const items: MenuItem[] = [
|
||||
{ label: mark('', ref.none), onSelect: () => handlers.onChange('') },
|
||||
// Built on click, not once per row: assets are created, renamed and deleted while the
|
||||
// inspector stands. Searchable: one sliced sheet alone contributes an entry per frame.
|
||||
...ref.choices(ctx.project(), def).map((choice) => ({
|
||||
label: mark(choice.id, choice.label),
|
||||
onSelect: () => handlers.onChange(choice.id),
|
||||
})),
|
||||
];
|
||||
new Menu(items, { searchable: true }).openBelow(button);
|
||||
};
|
||||
return {
|
||||
widget: button,
|
||||
grow: true,
|
||||
setValue: (v) => {
|
||||
current = textOf(v);
|
||||
button.setText(ref.label(ctx.project(), current));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** One field out of a bundle; a field the value does not carry reads as the empty one of its kind. */
|
||||
@@ -308,60 +350,9 @@ export function createField(
|
||||
const button = new KeyCaptureButton(textOf(value), (key) => handlers.onChange(key));
|
||||
return { widget: button, grow: true, setValue: (v) => button.setKey(textOf(v)) };
|
||||
}
|
||||
case 'asset': {
|
||||
// The current id is kept here rather than read back off the label: the label is a name,
|
||||
// two assets may share one, and a dangling reference has no name at all.
|
||||
let current = textOf(value);
|
||||
const button = new Dropdown(assetRefLabel(ctx.project(), current));
|
||||
button.onClick = () => {
|
||||
const mark = (id: string, text: string) => (id === current ? `● ${text}` : text);
|
||||
const items: MenuItem[] = [
|
||||
{ label: mark('', NO_ASSET_LABEL), onSelect: () => handlers.onChange('') },
|
||||
// Built on click, not once per row: assets are created, renamed and deleted while
|
||||
// the inspector stands.
|
||||
...assetRefChoices(ctx.project(), def.assetTypes).map((asset) => ({
|
||||
label: mark(asset.id, assetChoiceLabel(asset)),
|
||||
onSelect: () => handlers.onChange(asset.id),
|
||||
})),
|
||||
];
|
||||
// Searchable: the list grows with the project, like the parent picker.
|
||||
new Menu(items, { searchable: true }).openBelow(button);
|
||||
};
|
||||
return {
|
||||
widget: button,
|
||||
grow: true,
|
||||
setValue: (v) => {
|
||||
current = textOf(v);
|
||||
button.setText(assetRefLabel(ctx.project(), current));
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'texture': {
|
||||
// Kept for the same reason as in the asset case, and more urgently: a frame's label
|
||||
// repeats its sheet's name, so several entries can read identically.
|
||||
let current = textOf(value);
|
||||
const button = new Dropdown(textureRefLabel(ctx.project(), current));
|
||||
button.onClick = () => {
|
||||
const mark = (id: string, text: string) => (id === current ? `● ${text}` : text);
|
||||
const items: MenuItem[] = [
|
||||
{ label: mark('', NO_TEXTURE_LABEL), onSelect: () => handlers.onChange('') },
|
||||
...textureRefChoices(ctx.project()).map((choice) => ({
|
||||
label: mark(choice.id, choice.label),
|
||||
onSelect: () => handlers.onChange(choice.id),
|
||||
})),
|
||||
];
|
||||
// Searchable: one sliced sheet alone contributes an entry per frame.
|
||||
new Menu(items, { searchable: true }).openBelow(button);
|
||||
};
|
||||
return {
|
||||
widget: button,
|
||||
grow: true,
|
||||
setValue: (v) => {
|
||||
current = textOf(v);
|
||||
button.setText(textureRefLabel(ctx.project(), current));
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'asset':
|
||||
case 'texture':
|
||||
return referenceField(ctx, def, REFERENCES[def.kind], value, handlers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user