Define a struct once as an asset and reuse its shape (#253)
A struct used to declare its fields wherever it stood. The same shape in two variables meant typing it twice, and Pack/Expand copied it once without staying in sync. A Struct asset now holds the shape in one place, and every struct that picks it as its type follows it. - New asset type "Struct" (not placeable, group Structs, own icon). The inspector shows its fields with name, kind and default value, and "Open" shows them in the variables tab. - Struct variables and struct fields get a Type row: own fields, any Struct asset, or "Make struct type". With a type the fields come from the asset and cannot be added, renamed or removed there. The values stay per variable, overrides on placed objects still work. - Pack and Expand get the setting Type. With a type their ports are the fields of the asset and read-only in the inspector. - A change to the asset reaches every typed place in one undo step (syncStructTypes). A new field arrives with its default. A removed field goes along with its port, edge and typed value. A renamed field keeps its value in variables, overrides and on ports with edges, also through nested types. - Undo for these edits records plain assignments (Recorder), because the reach of a change is not known in advance. - Cycles such as Loot in Loot, also through another type, are refused and not offered in the picker. - Deleting the asset names how many places use it. Afterwards each place keeps its fields as its own. - Loading reshapes a stale copy in a project file to its type. - "Make struct type" creates an asset from an existing struct and links the place to it. Differs from the plan: - A changed default value applies to new fields and newly typed places only. Existing values stay. - Removing a single field shows no warning. Undo brings it back. Typed lists are left for #254. Lists stay dynamic for now. Found by e2e: after deleting or renaming the type the inspector did not rebuild and kept showing the old name. Fixed, the check was red before. Tests: - ./check.sh: 182 files, 2661 tests, CHECK EXIT 0 - GM_STEPS=1,4,21,22,54: ALL PASSED, 176 checks - 14 unit tests in tests/core/struct-types.test.ts - New e2e module 54 with 14 checks, real mouse. Module 04 counts 23 asset types. - Counter-probes: without the rename pass 4 unit tests fail, without reshaping values 8, without the cycle rule 1. In the browser, without spreading changes and with + Field despite a type, 3 checks fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4857bb75e6
commit
61bbb96a96
29 files changed
+1523
-66
No files matched your search
@@ -18,13 +18,17 @@ import {
|
||||
resolveDef,
|
||||
STATE_TRANSITION,
|
||||
stateName,
|
||||
STRUCT_TYPE_PARAM,
|
||||
STRUCT_TYPE_SETTING,
|
||||
TRANSITION_PARAM,
|
||||
variablesSourceOf,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import { findGraphAsset, type Project } from '../../core/model/Project';
|
||||
import { textOf } from '../../core/model/Properties';
|
||||
import { type PropertyValue, textOf } from '../../core/model/Properties';
|
||||
import { isTypedNode, nodeStructType, structAssetOf } from '../../core/model/structTypes';
|
||||
import { SetStructTypeCommand } from '../../core/commands/structTypeCommands';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { Label } from '../../ui';
|
||||
import { createFieldRow, type FieldHandlers } from './fieldFactory';
|
||||
@@ -63,7 +67,8 @@ export function nodeSignature(project: Project, assetId: string, nodeId: string)
|
||||
.join(',')
|
||||
: `${textOf(node.params[TRANSITION_PARAM] ?? '')}:${textOf(node.params[MACHINE_PORT] ?? '')}`;
|
||||
// The source names which declarations the port menu offers, so switching it has to rebuild.
|
||||
const source = variablesSourceOf(node.params);
|
||||
const typeName = structAssetOf(project, nodeStructType(node))?.name ?? '';
|
||||
const source = `${variablesSourceOf(node.params)}:${nodeStructType(node)}:${typeName}`;
|
||||
const choices = variableChoices(project, assetId, node)
|
||||
.map((v) => v.key)
|
||||
.join(',');
|
||||
@@ -94,25 +99,34 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
addNote(host, def.description);
|
||||
const problem = callProblem(node, graphLookup(host.state.project), assetId);
|
||||
if (problem) addNote(host, problem, 'warning');
|
||||
const addSetting = (param: PropertyDef) => {
|
||||
const row = createFieldRow(
|
||||
host,
|
||||
param,
|
||||
node.params[param.key] ?? param.default,
|
||||
paramHandlers(host, assetId, nodeId, param),
|
||||
);
|
||||
const addSetting = (param: PropertyDef, handlers = paramHandlers(host, assetId, nodeId, param)) => {
|
||||
const row = createFieldRow(host, param, node.params[param.key] ?? param.default, handlers);
|
||||
host.fields.set(`param:${param.key}`, row);
|
||||
host.body.add(row.row);
|
||||
};
|
||||
// Both draw their settings inside the port section: a setting decides what the menu offers.
|
||||
const ownSettings = declaresPropertyPorts(node.type) || declaresVariablePorts(node.type);
|
||||
// The type of a Pack/Expand decides its fields (#253), so it stands above them – and picking one
|
||||
// rewrites the ports, which a plain param edit would not.
|
||||
const typed = isTypedNode(node);
|
||||
if (typed) {
|
||||
const pick = (value: PropertyValue) => {
|
||||
const target = { kind: 'node', assetId, nodeId } as const;
|
||||
host.state.execute(new SetStructTypeCommand(target, textOf(value)));
|
||||
};
|
||||
addSetting(STRUCT_TYPE_SETTING, { onInput: () => {}, onChange: pick });
|
||||
}
|
||||
if (declaresVariablePorts(node.type)) buildVariablePortSection(host, assetId, node, addSetting);
|
||||
else if (ownSettings) buildPropertyPortSection(host, assetId, node, addSetting);
|
||||
else if (def.declaresPorts) buildPortSection(host, assetId, node, variablesOf(asset));
|
||||
else if (def.declaresPorts) {
|
||||
const typeName = structAssetOf(host.state.project, nodeStructType(node))?.name ?? null;
|
||||
buildPortSection(host, assetId, node, variablesOf(asset), typeName);
|
||||
}
|
||||
|
||||
if (!ownSettings && def.params.length > 0) {
|
||||
const settings = def.params.filter((param) => !(typed && param.key === STRUCT_TYPE_PARAM));
|
||||
if (!ownSettings && settings.length > 0) {
|
||||
addTitle(host, 'SETTINGS');
|
||||
for (const param of def.params) {
|
||||
for (const param of settings) {
|
||||
if (param.key === TRANSITION_PARAM && node.type === STATE_TRANSITION)
|
||||
buildTransitionPicker(host, assetId, node, asset.graph);
|
||||
else addSetting(param);
|
||||
|
||||
Reference in new issue
Block a user