Let a list hold structs, and read or write variables in bundles
A list item may now be a struct. The depth is bounded in the declaration
rather than by a comment: FieldKind (number|text|boolean|color) says what a
struct field holds, ListItemKind adds struct on top and says what a list
holds, so list -> struct -> primitive is the deepest storable shape and
sameValue stays finite. A struct item renders inline in the List tab, which
is why it needs no path in the slot address.
Four paths silently assumed a list item was a primitive: sameValue compared
items with ===, textOf would have printed [object Object], asListItem turned
a struct into text on Set item and Fill, and Index of used indexOf, which is
reference equality and could never have found a struct.
Tetris now runs on bundles. shapes is 112 {x, y} structs instead of 224 loose
numbers, piece and the candidate are structs, so Next move hands back a whole
candidate and each strand packs only the field it changes, since mergeStruct
leaves the rest standing. Declared behavior ports drop 49 -> 35, the master's
variables 18 -> 13, its graph 84/94 -> 69/76 nodes/edges.
Get variables and Set variables declare their ports from a list of variable
names, the way Pack and Expand already do, so one node can read or write
several at once. Renaming a variable rewires them, because there the port key
is the variable name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d62dfea7b
commit
f60358c951
32 files changed
+1083
-253
No files matched your search
+27
-17
@@ -13,7 +13,7 @@ import {
|
||||
import type { Asset, TextureFrame } from '../model/Asset';
|
||||
import { isAssetType } from '../model/AssetTypes';
|
||||
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
|
||||
import type { FieldValue, PropertyBag, PropertyValue } from '../model/Properties';
|
||||
import type { FieldValue, PropertyBag, PropertyValue, StructValue } from '../model/Properties';
|
||||
import { PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { isTextureFile, normalizeFrameRect } from '../model/texture';
|
||||
@@ -21,6 +21,8 @@ import { hasParentCycle } from '../model/walk';
|
||||
import {
|
||||
coerceValue,
|
||||
COMMON_PROPS,
|
||||
FIELD_KINDS,
|
||||
type FieldKind,
|
||||
isCommonProp,
|
||||
LIST_ITEM_KINDS,
|
||||
type ListItemKind,
|
||||
@@ -192,9 +194,9 @@ function normalizeStructFields(raw: unknown): StructFieldDef[] {
|
||||
const f = asRecord(item);
|
||||
const key = typeof f.key === 'string' ? f.key : '';
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || seen.has(key)) continue;
|
||||
if (!(LIST_ITEM_KINDS as readonly string[]).includes(String(f.kind))) continue;
|
||||
if (!(FIELD_KINDS as readonly string[]).includes(String(f.kind))) continue;
|
||||
seen.add(key);
|
||||
fields.push({ key, kind: f.kind as ListItemKind });
|
||||
fields.push({ key, kind: f.kind as FieldKind });
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
@@ -317,27 +319,35 @@ function asRecord(value: unknown): Record<string, unknown> {
|
||||
* flattened one level, so nothing deeper than the model allows can enter behind the type's back.
|
||||
*/
|
||||
function asStoredValue(value: unknown): PropertyValue | undefined {
|
||||
const field = asFieldValue(value);
|
||||
if (field !== undefined) return field;
|
||||
if (!value || typeof value !== 'object') return undefined;
|
||||
if (isPrimitive(value)) return value;
|
||||
if (Array.isArray(value)) return value.map(asStoredItem).filter((item) => item !== undefined);
|
||||
return asStoredStruct(value, asStoredField);
|
||||
}
|
||||
|
||||
const isPrimitive = (value: unknown): value is number | string | boolean =>
|
||||
typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean';
|
||||
|
||||
/** A list item: a primitive or a struct of primitives. */
|
||||
const asStoredItem = (value: unknown): number | string | boolean | StructValue | undefined =>
|
||||
isPrimitive(value) ? value : asStoredStruct(value, (item) => (isPrimitive(item) ? item : undefined));
|
||||
|
||||
/** A struct field: a primitive or a flat list. A field that is itself a struct is dropped. */
|
||||
const asStoredField = (value: unknown): FieldValue | undefined =>
|
||||
isPrimitive(value) ? value : Array.isArray(value) ? value.filter(isPrimitive) : undefined;
|
||||
|
||||
function asStoredStruct(
|
||||
value: unknown,
|
||||
ofField: (item: unknown) => FieldValue | undefined,
|
||||
): StructValue | undefined {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
||||
const fields: Record<string, FieldValue> = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
const stored = asFieldValue(item);
|
||||
const stored = ofField(item);
|
||||
if (stored !== undefined) fields[key] = stored;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function asFieldValue(value: unknown): FieldValue | undefined {
|
||||
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') return value;
|
||||
if (Array.isArray(value))
|
||||
return value.filter(
|
||||
(item): item is number | string | boolean =>
|
||||
typeof item === 'number' || typeof item === 'string' || typeof item === 'boolean',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown): number {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
|
||||
Reference in new issue
Block a user