Give a list one struct type so every item keeps its shape (#254)
A list of structs used to let every item carry its own fields. Adding an item meant typing the same fields again, and nothing kept the items in step with the struct asset from #253. A list can now name a struct type for its items, and the asset keeps them in shape. - A list variable and a list field get the row Items with Mixed, every struct asset and Make struct type. Make struct type collects every field any item has, so no item loses a field. - A typed item carries its type under the key `$struct` (ITEM_TYPE_KEY). listItems does not count it as a field. - A list that holds anything but structs refuses a type, also when the offending item sits in an override. The console names the list and the count, and the list stays as it was. - In a typed list `+ Item` adds an item with the defaults, without the kind menu. Field names are locked, values stay editable. - The rows of a typed item take their kind from the struct asset (declaredKind in InlineList.ts). An asset field shows the asset picker instead of a text field, as reported in the issue. - A struct item in a mixed list gets its own Type row. Switching a list back to Mixed keeps the type on each item. - syncStructTypes brings marked items into shape and carries renames into them, also inside overrides and nested lists. One undo step. - The delete dialog of a struct asset also counts typed lists. - The game never sees the key. World and sharedState strip it with withoutItemTypes and bagWithoutItemTypes, so a struct stays a plain object at runtime. - Add item and Set item get the setting Type. With a type they only accept a struct with exactly its field names (fitsType in SetItem.ts). Anything else is left out and logged. - The item indent from #252 is back to its old width, the narrow one squeezed the value fields. Not done: writing a variable does not check the type, and the List node offers no type. Found on the way: a test helper in list-of-structs.test.ts called nodes without params. It now passes the same arguments as the interpreter. Tests: - ./check.sh: 183 files, 2679 tests, CHECK EXIT 0 - e2e GM_STEPS=1,21,22,32,54,55: ALL PASSED, 150 checks - e2e GM_STEPS=1-6,21,22,32,54,55 before the follow-up: 404 checks - New: tests/core/typed-lists.test.ts, e2e module 55 with 13 checks - Counter-checks bite in unit and browser, originals restored by cmp Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
61bbb96a96
commit
b0d0868f55
25 files changed
+1446
-86
No files matched your search
@@ -37,13 +37,25 @@ export interface StateMachine {
|
||||
|
||||
/**
|
||||
* What a node may look up in the project around its graph: the behaviors it can run, the state
|
||||
* machines it can read and the curves a `Lerp` eases along (#247). One handle, so no caller can
|
||||
* resolve a node with half of it.
|
||||
* machines it can read, the curves a `Lerp` eases along (#247) and the struct types `Add item`
|
||||
* checks (#254). One handle, so no caller can resolve a node with half of it.
|
||||
*/
|
||||
export interface GraphLookup {
|
||||
behavior(assetId: string): BehaviorProgram | null;
|
||||
machine(assetId: string): StateMachine | null;
|
||||
curve(assetId: string): readonly VectorPoint[] | null;
|
||||
/** A Struct asset as a run knows it: a name and field names, nothing else travels (#254). */
|
||||
structType(assetId: string): StructShape | null;
|
||||
}
|
||||
|
||||
export const NO_LOOKUP: GraphLookup = { behavior: () => null, machine: () => null, curve: () => null };
|
||||
export interface StructShape {
|
||||
name: string;
|
||||
fields: readonly string[];
|
||||
}
|
||||
|
||||
export const NO_LOOKUP: GraphLookup = {
|
||||
behavior: () => null,
|
||||
machine: () => null,
|
||||
curve: () => null,
|
||||
structType: () => null,
|
||||
};
|
||||
@@ -1,20 +1,27 @@
|
||||
import type { PropertyValue } from '../../../model/Properties';
|
||||
import { type PropertyValue, textOf } from '../../../model/Properties';
|
||||
import { itemKindOf } from '../../../properties/listItems';
|
||||
import { STRUCT_TYPE_SETTING } from '../../nested';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asNumber, asValueItem, type ValueList } from '../../values';
|
||||
import { asList, asNumber, asString, asValueItem, type ValueList } from '../../values';
|
||||
import { any, int, list } from '../shared';
|
||||
import { fitToSlot } from './SetItem';
|
||||
import { fitsType, fitToSlot } from './SetItem';
|
||||
|
||||
export const AddItem: NodeDef = {
|
||||
type: 'AddItem',
|
||||
label: 'Add item',
|
||||
category: 'data',
|
||||
description:
|
||||
'The same list with one more item, at the end or before index. A struct or list goes in as one item. The list that came in is left alone.',
|
||||
'The same list with one more item, at the end or before index. A struct or list goes in as one item. The list that came in is left alone. With a type, only a struct with exactly its fields goes in – anything else is reported and the list stays as it was.',
|
||||
inputs: [list('list'), any('value'), int('index', -1)],
|
||||
outputs: [list('result')],
|
||||
run: (_ctx, inputs) => {
|
||||
run: (ctx, inputs, params, _def, _entry, lookup) => {
|
||||
const items = asList(inputs.list);
|
||||
const typeId = textOf(params[STRUCT_TYPE_SETTING.key] ?? '');
|
||||
const type = typeId ? lookup.structType(typeId) : null;
|
||||
if (type && !fitsType(inputs.value, type)) {
|
||||
ctx.log(`Add item: ${asString(inputs.value ?? null) || 'nothing'} is no ${type.name} – not added`);
|
||||
return { outputs: { result: items } };
|
||||
}
|
||||
const i = inputs.index === undefined ? -1 : Math.floor(asNumber(inputs.index));
|
||||
// No index, −1 or anything outside the list appends – an index can only ever miss towards the end.
|
||||
const at = i >= 0 && i <= items.length ? i : items.length;
|
||||
@@ -22,7 +29,7 @@ export const AddItem: NodeDef = {
|
||||
next.splice(at, 0, fitted(items, asValueItem(inputs.value)));
|
||||
return { outputs: { result: next } };
|
||||
},
|
||||
params: [],
|
||||
params: [STRUCT_TYPE_SETTING],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
import type { PropertyValue } from '../../../model/Properties';
|
||||
import { isStruct, type PropertyValue, tagOf, textOf } from '../../../model/Properties';
|
||||
import { itemKindOf, itemLike, makeItem } from '../../../properties/listItems';
|
||||
import type { StructShape } from '../../lookup';
|
||||
import { STRUCT_TYPE_SETTING } from '../../nested';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asNumber, asValueItem, isEntity, type ValueList } from '../../values';
|
||||
import { asList, asNumber, asString, asValueItem, isEntity, type Value, type ValueList } from '../../values';
|
||||
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.',
|
||||
description:
|
||||
'The same list with one item replaced. The list that came in is left alone. With a type, only a struct with exactly its fields goes in – anything else is reported and the list stays as it was.',
|
||||
inputs: [list('list'), int('index'), any('value')],
|
||||
outputs: [list('result')],
|
||||
run: (_ctx, inputs) => {
|
||||
run: (ctx, inputs, params, _def, _entry, lookup) => {
|
||||
const items = asList(inputs.list);
|
||||
const i = Math.floor(asNumber(inputs.index));
|
||||
if (i < 0 || i >= items.length) return { outputs: { result: items } };
|
||||
// Checked like `Add item` (#254): by the field names, the only thing a run knows a struct by.
|
||||
const typeId = textOf(params[STRUCT_TYPE_SETTING.key] ?? '');
|
||||
const type = typeId ? lookup.structType(typeId) : null;
|
||||
if (type && !fitsType(inputs.value, type)) {
|
||||
ctx.log(`Set item: ${asString(inputs.value ?? null) || 'nothing'} is no ${type.name} – not set`);
|
||||
return { outputs: { result: items } };
|
||||
}
|
||||
const next = [...items];
|
||||
// The slot keeps its kind: writing a number into a color item leaves a color behind, the same
|
||||
// way writing into a number variable leaves a number (see `asTypeOf`).
|
||||
next[i] = fitToSlot(items[i], asValueItem(inputs.value));
|
||||
return { outputs: { result: next } };
|
||||
},
|
||||
params: [],
|
||||
params: [STRUCT_TYPE_SETTING],
|
||||
};
|
||||
|
||||
/**
|
||||
* A run knows a struct only by its fields (#254): one fits a type when it has exactly the type's
|
||||
* field names – none missing, none more.
|
||||
*/
|
||||
export function fitsType(value: Value | undefined, type: StructShape): boolean {
|
||||
if (isEntity(value) || !isStruct(value) || tagOf(value) !== undefined) return false;
|
||||
const keys = Object.keys(value);
|
||||
return keys.length === type.fields.length && type.fields.every((field) => field in value);
|
||||
}
|
||||
|
||||
/** `value` as the kind `slot` holds, the way `Set item` and `Add item` write into a list. */
|
||||
export function fitToSlot(slot: ValueList[number] | undefined, value: ValueList[number]): ValueList[number] {
|
||||
// An object has no kind to keep and none to take on, so it is written as it is (#157).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropertyBag } from '../model/Properties';
|
||||
import { type PropertyBag, withoutItemTypes } from '../model/Properties';
|
||||
import type { Project } from '../model/Project';
|
||||
import { isPerScene, isShared, sharedScopeOf } from '../model/Scope';
|
||||
import type { NodeContext, SharedState } from './NodeContext';
|
||||
@@ -14,7 +14,7 @@ export function declaredVariables(project: Project): Record<string, PropertyBag>
|
||||
for (const asset of project.assets) {
|
||||
if (asset.type !== 'variables') continue;
|
||||
const bag: PropertyBag = {};
|
||||
for (const def of asset.variables) bag[def.key] = asset.props[def.key] ?? def.default;
|
||||
for (const def of asset.variables) bag[def.key] = withoutItemTypes(asset.props[def.key] ?? def.default);
|
||||
declared[asset.id] = bag;
|
||||
}
|
||||
return declared;
|
||||
|
||||
Reference in new issue
Block a user