Drop the per-list item declaration; every item carries its own kind
A list no longer says what its items are. ListDef.items, ListDef.fields,
ListFieldDef.items and SetListItemsCommand are gone; the single seam is
src/core/properties/listItems.ts. JSON already tells number, text, boolean
and struct apart, so only two kinds need a tag: {"$color": n} and the new
{"$texture": id}. Field names cannot start with "$", so a tag never
collides with a struct field. On a data link a texture item arrives as
its bare id; the graph has no images.
List tab: the mark in front of each row is the kind picker, "+ Item"
asks for the kind, both are ordinary value edits (one undo step). A list
inside a struct field only offers the flat kinds because the depth rule
forbids a struct item there (ListAccess.kinds); the menu used to offer
it and silently do nothing.
Legacy lists that declared items: 'color' are tagged on load.
Actor demo: the four walk animations and four standing frames live in
two list variables of texture items indexed by facing (Item at), so the
graph can be rebuilt in the editor. The stand frames get their grid ids
back; 44 nodes instead of 46.
Also: the user's edits to on-start-during-play.md and the new note
reseed-because-of-list.md.
Verified: check.sh 794 tests, full e2e 631 checks, smokes 24/19/20.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
e147dd6d21
commit
11c900c998
34 files changed
+1218
-640
No files matched your search
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
type FieldValue,
|
||||
isList,
|
||||
isStruct,
|
||||
type ListValue,
|
||||
type PropertyBag,
|
||||
type PropertyValue,
|
||||
type StructValue,
|
||||
@@ -10,13 +8,9 @@ import {
|
||||
import { overrideSites } from '../model/instance';
|
||||
import type { Project } from '../model/Project';
|
||||
import {
|
||||
coerceValue,
|
||||
emptyFieldValue,
|
||||
isValidFieldName,
|
||||
isValidVariableName,
|
||||
type ListItemKind,
|
||||
type PrimitiveFieldDef,
|
||||
primitiveFieldsOf,
|
||||
type PropertyDef,
|
||||
type StructFieldDef,
|
||||
type VariableKind,
|
||||
@@ -145,10 +139,8 @@ export class RenameVariableCommand implements Command {
|
||||
const INVALID_NAME = 'Variable names must start with a letter and contain only letters, digits or _';
|
||||
const INVALID_FIELD = 'Field names must start with a letter and contain only letters, digits or _';
|
||||
|
||||
/** Where declared fields live: on a struct variable, or on the struct items of a list variable. */
|
||||
/** Where declared fields live: on a struct variable. A list item carries its own shape instead. */
|
||||
interface FieldOwner {
|
||||
/** True for a list of structs, where a field may not itself be a list (`ListFieldDef`). */
|
||||
readonly inList: boolean;
|
||||
fields(): readonly StructFieldDef[];
|
||||
setFields(next: readonly StructFieldDef[]): void;
|
||||
}
|
||||
@@ -159,24 +151,13 @@ function variableOf(project: Project, assetId: string, key: string): PropertyDef
|
||||
|
||||
function requireFieldOwner(project: Project, assetId: string, key: string): FieldOwner {
|
||||
const def = variableOf(project, assetId, key);
|
||||
if (def?.kind === 'struct')
|
||||
return {
|
||||
inList: false,
|
||||
fields: () => def.fields,
|
||||
setFields: (next) => (def.fields = next),
|
||||
};
|
||||
if (def?.kind === 'list' && def.items === 'struct')
|
||||
return {
|
||||
inList: true,
|
||||
fields: () => def.fields ?? [],
|
||||
setFields: (next) => (def.fields = primitiveFieldsOf(next)),
|
||||
};
|
||||
throw new CommandError(`Variable "${key}" has no fields`);
|
||||
if (def?.kind !== 'struct') throw new CommandError(`Variable "${key}" has no fields`);
|
||||
return { fields: () => def.fields, setFields: (next) => (def.fields = next) };
|
||||
}
|
||||
|
||||
/**
|
||||
* One struct-carrying value of a variable (default, own, override, or list item); a field change hits
|
||||
* all of them at once so none keeps a stale shape. `id` pairs a site up again on undo.
|
||||
* One struct-carrying value of a variable (its declared default, the behavior's own, an override); a
|
||||
* field change hits all of them at once so none keeps a stale shape. `id` pairs a site up on undo.
|
||||
*/
|
||||
interface StructSite {
|
||||
id: string;
|
||||
@@ -185,36 +166,16 @@ interface StructSite {
|
||||
}
|
||||
|
||||
function structSites(project: Project, assetId: string, key: string): StructSite[] {
|
||||
const def = variableOf(project, assetId, key);
|
||||
const holders = valueHolders(project, assetId, key).filter(
|
||||
// An override the object does not have stays absent: writing one here would invent a deviation.
|
||||
(h) => !h.optional || (def?.kind === 'list' ? isList(h.read()) : isStruct(h.read())),
|
||||
);
|
||||
const sites: StructSite[] = [];
|
||||
for (const holder of holders) {
|
||||
if (def?.kind !== 'list') {
|
||||
sites.push({
|
||||
return (
|
||||
valueHolders(project, assetId, key)
|
||||
// An override the object does not have stays absent: writing one here would invent a deviation.
|
||||
.filter((h) => !h.optional || isStruct(h.read()))
|
||||
.map((holder) => ({
|
||||
id: holder.id,
|
||||
read: () => (isStruct(holder.read()) ? (holder.read() as StructValue) : {}),
|
||||
write: (value) => holder.write(value),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const list = holder.read();
|
||||
if (!isList(list)) continue;
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
sites.push({
|
||||
id: `${holder.id}#${index}`,
|
||||
read: () => {
|
||||
const item = (holder.read() as ListValue)[index];
|
||||
return isStruct(item) ? item : {};
|
||||
},
|
||||
write: (value) =>
|
||||
holder.write((holder.read() as ListValue).map((item, at) => (at === index ? value : item))),
|
||||
});
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
write: (value: StructValue) => holder.write(value),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/** Everywhere one variable's value is kept: its declared default, the behavior's own, the overrides. */
|
||||
@@ -278,8 +239,6 @@ export class AddStructFieldCommand implements Command {
|
||||
if (!isValidFieldName(this.field.key)) throw new CommandError(INVALID_FIELD);
|
||||
if (owner.fields().some((f) => f.key === this.field.key))
|
||||
throw new CommandError(`Field "${this.field.key}" already exists`);
|
||||
if (owner.inList && this.field.kind === 'list')
|
||||
throw new CommandError('A list field cannot sit in the items of a list');
|
||||
owner.setFields([...owner.fields(), { ...this.field }]);
|
||||
for (const site of structSites(project, this.assetId, this.key))
|
||||
site.write({ ...site.read(), [this.field.key]: emptyFieldValue(this.field) });
|
||||
@@ -367,71 +326,3 @@ export class RenameStructFieldCommand implements Command {
|
||||
site.write(renameField(site.read(), from, to));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes what a list variable holds. The declaration is the only place that says it, so every
|
||||
* value has to follow at once – each is run through `coerceValue` against the new declaration,
|
||||
* which is the same conversion an item edited in the List tab goes through.
|
||||
*/
|
||||
export class SetListItemsCommand implements Command {
|
||||
readonly label: string;
|
||||
private before?: {
|
||||
items: ListItemKind;
|
||||
fields?: readonly PrimitiveFieldDef[];
|
||||
values: Map<string, PropertyValue>;
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly key: string,
|
||||
private readonly items: ListItemKind,
|
||||
) {
|
||||
this.label = `Set ${key} items`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const def = requireListDef(project, this.assetId, this.key);
|
||||
if (def.items === this.items)
|
||||
throw new CommandError(`Variable "${this.key}" already holds ${this.items}`);
|
||||
const values = new Map<string, PropertyValue>();
|
||||
for (const holder of valueHolders(project, this.assetId, this.key)) {
|
||||
const value = holder.read();
|
||||
if (value !== undefined) values.set(holder.id, value);
|
||||
}
|
||||
this.before = { items: def.items, fields: def.fields, values };
|
||||
def.items = this.items;
|
||||
// Fields belong to struct items alone: kept while leaving so an undo has them back.
|
||||
if (this.items === 'struct' && !def.fields) def.fields = [];
|
||||
this.recoerce(project);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
if (!this.before) return;
|
||||
const def = requireListDef(project, this.assetId, this.key);
|
||||
def.items = this.before.items;
|
||||
if (this.before.fields) def.fields = this.before.fields;
|
||||
else delete def.fields;
|
||||
for (const holder of valueHolders(project, this.assetId, this.key)) {
|
||||
const value = this.before.values.get(holder.id);
|
||||
if (value !== undefined) holder.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
private recoerce(project: Project): void {
|
||||
const def = requireListDef(project, this.assetId, this.key);
|
||||
for (const holder of valueHolders(project, this.assetId, this.key)) {
|
||||
const value = holder.read();
|
||||
if (value !== undefined) holder.write(coerceValue(def, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requireListDef(
|
||||
project: Project,
|
||||
assetId: string,
|
||||
key: string,
|
||||
): Extract<PropertyDef, { kind: 'list' }> {
|
||||
const def = variableOf(project, assetId, key);
|
||||
if (def?.kind !== 'list') throw new CommandError(`Variable "${key}" is not a list`);
|
||||
return def;
|
||||
}
|
||||
Reference in new issue
Block a user