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
@@ -4,9 +4,11 @@
|
||||
* tab, so what the list belongs to is still on screen while it is edited.
|
||||
*/
|
||||
import { Button, HStack, Label, ScrollContainer, Spacer, Theme, VStack, type Widget } from '../../ui';
|
||||
import { createField, type FieldContext, primitiveDef } from '../inspector/fieldFactory';
|
||||
import { textOf, type ListValue, type PropertyValue } from '../../core/model/Properties';
|
||||
import { type StructDef, structOf } from '../../core/properties/PropertySchema';
|
||||
import { createField, type FieldContext, primitiveDef, type PrimitiveDef } from '../inspector/fieldFactory';
|
||||
import type { EditorState } from '../state';
|
||||
import { type ListAccess, resolveList } from './slot';
|
||||
import { type ListAccess, type ListDef, resolveList } from './slot';
|
||||
|
||||
const INDEX_WIDTH = 40;
|
||||
|
||||
@@ -19,8 +21,8 @@ export class ListEditor extends VStack {
|
||||
private rows = new VStack({ padding: Theme.padding, gap: Theme.gap });
|
||||
/** The structure the rows were built for; only a change here may destroy a row being typed in. */
|
||||
private signature = '';
|
||||
private setters: ((value: number | string | boolean) => void)[] = [];
|
||||
private fields: Widget[] = [];
|
||||
private setters: ((value: PropertyValue) => void)[] = [];
|
||||
private fields: { widget: Widget; parts?: ReadonlyMap<string, Widget> }[] = [];
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super({ gap: 0 });
|
||||
@@ -48,8 +50,9 @@ export class ListEditor extends VStack {
|
||||
}
|
||||
|
||||
/** For the check scripts: where an item's widget sits, since a number field carries no findable text. */
|
||||
fieldAt(index: number): { x: number; y: number } | null {
|
||||
const widget = this.fields[index];
|
||||
fieldAt(index: number, field = ''): { x: number; y: number } | null {
|
||||
const found = this.fields[index];
|
||||
const widget = field ? found?.parts?.get(field) : found?.widget;
|
||||
if (!widget) return null;
|
||||
const p = widget.getGlobalPosition();
|
||||
return { x: p.x + widget.w / 2, y: p.y + widget.h / 2 };
|
||||
@@ -61,7 +64,7 @@ export class ListEditor extends VStack {
|
||||
if (!access) return { title: '', items: [], kind: '' };
|
||||
return {
|
||||
title: access.title,
|
||||
items: access.value.map((item) => String(item)),
|
||||
items: access.value.map((item) => textOf(item)),
|
||||
kind: access.def.items,
|
||||
};
|
||||
}
|
||||
@@ -71,9 +74,11 @@ export class ListEditor extends VStack {
|
||||
return slot ? resolveList(this.state, slot) : null;
|
||||
}
|
||||
|
||||
/** Everything that decides how many rows there are and of what kind. */
|
||||
/** Everything that decides how many rows there are and of what kind – a struct's fields included. */
|
||||
private signatureOf(access: ListAccess | null): string {
|
||||
return access ? `${access.title}|${access.def.items}|${access.value.length}` : '';
|
||||
if (!access) return '';
|
||||
const fields = (access.def.fields ?? []).map((f) => `${f.key}:${f.kind}`).join(',');
|
||||
return `${access.title}|${access.def.items}|${fields}|${access.value.length}`;
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
@@ -109,18 +114,16 @@ export class ListEditor extends VStack {
|
||||
this.relayout();
|
||||
}
|
||||
|
||||
private itemRow(access: ListAccess, item: number | string | boolean, index: number): HStack {
|
||||
const row = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
row.minHeight = Theme.rowHeight;
|
||||
private itemRow(access: ListAccess, item: ListValue[number], index: number): HStack {
|
||||
// A struct item is a stack of field rows: its row grows with it and aligns to the top.
|
||||
const tall = access.def.items === 'struct';
|
||||
const row = new HStack({ gap: Theme.gap, align: tall ? 'start' : 'center' });
|
||||
if (!tall) row.minHeight = Theme.rowHeight;
|
||||
const label = new Label(String(index), { style: 'small', align: 'right' });
|
||||
label.minWidth = INDEX_WIDTH;
|
||||
if (tall) label.minHeight = Theme.rowHeight;
|
||||
row.add(label);
|
||||
const field = createField(
|
||||
this.ctx,
|
||||
primitiveDef(String(index), access.def.items),
|
||||
item,
|
||||
access.itemHandlers(index),
|
||||
);
|
||||
const field = createField(this.ctx, itemDef(access.def, index), item, access.itemHandlers(index));
|
||||
row.add(field.widget, field.grow ? 1 : 0);
|
||||
if (!field.grow) row.add(new Spacer(), 1);
|
||||
row.add(
|
||||
@@ -131,12 +134,20 @@ export class ListEditor extends VStack {
|
||||
}),
|
||||
);
|
||||
this.setters.push(field.setValue);
|
||||
this.fields.push(field.widget);
|
||||
this.fields.push({ widget: field.widget, parts: field.parts });
|
||||
return row;
|
||||
}
|
||||
|
||||
private addItem(): void {
|
||||
const access = this.access();
|
||||
if (access) access.replace([...access.value, primitiveDef('0', access.def.items).default]);
|
||||
if (access) access.replace([...access.value, itemDef(access.def, access.value.length).default]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Item `index` as a definition, so the inspector's own widgets render a row of the list. */
|
||||
function itemDef(def: ListDef, index: number): PrimitiveDef | StructDef {
|
||||
const key = String(index);
|
||||
return def.items === 'struct'
|
||||
? { key, label: key, kind: 'struct', fields: def.fields ?? [], default: structOf(def.fields ?? [], {}) }
|
||||
: primitiveDef(key, def.items);
|
||||
}
|
||||
+34
-5
@@ -12,13 +12,16 @@ import { resolveDef } from '../../core/graph/nested';
|
||||
import type { Asset } from '../../core/model/Asset';
|
||||
import { behaviorLookup } from '../../core/model/behaviorPrograms';
|
||||
import type { ObjectRef } from '../../core/model/instance';
|
||||
import { isList, type ListValue, type PropertyValue } from '../../core/model/Properties';
|
||||
import { isList, type ListValue, type PropertyValue, type StructValue } from '../../core/model/Properties';
|
||||
import { findAsset } from '../../core/model/Project';
|
||||
import {
|
||||
coerceField,
|
||||
coerceListItem,
|
||||
type PropertyDef,
|
||||
type PropertySchema,
|
||||
schemaForAsset,
|
||||
structOf,
|
||||
type StructFieldDef,
|
||||
} from '../../core/properties/PropertySchema';
|
||||
import { resolveAssetProps } from '../../core/properties/resolve';
|
||||
import type { FieldHandlers } from '../inspector/fieldFactory';
|
||||
@@ -53,9 +56,13 @@ const listDefIn = (schema: PropertySchema, key: string): ListDef | null => {
|
||||
return def?.kind === 'list' ? def : null;
|
||||
};
|
||||
|
||||
const withItem = (list: ListValue, index: number, item: number | string | boolean): ListValue =>
|
||||
const withItem = (list: ListValue, index: number, item: number | string | boolean | StructValue): ListValue =>
|
||||
list.map((value, i) => (i === index ? item : value));
|
||||
|
||||
/** The item at `index` in the shape the declaration promises, so a field write never invents one. */
|
||||
const structItemAt = (list: ListValue, index: number, def: ListDef): StructValue =>
|
||||
structOf(def.fields ?? [], list[index]);
|
||||
|
||||
/**
|
||||
* `current` re-resolves rather than closing over a snapshot: a preview writes into the model, so
|
||||
* the next keystroke has to build on what is in there now, and the commit on what the restore
|
||||
@@ -72,11 +79,33 @@ function access<Target>(
|
||||
def,
|
||||
title,
|
||||
value: current(),
|
||||
itemHandlers: (index) =>
|
||||
previewingHandlers<Target>(
|
||||
itemHandlers: (index) => ({
|
||||
...previewingHandlers<Target>(
|
||||
{ state },
|
||||
{ ...spec, toModel: (input) => withItem(current(), index, coerceListItem(input, def.items)) },
|
||||
{
|
||||
...spec,
|
||||
toModel: (input) => withItem(current(), index, coerceListItem(input, def.items, def.fields)),
|
||||
},
|
||||
),
|
||||
// A struct item is edited field by field, and each write rebuilds the whole item from what
|
||||
// is in the list at that moment -- the same rule as a row, one level deeper.
|
||||
...(def.items === 'struct'
|
||||
? {
|
||||
structFieldHandlers: (field: StructFieldDef) =>
|
||||
previewingHandlers<Target>(
|
||||
{ state },
|
||||
{
|
||||
...spec,
|
||||
toModel: (input) =>
|
||||
withItem(current(), index, {
|
||||
...structItemAt(current(), index, def),
|
||||
[field.key]: coerceField(input, field.kind),
|
||||
}),
|
||||
},
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
// No coercion here: every command behind `commit` runs the value through `coerceValue`.
|
||||
// A preview does not, which is why `toModel` above has to.
|
||||
replace: (next) => state.execute(spec.commit(next)),
|
||||
|
||||
Reference in new issue
Block a user