Put one item into a list, a struct staying whole, with Add item (#250)
A list could only grow through Join lists, and that takes its b apart: a struct becomes one item per field and a list is joined. A list of structs such as loot could not grow at all. - Add item returns a new list with value as exactly one more item. A struct or a list goes in whole. The list that came in is left alone, like with Set item. - index -1 or unset appends. Otherwise the item goes in before that index. An index outside the list appends, so the value is not lost. - The rule that an item takes the kind of its slot moved out of Set item into fitToSlot. Both nodes use it, Set item behaves as before. - No e2e check and no browser look. The node is covered by unit tests. Tests: - ./check.sh: 180 files, 2634 tests, CHECK EXIT 0 - 7 catalog cases, 4 cases in list-of-structs.test.ts, and index in the whole-number inputs of texture-kind.test.ts - Counter-check: taking the value apart like Join lists fails 3 tests - This commit alone: tsc and the three touched unit files pass (337 tests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb8a2d029a
commit
b90b62886b
6 files changed
+102
-5
No files matched your search
@@ -0,0 +1,38 @@
|
|||||||
|
import type { PropertyValue } from '../../../model/Properties';
|
||||||
|
import { itemKindOf } from '../../../properties/listItems';
|
||||||
|
import type { NodeDef } from '../../NodeRegistry';
|
||||||
|
import { asList, asNumber, asValueItem, type ValueList } from '../../values';
|
||||||
|
import { any, int, list } from '../shared';
|
||||||
|
import { 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.',
|
||||||
|
inputs: [list('list'), any('value'), int('index', -1)],
|
||||||
|
outputs: [list('result')],
|
||||||
|
run: (_ctx, inputs) => {
|
||||||
|
const items = asList(inputs.list);
|
||||||
|
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;
|
||||||
|
const next = [...items];
|
||||||
|
next.splice(at, 0, fitted(items, asValueItem(inputs.value)));
|
||||||
|
return { outputs: { result: next } };
|
||||||
|
},
|
||||||
|
params: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list whose items are all one kind gets the new one in that kind too, so a list of pictures
|
||||||
|
* stays one. A mixed or empty list has no kind to keep, and the value goes in as it is.
|
||||||
|
*/
|
||||||
|
function fitted(items: ValueList, value: ValueList[number]): ValueList[number] {
|
||||||
|
const first = items[0];
|
||||||
|
if (first === undefined) return value;
|
||||||
|
const kind = itemKindOf(first as PropertyValue);
|
||||||
|
const oneKind = items.every((item) => itemKindOf(item as PropertyValue) === kind);
|
||||||
|
return oneKind ? fitToSlot(first, value) : value;
|
||||||
|
}
|
||||||
@@ -18,16 +18,19 @@ export const SetItem: NodeDef = {
|
|||||||
const next = [...items];
|
const next = [...items];
|
||||||
// The slot keeps its kind: writing a number into a color item leaves a color behind, the same
|
// 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`).
|
// way writing into a number variable leaves a number (see `asTypeOf`).
|
||||||
const value = asValueItem(inputs.value);
|
next[i] = fitToSlot(items[i], asValueItem(inputs.value));
|
||||||
const slot = items[i];
|
|
||||||
// An object has no kind to keep and none to take on, so it is written as it is (#157).
|
|
||||||
if (isEntity(value) || isEntity(slot)) next[i] = value;
|
|
||||||
else next[i] = nestedKind(slot) ? sameNested(slot, value) : itemLike(slot, value);
|
|
||||||
return { outputs: { result: next } };
|
return { outputs: { result: next } };
|
||||||
},
|
},
|
||||||
params: [],
|
params: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** `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).
|
||||||
|
if (isEntity(value) || isEntity(slot)) return value;
|
||||||
|
return nestedKind(slot) ? sameNested(slot, value) : itemLike(slot, value);
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether a slot holds a list or a struct, which may carry objects `itemLike` would not keep. */
|
/** Whether a slot holds a list or a struct, which may carry objects `itemLike` would not keep. */
|
||||||
const nestedKind = (slot: ValueList[number] | undefined): boolean => {
|
const nestedKind = (slot: ValueList[number] | undefined): boolean => {
|
||||||
const kind = itemKindOf(slot as PropertyValue);
|
const kind = itemKindOf(slot as PropertyValue);
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import { Subtract } from './data/Subtract';
|
|||||||
import { Time } from './data/Time';
|
import { Time } from './data/Time';
|
||||||
import { ToNumber } from './data/ToNumber';
|
import { ToNumber } from './data/ToNumber';
|
||||||
import { ToText } from './data/ToText';
|
import { ToText } from './data/ToText';
|
||||||
|
import { AddItem } from './data/AddItem';
|
||||||
import { Concat } from './data/Concat';
|
import { Concat } from './data/Concat';
|
||||||
import { Count } from './data/Count';
|
import { Count } from './data/Count';
|
||||||
import { CurrentState } from './data/CurrentState';
|
import { CurrentState } from './data/CurrentState';
|
||||||
@@ -213,6 +214,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
|||||||
Count,
|
Count,
|
||||||
ItemAt,
|
ItemAt,
|
||||||
SetItem,
|
SetItem,
|
||||||
|
AddItem,
|
||||||
SubList,
|
SubList,
|
||||||
Concat,
|
Concat,
|
||||||
IndexOf,
|
IndexOf,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { AddItem } from '../../src/core/graph/nodes/data/AddItem';
|
||||||
import { Fill } from '../../src/core/graph/nodes/data/Fill';
|
import { Fill } from '../../src/core/graph/nodes/data/Fill';
|
||||||
import { IndexOf } from '../../src/core/graph/nodes/data/IndexOf';
|
import { IndexOf } from '../../src/core/graph/nodes/data/IndexOf';
|
||||||
import { ItemAt } from '../../src/core/graph/nodes/data/ItemAt';
|
import { ItemAt } from '../../src/core/graph/nodes/data/ItemAt';
|
||||||
@@ -130,6 +131,12 @@ describe('the list nodes with tagged items', () => {
|
|||||||
result: [tagged('texture', 'b')],
|
result: [tagged('texture', 'b')],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds a plain id to a list of pictures as a picture', () => {
|
||||||
|
expect(run(AddItem, { list: [tagged('texture', 'a')], value: 'b', index: -1 })).toEqual({
|
||||||
|
result: [tagged('texture', 'a'), tagged('texture', 'b')],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the list nodes with struct items', () => {
|
describe('the list nodes with struct items', () => {
|
||||||
@@ -148,6 +155,23 @@ describe('the list nodes with struct items', () => {
|
|||||||
expect(run(IndexOf, { list: cells([1, 2]), value: { x: 9, y: 9 } })).toEqual({ index: -1 });
|
expect(run(IndexOf, { list: cells([1, 2]), value: { x: 9, y: 9 } })).toEqual({ index: -1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** `Join lists` would take the struct apart into its field values. */
|
||||||
|
it('adds a struct as one whole item', () => {
|
||||||
|
expect(run(AddItem, { list: cells([1, 2]), value: { x: 3, y: 4 }, index: -1 })).toEqual({
|
||||||
|
result: cells([1, 2], [3, 4]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a struct to an empty list as one whole item', () => {
|
||||||
|
expect(run(AddItem, { list: [], value: { x: 3, y: 4 }, index: -1 })).toEqual({ result: cells([3, 4]) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves a number a number beside structs, since a mixed list has no kind to keep', () => {
|
||||||
|
expect(run(AddItem, { list: [7, { x: 1, y: 2 }], value: 8, index: -1 })).toEqual({
|
||||||
|
result: [7, { x: 1, y: 2 }, 8],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('fills a list with a struct', () => {
|
it('fills a list with a struct', () => {
|
||||||
expect(run(Fill, { value: { x: 1, y: 2 }, count: 2 })).toEqual({ result: cells([1, 2], [1, 2]) });
|
expect(run(Fill, { value: { x: 1, y: 2 }, count: 2 })).toEqual({ result: cells([1, 2], [1, 2]) });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -728,6 +728,35 @@ const CASES: Record<string, Case[]> = {
|
|||||||
outputs: { result: [1, 2] },
|
outputs: { result: [1, 2] },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
AddItem: [
|
||||||
|
{ name: 'appends by default', inputs: { list: [1, 2], value: 3 }, outputs: { result: [1, 2, 3] } },
|
||||||
|
{
|
||||||
|
name: 'inserts before index',
|
||||||
|
inputs: { list: [1, 2, 3], value: 9, index: 1 },
|
||||||
|
outputs: { result: [1, 9, 2, 3] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'index equal to the length appends',
|
||||||
|
inputs: { list: [1, 2], value: 9, index: 2 },
|
||||||
|
outputs: { result: [1, 2, 9] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'an index outside the list appends instead of dropping the value',
|
||||||
|
inputs: { list: [1, 2], value: 9, index: 7 },
|
||||||
|
outputs: { result: [1, 2, 9] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'leaves the list it was given untouched – a graph may hold it elsewhere',
|
||||||
|
inputs: { list: Object.freeze([1, 2]), value: 3 },
|
||||||
|
outputs: { result: [1, 2, 3] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a list goes in as one item, not joined',
|
||||||
|
inputs: { list: [[1]], value: [2, 3] },
|
||||||
|
outputs: { result: [[1], [2, 3]] },
|
||||||
|
},
|
||||||
|
{ name: 'an unset list starts empty', inputs: { value: 5 }, outputs: { result: [5] } },
|
||||||
|
],
|
||||||
SubList: [
|
SubList: [
|
||||||
{
|
{
|
||||||
name: 'takes count items from start',
|
name: 'takes count items from start',
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ describe('the whole-number inputs', () => {
|
|||||||
const whole = [
|
const whole = [
|
||||||
['ItemAt', 'index'],
|
['ItemAt', 'index'],
|
||||||
['SetItem', 'index'],
|
['SetItem', 'index'],
|
||||||
|
['AddItem', 'index'],
|
||||||
['SubList', 'start'],
|
['SubList', 'start'],
|
||||||
['SubList', 'count'],
|
['SubList', 'count'],
|
||||||
['Fill', 'count'],
|
['Fill', 'count'],
|
||||||
|
|||||||
Reference in new issue
Block a user