/** * Editing one field of a struct rebuilds the whole bundle – an override is one value per property * key, not per field. The question here is what the untouched fields are filled from: an override * is sparse and starts out unset, so only the *resolved* value carries the asset's own numbers. */ import { beforeEach, describe, expect, it } from 'vitest'; import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands'; import { PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands'; import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands'; import type { PropertyValue } from '../../src/core/model/Properties'; import type { SceneObject } from '../../src/core/model/SceneObject'; import { createEmptyProject, findAsset, findScene } from '../../src/core/model/Project'; import { requirePropDef } from '../../src/core/commands/require'; import { resolveAssetProps, resolveSceneObjectProps } from '../../src/core/properties/resolve'; import { structFieldsOf } from '../../src/editor/inspector/previewHandlers'; import { EditorState } from '../../src/editor/state'; const FIELDS = [ { key: 'x', kind: 'number' }, { key: 'y', kind: 'number' }, { key: 'tag', kind: 'text' }, ] as const; function setup() { const state = new EditorState(); state.loadProject(createEmptyProject('p', 'P'), 1); const scene = new CreateAssetCommand('scene', 'Level'); const behavior = new CreateAssetCommand('behavior', 'B'); // A rejected command only returns false, so an unasserted setup would let the tests below pass // for the wrong reason. expect(state.execute(scene)).toBe(true); expect(state.execute(behavior)).toBe(true); expect(state.execute(new AddVariableCommand(behavior.asset.id, 'piece', 'struct'))).toBe(true); for (const field of FIELDS) expect(state.execute(new AddStructFieldCommand(behavior.asset.id, 'piece', { ...field }))).toBe(true); // The asset's own values: what the inspector shows for an object that overrides nothing. expect( state.execute(new SetAssetPropertyCommand(behavior.asset.id, 'piece', { x: 3, y: 4, tag: 'T' })), ).toBe(true); const place = new PlaceAssetCommand(scene.asset.id, behavior.asset.id, { x: 0, y: 0 }); expect(state.execute(place)).toBe(true); return { state, sceneId: scene.asset.id, behaviorId: behavior.asset.id, objectId: place.objectId }; } describe('editing one field of a struct', () => { let f: ReturnType; beforeEach(() => { f = setup(); }); const asset = () => findAsset(f.state.project, f.behaviorId)!; const object = (): SceneObject => findScene(f.state.project, f.sceneId)!.objects.find((o) => o.id === f.objectId)!; const structDef = () => { const def = requirePropDef(f.state.project, asset(), 'piece'); if (def.kind !== 'struct') throw new Error('piece is not a struct'); return def; }; /** The spec `objectSection` builds for an override, resolved value included. */ const objectHandlers = () => structFieldsOf({ state: f.state }, structDef(), { target: object, read: (o) => o.overrides.piece, current: (o) => resolveSceneObjectProps(f.state.project, asset(), o).piece, write: (o, value) => (o.overrides.piece = value), remove: (o) => delete o.overrides.piece, commit: (value) => new SetOverrideCommand(f.sceneId, f.objectId, 'piece', value), }); it('fills the untouched fields of a first override from the asset, not from an empty bundle', () => { objectHandlers()({ key: 'x', kind: 'number' }).onChange(9); expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'T' }); }); it('leaves the others alone on a second edit, and undo goes back one field at a time', () => { objectHandlers()({ key: 'x', kind: 'number' }).onChange(9); objectHandlers()({ key: 'tag', kind: 'text' }).onChange('hi'); expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'hi' }); f.state.commands.undo(); expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'T' }); f.state.commands.undo(); expect(object().overrides.piece).toBeUndefined(); }); it('builds on the preview while typing, and still undoes to before it', () => { const handlers = objectHandlers()({ key: 'x', kind: 'number' }); handlers.onInput(1); handlers.onInput(12); handlers.onChange(12); expect(object().overrides.piece).toEqual({ x: 12, y: 4, tag: 'T' }); f.state.commands.undo(); expect(object().overrides.piece).toBeUndefined(); }); it('copies those values in rather than keeping them tied to the asset', () => { objectHandlers()({ key: 'x', kind: 'number' }).onChange(9); f.state.execute(new SetAssetPropertyCommand(f.behaviorId, 'piece', { x: 3, y: 99, tag: 'T' })); // The whole bundle is overridden now, so the asset moving on no longer reaches y. expect(resolveSceneObjectProps(f.state.project, asset(), object()).piece).toEqual({ x: 9, y: 4, tag: 'T', }); }); it('takes the stored value where that already is the resolved one – the dense asset bag', () => { const handlers = structFieldsOf<{ props: Record }>( { state: f.state }, structDef(), { target: () => asset(), read: (a) => a.props.piece, write: (a, value) => (a.props.piece = value), commit: (value) => new SetAssetPropertyCommand(f.behaviorId, 'piece', value), }, ); handlers({ key: 'y', kind: 'number' }).onChange(8); expect(resolveAssetProps(f.state.project, asset()).piece).toEqual({ x: 3, y: 8, tag: 'T' }); }); });