Make texture and colour full kinds and drop the load-time migrations

- `Texture` and `Color` can be picked as a variable kind, a struct field
  kind and a declared port kind; a new `Texture` node hands a picture id
  into the graph. Declared, a texture is its bare id – the `$texture` tag
  stays a list-only thing.
- The `String` node is labelled "Text" like its kind; the type id stays.
- Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`,
  `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts).
- All server projects are reseeded, so `legacyNodes.ts`,
  `migrateDeclaredLists` and the `ownerId` fallback go, with their tests.
- Note `reseed-because-of-list` moved to done/; new note `refactor-types`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-12 14:06:14 +02:00
1 parent 11c900c998
commit 8a7185948d
39 files changed
+378 -765

No files matched your search

+7 -3
View File
@@ -61,11 +61,11 @@ 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'] as const;
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, 'texture', 'struct'] as const;
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`. */
@@ -89,7 +89,7 @@ export type StructDef = Extract<PropertyDef, { kind: 'struct' }>;
export type PropertySchema = readonly PropertyDef[];
export const VARIABLE_KINDS = ['number', 'boolean', 'text', 'list', 'struct'] as const;
export const VARIABLE_KINDS = ['number', 'boolean', 'text', 'color', 'texture', 'list', 'struct'] as const;
export type VariableKind = (typeof VARIABLE_KINDS)[number];
/**
@@ -243,7 +243,10 @@ export function variableDef(
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': {
@@ -259,6 +262,7 @@ const EMPTY_FIELD: Record<FieldKind, number | string | boolean> = {
text: '',
boolean: false,
color: 0,
texture: '',
};
export function emptyFieldValue(field: StructFieldDef): FieldValue {
+14 -14
View File
@@ -4,8 +4,7 @@
* texture need a tag (`COLOR_TAG`, `TEXTURE_TAG`) – 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. A texture is a list item only; in a flat position it
* keeps its id as text rather than vanishing.
* and so does a list inside a struct field.
*/
import {
booleanOf,
@@ -21,6 +20,7 @@ import {
type PropertyValue,
type StructValue,
textOf,
type TextureItem,
textureItem,
textureItemValue,
} from '../model/Properties';
@@ -29,8 +29,8 @@ import type { FieldKind, ListItemKind } from './PropertySchema';
/** 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, so the depth stays capped. */
export type FlatItem = number | string | boolean | ColorItem;
/** 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;
/** 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 {
@@ -42,10 +42,10 @@ export function itemKindOf(item: PropertyValue | undefined): ListItemKind {
return 'number';
}
/** Flat positions hold no struct and no texture, so either of them reads as its text. */
/** Flat positions hold no struct, so one reads as its text. */
export function fieldKindOf(value: PropertyValue | undefined): FieldKind {
const kind = itemKindOf(value);
return kind === 'struct' || kind === 'texture' ? 'text' : kind;
return kind === 'struct' ? 'text' : kind;
}
/** What the widgets and the graph see: a tag is what it wraps, everything else is itself. */
@@ -77,9 +77,9 @@ 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 {
return kind === 'color'
? colorItem(colorOf(itemValue(value) ?? 0, 0))
: coerceField(itemValue(value), kind);
if (kind === 'color') return colorItem(colorOf(itemValue(value) ?? 0, 0));
if (kind === 'texture') return textureItem(textOf(itemValue(value)));
return coerceField(itemValue(value), kind);
}
/** A flat slot (a struct item's field, an item of a list in a struct field) keeps its kind too. */
@@ -93,12 +93,12 @@ export function listItemOf(value: PropertyValue | undefined): ListItem {
}
/**
* A flat position holds a primitive or a tagged color. 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 color or texture. 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 textureItemValue(value);
if (isTextureItem(value)) return textureItem(textureItemValue(value));
if (isList(value) || isStruct(value)) return undefined;
return coerceField(value, fieldKindOf(value));
}
@@ -126,9 +126,9 @@ export function structItemFields(item: PropertyValue | undefined): { key: string
return Object.entries(item).map(([key, value]) => ({ key, kind: fieldKindOf(value) }));
}
/** A primitive as one of the four kinds. The spelling of a text counts, see `booleanOf`. */
/** 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') return textOf(value);
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);