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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Every kind a value can be, in one table: what it is called, how it is marked, what it travels as
|
||||
* on a data edge, its empty value, how any value is read as it, and where it may be picked. A new
|
||||
* kind is a row here – the kind lists, the menus, the tags and the coercions follow from it.
|
||||
*
|
||||
* A `tagged` kind is one JSON cannot show: a color is a number, a texture id a text. In a list,
|
||||
* where nothing is declared, such an item wears its tag (`tagOf`), and a port of that kind carries
|
||||
* the field that edits it. A `reference` kind names something in the project.
|
||||
*/
|
||||
import type { PortType } from '../graph/Graph';
|
||||
import { booleanOf, colorOf, type PropertyValue, textOf } from '../model/Properties';
|
||||
|
||||
interface KindRow {
|
||||
readonly kind: string;
|
||||
readonly label: string;
|
||||
readonly mark?: string;
|
||||
readonly travels: PortType;
|
||||
readonly empty?: number | string | boolean;
|
||||
readonly coerce?: (value: unknown, fallback: never) => number | string | boolean;
|
||||
readonly tagged?: true;
|
||||
readonly reference?: true;
|
||||
readonly variable?: true;
|
||||
readonly field?: true;
|
||||
readonly port?: true;
|
||||
}
|
||||
|
||||
const numberOf = (value: unknown, fallback: number): number => {
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
};
|
||||
const text = (value: unknown): string => textOf(value as PropertyValue | undefined);
|
||||
const bool = (value: unknown): boolean => booleanOf(value);
|
||||
const color = (value: unknown, fallback: number): number => colorOf(value as PropertyValue, fallback);
|
||||
|
||||
export const KINDS = [
|
||||
{
|
||||
kind: 'number',
|
||||
label: 'Number',
|
||||
mark: '#',
|
||||
travels: 'number',
|
||||
empty: 0,
|
||||
coerce: numberOf,
|
||||
variable: true,
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
{
|
||||
kind: 'text',
|
||||
label: 'Text',
|
||||
mark: 'Aa',
|
||||
travels: 'string',
|
||||
empty: '',
|
||||
coerce: text,
|
||||
variable: true,
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
{
|
||||
kind: 'boolean',
|
||||
label: 'Boolean',
|
||||
mark: '✓',
|
||||
travels: 'boolean',
|
||||
empty: false,
|
||||
coerce: bool,
|
||||
variable: true,
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
{
|
||||
kind: 'color',
|
||||
label: 'Color',
|
||||
mark: '◧',
|
||||
travels: 'number',
|
||||
empty: 0,
|
||||
coerce: color,
|
||||
tagged: true,
|
||||
variable: true,
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
/** A texture, one of its frames or an animation – all one id, which is why it is not an `asset`. */
|
||||
{
|
||||
kind: 'texture',
|
||||
label: 'Texture',
|
||||
mark: '▦',
|
||||
travels: 'string',
|
||||
empty: '',
|
||||
coerce: text,
|
||||
tagged: true,
|
||||
reference: true,
|
||||
variable: true,
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
{ kind: 'object', label: 'Object', mark: '◇', travels: 'object', port: true },
|
||||
{ kind: 'list', label: 'List', mark: '≡', travels: 'list', variable: true, port: true },
|
||||
{ kind: 'struct', label: 'Struct', mark: '⊞', travels: 'struct', variable: true, port: true },
|
||||
// Schema-only kinds: a setting has them, nothing a user declares does.
|
||||
{ kind: 'enum', label: 'Choice', travels: 'string', empty: '', coerce: text },
|
||||
/** A keyboard key name (`KeyboardEvent.key`), captured by pressing it. */
|
||||
{ kind: 'key', label: 'Key', travels: 'string', empty: '', coerce: text },
|
||||
/** An asset id, `''` = none. Ids are not validated on load, so a reference can dangle. */
|
||||
{ kind: 'asset', label: 'Asset', travels: 'string', empty: '', coerce: text, reference: true },
|
||||
] as const satisfies readonly KindRow[];
|
||||
|
||||
type Row = (typeof KINDS)[number];
|
||||
export type Kind = Row['kind'];
|
||||
type RowOf<K extends Kind> = Extract<Row, { kind: K }>;
|
||||
type With<Role extends string> = Extract<Row, Record<Role, true>>['kind'];
|
||||
|
||||
export type VariableKind = With<'variable'>;
|
||||
export type FieldKind = With<'field'>;
|
||||
export type DeclaredPortKind = With<'port'>;
|
||||
export type TaggedKind = With<'tagged'>;
|
||||
export type ReferenceKind = With<'reference'>;
|
||||
/** The kinds whose value is one primitive – the ones with an empty value. */
|
||||
export type FlatKind = Extract<Row, { empty: unknown }>['kind'];
|
||||
/** What one item of a list may be – a primitive plus `struct`, so a bundle can sit in a list. */
|
||||
export type ListItemKind = FieldKind | 'struct';
|
||||
/** What a field of a struct holds – a primitive plus `list`. No `struct`: that bounds the depth. */
|
||||
export type StructFieldKind = FieldKind | 'list';
|
||||
|
||||
type Widen<T> = T extends number ? number : T extends string ? string : T extends boolean ? boolean : never;
|
||||
export type ValueOf<K extends FlatKind> = Widen<RowOf<K>['empty']>;
|
||||
|
||||
export const KIND = Object.fromEntries(KINDS.map((row) => [row.kind, row])) as { [K in Kind]: RowOf<K> };
|
||||
|
||||
const rowOf = (kind: string): Row | undefined => (KIND as Partial<Record<string, Row>>)[kind];
|
||||
|
||||
export function isKindWith<Role extends string>(role: Role, kind: string): kind is With<Role> {
|
||||
return (rowOf(kind) as Partial<Record<string, unknown>> | undefined)?.[role] === true;
|
||||
}
|
||||
|
||||
const kindsWith = <Role extends string>(role: Role): readonly With<Role>[] =>
|
||||
KINDS.map((row) => row.kind).filter((kind): kind is With<Role> => isKindWith(role, kind));
|
||||
|
||||
export const VARIABLE_KINDS = kindsWith('variable');
|
||||
export const FIELD_KINDS = kindsWith('field');
|
||||
export const DECLARED_PORT_KINDS = kindsWith('port');
|
||||
export const LIST_ITEM_KINDS: readonly ListItemKind[] = [...FIELD_KINDS, 'struct'];
|
||||
export const STRUCT_FIELD_KINDS: readonly StructFieldKind[] = [...FIELD_KINDS, 'list'];
|
||||
|
||||
export const isTaggedKind = (kind: string): kind is TaggedKind => isKindWith('tagged', kind);
|
||||
|
||||
export function kindLabel(kind: string): string {
|
||||
return rowOf(kind)?.label ?? kind;
|
||||
}
|
||||
|
||||
export function kindMark(kind: string): string {
|
||||
const row = rowOf(kind);
|
||||
return row && 'mark' in row ? row.mark : '?';
|
||||
}
|
||||
|
||||
/** A flat row as its kind's value type – what the table promises and a generic `K` cannot see. */
|
||||
const flatRow = <K extends FlatKind>(kind: K) =>
|
||||
KIND[kind] as unknown as {
|
||||
empty: ValueOf<K>;
|
||||
coerce: (value: unknown, fallback: ValueOf<K>) => ValueOf<K>;
|
||||
};
|
||||
|
||||
export function emptyOf<K extends FlatKind>(kind: K): ValueOf<K> {
|
||||
return flatRow(kind).empty;
|
||||
}
|
||||
|
||||
/** `value` read as `kind`, `fallback` where it cannot be. */
|
||||
export function coerceAs<K extends FlatKind>(value: unknown, kind: K, fallback: ValueOf<K>): ValueOf<K> {
|
||||
return flatRow(kind).coerce(value, fallback);
|
||||
}
|
||||
|
||||
export function coerceField<K extends FlatKind>(value: unknown, kind: K): ValueOf<K> {
|
||||
return coerceAs(value, kind, emptyOf(kind));
|
||||
}
|
||||
|
||||
/** The kind a bare value travelling as `type` reads as: the untagged kind of that port type. */
|
||||
export function plainKindOf(type: string): VariableKind | undefined {
|
||||
return VARIABLE_KINDS.find((kind) => !isTaggedKind(kind) && KIND[kind].travels === type);
|
||||
}
|
||||
@@ -1,45 +1,44 @@
|
||||
/**
|
||||
* A list declares nothing about what it holds: every item carries its own kind, read back off the
|
||||
* value. JSON already says what a number, a text, a boolean and a struct are, so only a color and a
|
||||
* texture need a tag (`COLOR_TAG`, `TEXTURE_TAG`) – which keeps every list ever stored readable.
|
||||
* value. JSON says what a number, a text, a boolean and a struct are; a tagged kind says so itself
|
||||
* (`tagOf`), which keeps every list ever stored readable.
|
||||
*
|
||||
* Depth is still capped the way the declarations used to cap it: a struct item holds flat items,
|
||||
* and so does a list inside a struct field.
|
||||
* Depth is capped the way the declarations used to cap it: a struct item holds flat items, and so
|
||||
* does a list inside a struct field.
|
||||
*/
|
||||
import {
|
||||
booleanOf,
|
||||
type ColorItem,
|
||||
colorItem,
|
||||
colorItemValue,
|
||||
colorOf,
|
||||
type FieldValue,
|
||||
isColorItem,
|
||||
isList,
|
||||
isStruct,
|
||||
isTextureItem,
|
||||
type PropertyValue,
|
||||
type StructValue,
|
||||
textOf,
|
||||
type TextureItem,
|
||||
textureItem,
|
||||
textureItemValue,
|
||||
type TaggedItem,
|
||||
tagged,
|
||||
tagOf,
|
||||
untag,
|
||||
} from '../model/Properties';
|
||||
import type { FieldKind, ListItemKind } from './PropertySchema';
|
||||
import {
|
||||
coerceField,
|
||||
type FieldKind,
|
||||
isKindWith,
|
||||
isTaggedKind,
|
||||
type ListItemKind,
|
||||
plainKindOf,
|
||||
} from './kinds';
|
||||
|
||||
/** What a list holds. Identical to `ListValue[number]`, named for the places that mean one item. */
|
||||
export type ListItem = number | string | boolean | StructValue;
|
||||
|
||||
/** What a flat position holds: no struct except a tagged color or texture, so the depth stays capped. */
|
||||
export type FlatItem = number | string | boolean | ColorItem | TextureItem;
|
||||
/** What a flat position holds: no struct except a tagged item, so the depth stays capped. */
|
||||
export type FlatItem = number | string | boolean | TaggedItem;
|
||||
|
||||
/** The kind an item is, taken from the item itself – there is nowhere else it could come from. */
|
||||
export function itemKindOf(item: PropertyValue | undefined): ListItemKind {
|
||||
if (isColorItem(item)) return 'color';
|
||||
if (isTextureItem(item)) return 'texture';
|
||||
const tag = tagOf(item);
|
||||
if (tag !== undefined && isTaggedKind(tag)) return tag;
|
||||
if (isStruct(item) || isList(item)) return 'struct';
|
||||
if (typeof item === 'string') return 'text';
|
||||
if (typeof item === 'boolean') return 'boolean';
|
||||
return 'number';
|
||||
const plain = plainKindOf(typeof item);
|
||||
return plain !== undefined && isKindWith('field', plain) ? plain : 'number';
|
||||
}
|
||||
|
||||
/** Flat positions hold no struct, so one reads as its text. */
|
||||
@@ -48,26 +47,9 @@ export function fieldKindOf(value: PropertyValue | undefined): FieldKind {
|
||||
return kind === 'struct' ? 'text' : kind;
|
||||
}
|
||||
|
||||
/** What the widgets and the graph see: a tag is what it wraps, everything else is itself. */
|
||||
export function itemValue(item: PropertyValue | undefined): PropertyValue | undefined {
|
||||
if (isColorItem(item)) return colorItemValue(item);
|
||||
if (isTextureItem(item)) return textureItemValue(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/** `value` as an item of `kind`, which is how a row changes what it holds. */
|
||||
export function makeItem(kind: ListItemKind, value: PropertyValue | undefined): ListItem {
|
||||
switch (kind) {
|
||||
case 'color':
|
||||
// Through `itemValue`: a tagged color handed to `colorOf` would be read as hex text.
|
||||
return colorItem(colorOf(itemValue(value) ?? 0, 0));
|
||||
case 'texture':
|
||||
return textureItem(textOf(itemValue(value)));
|
||||
case 'struct':
|
||||
return structItemOf(value);
|
||||
default:
|
||||
return coerceField(itemValue(value), kind);
|
||||
}
|
||||
return kind === 'struct' ? structItemOf(value) : makeFlatItem(kind, value);
|
||||
}
|
||||
|
||||
/** `value` as an item of the kind the slot already holds, so writing a color keeps it a color. */
|
||||
@@ -77,9 +59,8 @@ export function itemLike(current: PropertyValue | undefined, value: PropertyValu
|
||||
|
||||
/** `value` as a flat item of `kind` – a struct item's field, where a struct itself cannot go. */
|
||||
export function makeFlatItem(kind: FieldKind, value: PropertyValue | undefined): FlatItem {
|
||||
if (kind === 'color') return colorItem(colorOf(itemValue(value) ?? 0, 0));
|
||||
if (kind === 'texture') return textureItem(textOf(itemValue(value)));
|
||||
return coerceField(itemValue(value), kind);
|
||||
if (isTaggedKind(kind)) return tagged(kind, coerceField(untag(value), kind));
|
||||
return coerceField(untag(value), kind);
|
||||
}
|
||||
|
||||
/** A flat slot (a struct item's field, an item of a list in a struct field) keeps its kind too. */
|
||||
@@ -93,19 +74,17 @@ export function listItemOf(value: PropertyValue | undefined): ListItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* A flat position holds a primitive or a tagged color or texture. Anything deeper is dropped rather
|
||||
* than flattened – the same bound the declarations used to draw, and the one the store writes.
|
||||
* A flat position holds a primitive or a tagged item. Anything deeper is dropped rather than
|
||||
* flattened – the same bound the declarations used to draw, and the one the store writes.
|
||||
*/
|
||||
export function flatItemOf(value: PropertyValue | undefined): FlatItem | undefined {
|
||||
if (isColorItem(value)) return colorItem(colorItemValue(value));
|
||||
if (isTextureItem(value)) return textureItem(textureItemValue(value));
|
||||
if (isList(value) || isStruct(value)) return undefined;
|
||||
return coerceField(value, fieldKindOf(value));
|
||||
if (isList(value) || (isStruct(value) && !isTaggedKind(tagOf(value) ?? ''))) return undefined;
|
||||
return makeFlatItem(fieldKindOf(value), value);
|
||||
}
|
||||
|
||||
/** A struct item keeps the fields it carries, each as the kind its own value says. */
|
||||
export function structItemOf(value: PropertyValue | undefined): StructValue {
|
||||
if (!isStruct(value) || isColorItem(value) || isTextureItem(value)) return {};
|
||||
if (!isStruct(value) || tagOf(value) !== undefined) return {};
|
||||
const fields: Record<string, FieldValue> = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
const flat = flatItemOf(item);
|
||||
@@ -122,15 +101,6 @@ export function flatListOf(value: PropertyValue | undefined): readonly FlatItem[
|
||||
|
||||
/** The shape a struct item has right now: its keys in order, each with the kind of its value. */
|
||||
export function structItemFields(item: PropertyValue | undefined): { key: string; kind: FieldKind }[] {
|
||||
if (!isStruct(item) || isColorItem(item) || isTextureItem(item)) return [];
|
||||
if (!isStruct(item) || tagOf(item) !== undefined) return [];
|
||||
return Object.entries(item).map(([key, value]) => ({ key, kind: fieldKindOf(value) }));
|
||||
}
|
||||
|
||||
/** A primitive as one of the flat kinds. The spelling of a text counts, see `booleanOf`. */
|
||||
export function coerceField(value: PropertyValue | undefined, kind: FieldKind): number | string | boolean {
|
||||
if (kind === 'text' || kind === 'texture') return textOf(value);
|
||||
if (kind === 'boolean') return typeof value === 'boolean' ? value : booleanOf(textOf(value));
|
||||
if (kind === 'color') return colorOf(value ?? 0, 0);
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
Reference in new issue
Block a user