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>
60 lines
2.8 KiB
TypeScript
60 lines
2.8 KiB
TypeScript
import type { Asset } from '../model/Asset';
|
||
import { ancestorIdsOf, type ObjectTree } from '../model/hierarchy';
|
||
import { instanceTargetOf, isEmptyEdit, resolveObjects } from '../model/instance';
|
||
import { findAsset, type Project } from '../model/Project';
|
||
import type { SceneObject } from '../model/SceneObject';
|
||
import { type PropertySchema, schemaFor, schemaForAsset, schemaForObject } from './PropertySchema';
|
||
import { deviatesFromAsset, isModified } from './resolve';
|
||
|
||
/**
|
||
* The rows the asset inspector marks as modified. A behavior's variables are left out: their
|
||
* default is declared on the variable itself, so the asset value cannot differ from it.
|
||
*/
|
||
export function markedAssetSchema(project: Project, asset: Asset): PropertySchema {
|
||
// Declared variables are drawn by the inspector's own section, not as asset rows.
|
||
return asset.type === 'behavior' || asset.type === 'variables' || asset.type === 'struct'
|
||
? schemaFor(asset.type)
|
||
: schemaForAsset(project, asset);
|
||
}
|
||
|
||
/** Whether any asset property the inspector would mark gold differs from its default. */
|
||
export function assetDeviates(project: Project, asset: Asset): boolean {
|
||
return markedAssetSchema(project, asset).some((def) => isModified(project, asset, def.key));
|
||
}
|
||
|
||
/**
|
||
* Whether a placed object deviates from whatever it follows: the container template it was
|
||
* resolved from, or its asset. `holder` must be the stored holder – what an object follows is
|
||
* decided by where it lives, and in a resolved view everything looks stored.
|
||
*/
|
||
export function objectDeviates(project: Project, holder: ObjectTree, object: SceneObject): boolean {
|
||
const target = instanceTargetOf(project, holder, object.id);
|
||
if (target) {
|
||
// Instance edits are pruned to what really deviates, so their presence is the answer.
|
||
const edit = target.instance.instanceEdits[target.path];
|
||
return edit !== undefined && !isEmptyEdit(edit);
|
||
}
|
||
if (object.editorOffset.x !== 0 || object.editorOffset.y !== 0) return true;
|
||
const asset = findAsset(project, object.assetId);
|
||
if (!asset) return false;
|
||
return schemaForObject(project, asset, object).some((def) =>
|
||
deviatesFromAsset(project, object, asset, def.key),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Ids the hierarchy marks: everything deviating itself, plus every ancestor of one. The ancestors
|
||
* matter because subtrees fold – without them a collapsed row would hide that something below it
|
||
* was changed.
|
||
*/
|
||
export function deviationMarks(project: Project, holder: ObjectTree): Set<string> {
|
||
const view: ObjectTree = { objects: resolveObjects(project, holder) };
|
||
const marks = new Set<string>();
|
||
for (const object of view.objects) {
|
||
if (!objectDeviates(project, holder, object)) continue;
|
||
marks.add(object.id);
|
||
for (const ancestorId of ancestorIdsOf(view, object.id)) marks.add(ancestorId);
|
||
}
|
||
return marks;
|
||
}
|