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:
1 parent
11c900c998
commit
8a7185948d
39 files changed
+378
-765
No files matched your search
+13
-2
@@ -36,11 +36,22 @@ export interface DeclaredPort {
|
||||
write?: true;
|
||||
}
|
||||
|
||||
export const DECLARED_PORT_KINDS = ['number', 'boolean', 'text', 'object', 'list', 'struct'] as const;
|
||||
/** A color travels as a number and a texture as text; the kind says which field edits the value. */
|
||||
export const DECLARED_PORT_KINDS = [
|
||||
'number',
|
||||
'boolean',
|
||||
'text',
|
||||
'color',
|
||||
'texture',
|
||||
'object',
|
||||
'list',
|
||||
'struct',
|
||||
] as const;
|
||||
export type DeclaredPortKind = (typeof DECLARED_PORT_KINDS)[number];
|
||||
|
||||
export function declaredPortType(kind: DeclaredPortKind): PortType {
|
||||
return kind === 'text' ? 'string' : kind;
|
||||
if (kind === 'text' || kind === 'texture') return 'string';
|
||||
return kind === 'color' ? 'number' : kind;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
|
||||
@@ -75,8 +75,14 @@ export function isTerminalNode(node: GraphNode): boolean {
|
||||
return isTerminalType(node.type);
|
||||
}
|
||||
|
||||
/** A color or texture port carries the field that edits it; the graph itself sees a number or a text. */
|
||||
export function portDefOf(port: DeclaredPort): PortDef {
|
||||
return { key: port.key, label: port.key, type: declaredPortType(port.kind) };
|
||||
const def: PortDef = { key: port.key, label: port.key, type: declaredPortType(port.kind) };
|
||||
if (port.kind === 'color')
|
||||
return { ...def, field: { key: port.key, label: port.key, kind: 'color', default: 0 } };
|
||||
if (port.kind === 'texture')
|
||||
return { ...def, field: { key: port.key, label: port.key, kind: 'texture', default: '' } };
|
||||
return def;
|
||||
}
|
||||
|
||||
/** The ports the start (or end) node of `graph` declares, as seen from a node calling it. */
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) } }),
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ASSET_TYPE_LABELS } from '../model/AssetTypes';
|
||||
import { type PropertyBag, textOf } from '../model/Properties';
|
||||
import { type PropertyDef, type PropertySchema, schemaFor } from '../properties/PropertySchema';
|
||||
import type { DeclaredPort } from './Graph';
|
||||
import { type DeclaredPort, declaredPortType } from './Graph';
|
||||
import { portDefOf } from './nested';
|
||||
import { type PortDef, withInlineDefault } from './NodeRegistry';
|
||||
import { portKindOf } from './variablePorts';
|
||||
@@ -55,10 +55,15 @@ export function targetProperties(target: PropertyTarget): PropertySchema {
|
||||
return target === 'any' ? [] : schemaFor(target);
|
||||
}
|
||||
|
||||
/** Carries the def its type has under that key (default, field); under Any or at another kind, none. */
|
||||
/** A port names the property when it travels as that property does: a `color` port and a `number` port both do. */
|
||||
export function isPortOf(port: DeclaredPort, def: PropertyDef): boolean {
|
||||
return declaredPortType(portKindOf(def)) === declaredPortType(port.kind);
|
||||
}
|
||||
|
||||
/** Carries the def its type has under that key (default, field); under Any or at another type, none. */
|
||||
export function propertyPort(port: DeclaredPort, params: PropertyBag): PortDef {
|
||||
const plain = withInlineDefault(portDefOf(port));
|
||||
const field = targetProperties(propertyTargetOf(params)).find((def) => def.key === port.key);
|
||||
if (!field || portKindOf(field) !== port.kind) return plain;
|
||||
if (!field || !isPortOf(port, field)) return plain;
|
||||
return { ...plain, field, default: field.default };
|
||||
}
|
||||
@@ -2,19 +2,13 @@ import { itemKindOf, structItemFields } from '../properties/listItems';
|
||||
import type { PropertyDef, StructFieldDef } from '../properties/PropertySchema';
|
||||
import { DECLARED_PORT_KINDS, type DeclaredPort, type DeclaredPortKind, type GraphNode } from './Graph';
|
||||
|
||||
/** The port a variable or property travels on. A color is a number; an enum or a texture is text. */
|
||||
/** The port a variable or property travels on. An enum, a key or an asset is text. */
|
||||
export function portKindOf(def: PropertyDef): DeclaredPortKind {
|
||||
if (def.kind === 'color') return 'number';
|
||||
return (DECLARED_PORT_KINDS as readonly string[]).includes(def.kind)
|
||||
? (def.kind as DeclaredPortKind)
|
||||
: 'text';
|
||||
}
|
||||
|
||||
/** A color is a number in the value model, and the graph has no port kind of its own for it. */
|
||||
export function fieldPortKind(field: StructFieldDef): DeclaredPortKind {
|
||||
return field.kind === 'color' ? 'number' : field.kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundle shape of a variable, if it has one: a struct variable declares it, a list takes it from
|
||||
* the first struct item its default holds – items declare nothing, so the value is the only source.
|
||||
@@ -33,5 +27,5 @@ export function structShapeOf(def: PropertyDef): readonly StructFieldDef[] | und
|
||||
*/
|
||||
export function missingFieldPorts(node: GraphNode, fields: readonly StructFieldDef[]): DeclaredPort[] {
|
||||
const taken = new Set((node.ports ?? []).map((p) => p.key));
|
||||
return fields.filter((f) => !taken.has(f.key)).map((f) => ({ key: f.key, kind: fieldPortKind(f) }));
|
||||
return fields.filter((f) => !taken.has(f.key)).map((f) => ({ key: f.key, kind: f.kind }));
|
||||
}
|
||||
Reference in new issue
Block a user