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

+2 -2
View File
@@ -1,6 +1,6 @@
import type { NodeDef } from '../../NodeRegistry';
import { asListItem, asNumber } from '../../values';
import { any, list, num, withDefault } from '../shared';
import { any, int, list } from '../shared';
/** Same order of magnitude as the interpreter's step limit – a typo in `count` must not eat memory. */
const MAX_ITEMS = 100_000;
@@ -10,7 +10,7 @@ export const Fill: NodeDef = {
label: 'Fill',
category: 'data',
description: 'A new list of the same value repeated – how an empty board is made.',
inputs: [any('value'), withDefault(num('count'), 1)],
inputs: [any('value'), int('count', 1)],
outputs: [list('result')],
params: [],
run: (_ctx, inputs) => {
+2 -2
View File
@@ -1,14 +1,14 @@
import { itemValue } from '../../../properties/listItems';
import type { NodeDef } from '../../NodeRegistry';
import { asList, asNumber } from '../../values';
import { any, list, num } from '../shared';
import { any, int, list } from '../shared';
export const ItemAt: NodeDef = {
type: 'ItemAt',
label: 'Item at',
category: 'data',
description: 'The item at an index, counting from 0. Out of range gives 0.',
inputs: [list('list'), num('index')],
inputs: [list('list'), int('index')],
outputs: [any('item')],
// A color item reads as its number: the graph has no color of its own (see `portKindOf`).
run: (_ctx, inputs) => {
+2 -2
View File
@@ -1,14 +1,14 @@
import { itemLike } from '../../../properties/listItems';
import type { NodeDef } from '../../NodeRegistry';
import { asList, asListItem, asNumber } from '../../values';
import { any, list, num } from '../shared';
import { any, int, list } from '../shared';
export const SetItem: NodeDef = {
type: 'SetItem',
label: 'Set item',
category: 'data',
description: 'The same list with one item replaced. The list that came in is left alone.',
inputs: [list('list'), num('index'), any('value')],
inputs: [list('list'), int('index'), any('value')],
outputs: [list('result')],
run: (_ctx, inputs) => {
const items = asList(inputs.list);
+1 -1
View File
@@ -4,7 +4,7 @@ import { str } from '../shared';
export const StringLiteral: NodeDef = {
type: 'String',
label: 'String',
label: 'Text',
category: 'data',
description: 'A constant text.',
inputs: [],
+2 -2
View File
@@ -1,13 +1,13 @@
import type { NodeDef } from '../../NodeRegistry';
import { asList, asNumber } from '../../values';
import { list, num, withDefault } from '../shared';
import { int, list } from '../shared';
export const SubList: NodeDef = {
type: 'SubList',
label: 'Sub list',
category: 'data',
description: 'A part of a list: "count" items starting at "start".',
inputs: [list('list'), num('start'), withDefault(num('count'), 1)],
inputs: [list('list'), int('start'), int('count', 1)],
outputs: [list('result')],
params: [],
run: (_ctx, inputs) => {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NodeDef } from '../../NodeRegistry';
import { asNumber, asString } from '../../values';
import { num, str, withDefault } from '../shared';
import { int, str } from '../shared';
export const Substring: NodeDef = {
type: 'Substring',
@@ -9,7 +9,7 @@ export const Substring: NodeDef = {
description:
'A part of a text: "count" characters starting at "start" (the first is 0). ' +
'A negative count reaches to the end.',
inputs: [str('text'), num('start'), withDefault(num('count'), -1)],
inputs: [str('text'), int('start'), int('count', -1)],
outputs: [str('text')],
params: [],
run: (_ctx, inputs) => {
+15
View File
@@ -0,0 +1,15 @@
import type { NodeDef } from '../../NodeRegistry';
import { asString } from '../../values';
import { str } from '../shared';
/** The picture is its id on the edge: a whole texture, one frame of a sheet or an animation. */
export const TextureLiteral: NodeDef = {
type: 'Texture',
label: 'Texture',
category: 'data',
description: 'A constant picture: a texture, one of its frames or an animation, as a sprite wears it.',
inputs: [],
outputs: [str('value')],
params: [{ key: 'value', label: 'Value', kind: 'texture', default: '' }],
run: (_ctx, _inputs, params) => ({ outputs: { value: asString(params.value) } }),
};
+2 -2
View File
@@ -1,13 +1,13 @@
import type { NodeDef } from '../../NodeRegistry';
import { asNumber } from '../../values';
import { any, num, str } from '../shared';
import { any, int, str } from '../shared';
export const ToText: NodeDef = {
type: 'ToText',
label: 'To text',
category: 'data',
description: 'Turns a number into text with a fixed number of decimals.',
inputs: [any('value'), num('decimals')],
inputs: [any('value'), int('decimals')],
outputs: [str('text')],
params: [],
run: (_ctx, inputs) => {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { NodeDef } from '../../NodeRegistry';
import { asNumber } from '../../values';
import { exec, num, withDefault } from '../shared';
import { exec, int, num } from '../shared';
export const Repeat: NodeDef = {
type: 'Repeat',
@@ -9,7 +9,7 @@ export const Repeat: NodeDef = {
description:
'Runs "body" a number of times, counting "index" up from 0, and continues at "done" afterwards. ' +
'Every round reads its data inputs anew, so an expression built on "index" changes with it.',
inputs: [exec(), withDefault(num('times'), 1)],
inputs: [exec(), int('times', 1)],
outputs: [exec('body', 'body'), num('index'), exec('done', 'done')],
params: [],
run: (_ctx, inputs) => ({
+2
View File
@@ -50,6 +50,7 @@ import { Sqrt } from './data/Sqrt';
import { StringLiteral } from './data/String';
import { Substring } from './data/Substring';
import { Subtract } from './data/Subtract';
import { TextureLiteral } from './data/Texture';
import { Time } from './data/Time';
import { ToNumber } from './data/ToNumber';
import { ToText } from './data/ToText';
@@ -121,6 +122,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
NumberLiteral,
BooleanLiteral,
StringLiteral,
TextureLiteral,
Add,
Subtract,
Multiply,
+9
View File
@@ -14,6 +14,15 @@ export const any = (key: string, label = key): PortDef => ({ key, label, type: '
export const withDefault = (port: PortDef, value: PropertyValue): PortDef => ({ ...port, default: value });
/** A whole-number input (an index, a count): its field shows no decimals, the node floors it anyway. */
export const int = (key: string, value = 0): PortDef => ({
key,
label: key,
type: 'number',
default: value,
field: { key, label: key, kind: 'number', default: value, step: 1, decimals: 0 },
});
/** Outputs of the three collision events: the other body, its collider and the own collider. */
export const collisionOutputs = (): PortDef[] => [
exec(),