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
@@ -9,44 +9,40 @@ import {
|
||||
type PropertyValue,
|
||||
type StructValue,
|
||||
textOf,
|
||||
booleanOf,
|
||||
colorOf,
|
||||
} from '../model/Properties';
|
||||
import { coerceField, flatListOf, listItemOf } from './listItems';
|
||||
import {
|
||||
coerceAs,
|
||||
coerceField,
|
||||
emptyOf,
|
||||
type FieldKind,
|
||||
type FlatKind,
|
||||
type ValueOf,
|
||||
type VariableKind,
|
||||
} from './kinds';
|
||||
import { flatListOf, listItemOf } from './listItems';
|
||||
|
||||
/**
|
||||
* What a kind's definition carries beyond key, label and default. `decimals` belongs only on
|
||||
* properties that are integral by meaning (counts, sizes in whole pixels), not on geometry: dragging
|
||||
* produces fractions, and the field rounds the value away rather than hiding it. Without
|
||||
* `assetTypes` an asset picker offers every placeable asset.
|
||||
*/
|
||||
type Extras<K extends FlatKind> = K extends 'number'
|
||||
? { min?: number; max?: number; step?: number; decimals?: number }
|
||||
: K extends 'enum'
|
||||
? { options: { value: string; label: string }[] }
|
||||
: K extends 'asset'
|
||||
? { assetTypes?: readonly AssetType[] }
|
||||
: unknown;
|
||||
|
||||
/** The definition of a flat kind: its default is a value of that kind, as the table says. */
|
||||
export type FlatDef<K extends FlatKind = FlatKind> = {
|
||||
[P in K]: { key: string; label: string; kind: P; default: ValueOf<P> } & Extras<P>;
|
||||
}[K];
|
||||
|
||||
/** The inspector is generated from these entries, so a new property is one line in a schema below. */
|
||||
export type PropertyDef =
|
||||
/**
|
||||
* `decimals` belongs only on properties that are integral by meaning (counts, sizes in whole
|
||||
* pixels), not on geometry: dragging produces fractions, and the field rounds the value away
|
||||
* rather than hiding it.
|
||||
*/
|
||||
| {
|
||||
key: string;
|
||||
label: string;
|
||||
kind: 'number';
|
||||
default: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
decimals?: number;
|
||||
}
|
||||
| { key: string; label: string; kind: 'text'; default: string }
|
||||
| { key: string; label: string; kind: 'color'; default: number }
|
||||
| { key: string; label: string; kind: 'boolean'; default: boolean }
|
||||
| { key: string; label: string; kind: 'enum'; default: string; options: { value: string; label: string }[] }
|
||||
/** A keyboard key name (`KeyboardEvent.key`), captured by pressing it. */
|
||||
| { key: string; label: string; kind: 'key'; default: string }
|
||||
/**
|
||||
* An asset id (`''` = none); without `assetTypes` the picker offers every placeable asset.
|
||||
* Ids are not validated on load, so a reference can dangle and readers must cope.
|
||||
*/
|
||||
| { key: string; label: string; kind: 'asset'; default: string; assetTypes?: readonly AssetType[] }
|
||||
/**
|
||||
* The id of a texture asset or of one of its frames. Its own kind rather than an `asset`
|
||||
* reference, because a frame is not an asset and the picker has to offer both. May dangle.
|
||||
*/
|
||||
| { key: string; label: string; kind: 'texture'; default: string }
|
||||
| FlatDef
|
||||
/**
|
||||
* A list declares nothing about its items: each one carries its own kind (see `listItems.ts`),
|
||||
* and the graph types a list as a list, so one `list` port fits them all. There is no `object`
|
||||
@@ -60,18 +56,6 @@ export type PropertyDef =
|
||||
*/
|
||||
| { key: string; label: string; kind: 'struct'; default: StructValue; fields: readonly StructFieldDef[] };
|
||||
|
||||
/** The primitives. No `struct` anywhere below: that is the rule that bounds the nesting depth. */
|
||||
export const FIELD_KINDS = ['number', 'text', 'boolean', 'color', 'texture'] as const;
|
||||
export type FieldKind = (typeof FIELD_KINDS)[number];
|
||||
|
||||
/** What one item of a list may be – a primitive plus `struct`, so a bundle can sit in a list. */
|
||||
export const LIST_ITEM_KINDS = [...FIELD_KINDS, 'struct'] as const;
|
||||
export type ListItemKind = (typeof LIST_ITEM_KINDS)[number];
|
||||
|
||||
/** What a field of a struct property holds – a primitive plus `list`. */
|
||||
export const STRUCT_FIELD_KINDS = [...FIELD_KINDS, 'list'] as const;
|
||||
export type StructFieldKind = (typeof STRUCT_FIELD_KINDS)[number];
|
||||
|
||||
export interface PrimitiveFieldDef {
|
||||
key: string;
|
||||
kind: FieldKind;
|
||||
@@ -89,8 +73,17 @@ export type StructDef = Extract<PropertyDef, { kind: 'struct' }>;
|
||||
|
||||
export type PropertySchema = readonly PropertyDef[];
|
||||
|
||||
export const VARIABLE_KINDS = ['number', 'boolean', 'text', 'color', 'texture', 'list', 'struct'] as const;
|
||||
export type VariableKind = (typeof VARIABLE_KINDS)[number];
|
||||
/**
|
||||
* The definition of a field kind, its default read from `value` (empty when nothing is given). The
|
||||
* cast is the table's promise that `coerce` yields the kind.
|
||||
*/
|
||||
export function flatDef<K extends FieldKind>(
|
||||
key: string,
|
||||
kind: K,
|
||||
{ label = key, value }: { label?: string; value?: PropertyValue } = {},
|
||||
): FlatDef<K> {
|
||||
return { key, label, kind, default: coerceField(value, kind) } as FlatDef<K>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties every placeable asset has. A disabled object is not drawn, does not collide and its
|
||||
@@ -238,35 +231,20 @@ export function variableDef(
|
||||
fields?: readonly StructFieldDef[],
|
||||
): PropertyDef {
|
||||
switch (kind) {
|
||||
case 'number':
|
||||
return { key, label: key, kind, default: typeof defaultValue === 'number' ? defaultValue : 0 };
|
||||
case 'boolean':
|
||||
return { key, label: key, kind, default: Boolean(defaultValue ?? false) };
|
||||
case 'text':
|
||||
case 'texture':
|
||||
return { key, label: key, kind, default: defaultValue === undefined ? '' : textOf(defaultValue) };
|
||||
case 'color':
|
||||
return { key, label: key, kind, default: colorOf(defaultValue ?? 0, 0) };
|
||||
case 'list':
|
||||
return { key, label: key, kind, default: (isList(defaultValue) ? defaultValue : []).map(listItemOf) };
|
||||
case 'struct': {
|
||||
const declared = fields ?? [];
|
||||
return { key, label: key, kind, fields: declared, default: structOf(declared, defaultValue) };
|
||||
}
|
||||
default:
|
||||
return flatDef(key, kind, { value: defaultValue });
|
||||
}
|
||||
}
|
||||
|
||||
/** The empty value of a kind, so a field that is not there yet still has one. */
|
||||
const EMPTY_FIELD: Record<FieldKind, number | string | boolean> = {
|
||||
number: 0,
|
||||
text: '',
|
||||
boolean: false,
|
||||
color: 0,
|
||||
texture: '',
|
||||
};
|
||||
|
||||
export function emptyFieldValue(field: StructFieldDef): FieldValue {
|
||||
return field.kind === 'list' ? [] : EMPTY_FIELD[field.kind];
|
||||
return field.kind === 'list' ? [] : emptyOf(field.kind);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,21 +263,11 @@ export function structOf(fields: readonly StructFieldDef[], from: PropertyValue
|
||||
export function coerceValue(def: PropertyDef, value: PropertyValue): PropertyValue {
|
||||
switch (def.kind) {
|
||||
case 'number': {
|
||||
let n = typeof value === 'number' ? value : Number(value);
|
||||
if (!Number.isFinite(n)) n = def.default;
|
||||
let n = coerceAs(value, def.kind, def.default);
|
||||
if (def.min !== undefined) n = Math.max(def.min, n);
|
||||
if (def.max !== undefined) n = Math.min(def.max, n);
|
||||
return n;
|
||||
}
|
||||
case 'color':
|
||||
return colorOf(value, def.default);
|
||||
case 'boolean':
|
||||
return booleanOf(value);
|
||||
case 'text':
|
||||
case 'key':
|
||||
case 'asset':
|
||||
case 'texture':
|
||||
return textOf(value);
|
||||
case 'enum': {
|
||||
const s = textOf(value);
|
||||
return def.options.some((o) => o.value === s) ? s : def.default;
|
||||
@@ -309,6 +277,8 @@ export function coerceValue(def: PropertyDef, value: PropertyValue): PropertyVal
|
||||
return isList(value) ? value.map(listItemOf) : def.default;
|
||||
case 'struct':
|
||||
return structOf(def.fields, value);
|
||||
default:
|
||||
return coerceAs(value, def.kind, def.default);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user