The List tab could show one list at a time and was the only place to see its items. A list now shows its items right where it lives, and a new Variables tab shows all variables of an asset or placed object. - InlineList draws the items of a list under its row in the inspector, in struct fields and in the Variables tab. A list inside a list or a struct field is drawn the same way, one step indented. - The index column is the indent. Items sit under their list without an own margin, and the index column is 16 px wide instead of 28 px. - Above 20 items (INLINE_LIMIT) the rest collapses into "... N more - open in tab". During a run the rows show the values from the game. - ListHead puts "N items" and "+ Item" in one row. "N items" opens the list in a tab. A separate Open button squeezed the counter to zero width in the narrow inspector column. - VariablesEditor shows every variable of a behavior asset, a variables asset or a placed behavior. On an asset it edits the declarations. On a placed object an edit only overrides that object, with the usual reset and push buttons. - Opening a list inside a tab adds a new tab and keeps the open one. valuesKey gives each list its own document, so a list that is already open is shown instead of opened twice. - Titles read "Pickup · Variables" or "Listy · nums · [3]". The tab closes when its asset or object is deleted. - Lists on node inputs stay inline only, a node owns no variables. - The List tab, ListEditor and the __gm.list hook are gone. Tests: - ./check.sh: 181 files, 2645 tests, CHECK EXIT 0 - GM_STEPS=1,21,22,32: ALL PASSED, 123 checks - e2e 21 is rewritten for inline lists and the new tab, 22 and 32 are adapted. Sum in README: 1486 (+6 in 21, -1 in 22), counted by hand. - Counter-probes fail as expected: sub-lists not told apart (1 unit test), sub-list replacing the open tab, all items drawn inline (60 rows instead of 20), no live values (0 rows at 24 items in a run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
5.1 KiB
TypeScript
121 lines
5.1 KiB
TypeScript
import { SetAssetPropertyCommand } from '../../core/commands/assetCommands';
|
||
import { SetNodeParamCommand } from '../../core/commands/graphCommands';
|
||
import { SetOverrideCommand } from '../../core/commands/sceneObjectCommands';
|
||
import { heldPosition, placedGridSize, ResizeTilemapEdgeCommand } from '../../core/commands/tilemapCommands';
|
||
import { findNode, type Graph, type GraphNode } from '../../core/graph/Graph';
|
||
import type { Asset } from '../../core/model/Asset';
|
||
import type { ObjectRef } from '../../core/model/instance';
|
||
import type { Position } from '../../core/model/Properties';
|
||
import { findAsset, findGraphAsset, type Project } from '../../core/model/Project';
|
||
import { TILEMAP_COLUMNS_KEY, type TilemapEdge, tilemapTileSize } from '../../core/model/tilemap';
|
||
import { coerceValue, schemaFor } from '../../core/properties/PropertySchema';
|
||
import { resolveSceneObjectProps } from '../../core/properties/resolve';
|
||
import type { EditorState } from '../state';
|
||
import { currentProps, currentRef } from './objectQueries';
|
||
import type { PreviewedValue } from './previewHandlers';
|
||
|
||
/** Where one value lives, shared by the inspector rows and the inline lists so both write it the same way. */
|
||
export function assetValue(state: EditorState, assetId: string, key: string): PreviewedValue<Asset> {
|
||
return {
|
||
target: () => findAsset(state.project, assetId),
|
||
read: (asset) => asset.props[key],
|
||
write: (asset, value) => (asset.props[key] = value),
|
||
// No `remove`: an asset bag is dense – loading fills the defaults, a new variable brings its own.
|
||
commit: (value) => new SetAssetPropertyCommand(assetId, key, value),
|
||
};
|
||
}
|
||
|
||
export function overrideValue(
|
||
state: EditorState,
|
||
holderId: string,
|
||
objectId: string,
|
||
key: string,
|
||
): PreviewedValue<ObjectRef> {
|
||
return {
|
||
target: () => currentRef(state, holderId, objectId),
|
||
read: (ref) => ref.object.overrides[key],
|
||
current: () => currentProps(state, holderId, objectId)?.[key],
|
||
write: (ref, value) => ref.setOverride(key, value),
|
||
remove: (ref) => ref.setOverride(key, undefined),
|
||
commit: (value) => new SetOverrideCommand(holderId, objectId, key, value),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Columns or Rows of a placed tilemap (#136): the grid grows and shrinks at the right or bottom edge
|
||
* while the painted cells stay where they are in the scene, so the origin moves along. The preview
|
||
* works from where the edit started rather than step by step – dragging the value back and forth
|
||
* would otherwise leave rounding drift behind, and restoring it before the commit would not be exact.
|
||
*/
|
||
export function tilemapSizeValue(
|
||
state: EditorState,
|
||
holderId: string,
|
||
objectId: string,
|
||
key: string,
|
||
): PreviewedValue<ObjectRef> {
|
||
const plain = overrideValue(state, holderId, objectId, key);
|
||
const edge: TilemapEdge = key === TILEMAP_COLUMNS_KEY ? 'right' : 'bottom';
|
||
let start: { position: Position; size: number } | undefined;
|
||
// Writes the value, then puts the origin where the size now asks for, counted from the start.
|
||
const holding = (ref: ObjectRef, write: () => void): void => {
|
||
const asset = findAsset(state.project, ref.object.assetId);
|
||
if (!asset) return write();
|
||
start ??= {
|
||
position: { ...ref.object.position },
|
||
size: placedGridSize(state.project, asset, ref.object, key),
|
||
};
|
||
write();
|
||
const props = resolveSceneObjectProps(state.project, asset, ref.object);
|
||
const size = placedGridSize(state.project, asset, ref.object, key);
|
||
ref.setPosition(
|
||
heldPosition(asset, ref.object, start.position, edge, size - start.size, tilemapTileSize(props)),
|
||
);
|
||
};
|
||
return {
|
||
...plain,
|
||
write: (ref, value) => holding(ref, () => ref.setOverride(key, value)),
|
||
remove: (ref) => holding(ref, () => ref.setOverride(key, undefined)),
|
||
commit: (value) => {
|
||
start = undefined;
|
||
const ref = currentRef(state, holderId, objectId);
|
||
const asset = ref && findAsset(state.project, ref.object.assetId);
|
||
const def = asset && schemaFor('tilemap').find((d) => d.key === key);
|
||
if (!ref || !asset || !def) return plain.commit(value);
|
||
const next = Number(coerceValue(def, value));
|
||
return new ResizeTilemapEdgeCommand(
|
||
holderId,
|
||
objectId,
|
||
edge,
|
||
next - placedGridSize(state.project, asset, ref.object, key),
|
||
);
|
||
},
|
||
};
|
||
}
|
||
|
||
export function nodeParamValue(
|
||
state: EditorState,
|
||
assetId: string,
|
||
nodeId: string,
|
||
key: string,
|
||
): PreviewedValue<GraphNode> {
|
||
return {
|
||
target: () => currentNode(state.project, assetId, nodeId)?.node,
|
||
read: (node) => node.params[key],
|
||
write: (node, value) => (node.params[key] = value),
|
||
remove: (node) => delete node.params[key],
|
||
commit: (value) => new SetNodeParamCommand(assetId, nodeId, key, value),
|
||
};
|
||
}
|
||
|
||
/** The node together with its graph, because its ports are derived from both. */
|
||
export function currentNode(
|
||
project: Project,
|
||
assetId: string,
|
||
nodeId: string,
|
||
): { node: GraphNode; graph: Graph } | undefined {
|
||
const asset = findGraphAsset(project, assetId);
|
||
if (!asset) return undefined;
|
||
const node = findNode(asset.graph, nodeId);
|
||
return node && { node, graph: asset.graph };
|
||
}
|