Apply/revert buttons for overrides, container instances and child positions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b78382bcc
commit
3379232e64
7 files changed
+544
-37
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ApplyOverrideToAssetCommand,
|
||||
ApplyOverrideToContainerCommand,
|
||||
ApplyPositionToContainerCommand,
|
||||
RevertOverrideToTemplateCommand,
|
||||
RevertPositionToTemplateCommand,
|
||||
} from '../../src/core/commands/applyCommands';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { CommandStack } from '../../src/core/commands/CommandStack';
|
||||
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
|
||||
import { MoveSceneObjectCommand, PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import type { ContainerAsset } from '../../src/core/model/Asset';
|
||||
import { templateLinkOf } from '../../src/core/model/container';
|
||||
import { createEmptyProject, findAsset, findScene } from '../../src/core/model/Project';
|
||||
|
||||
/**
|
||||
* A box asset, a container made from two boxes (a at (100,100), b at (10,0)), and a second
|
||||
* placed instance of that container. `a` is the original, `a2` its copy in the instance.
|
||||
*/
|
||||
function setup() {
|
||||
const project = createEmptyProject('p', 'Apply');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const sprite = new CreateAssetCommand('sprite', 'Box');
|
||||
stack.execute(scene);
|
||||
stack.execute(sprite);
|
||||
const a = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 100, y: 100 });
|
||||
const b = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 10, y: 0 });
|
||||
stack.execute(a);
|
||||
stack.execute(b);
|
||||
const create = new CreateContainerCommand(scene.asset.id, [a.objectId, b.objectId]);
|
||||
stack.execute(create);
|
||||
const place = new PlaceAssetCommand(scene.asset.id, create.assetId, { x: 1000, y: 0 });
|
||||
stack.execute(place);
|
||||
const sceneOf = () => findScene(project, scene.asset.id)!;
|
||||
const object = (id: string) => sceneOf().objects.find((o) => o.id === id)!;
|
||||
const container = () => findAsset(project, create.assetId) as ContainerAsset;
|
||||
const templateOfA = () => container().objects.find((t) => t.id === object(a.objectId).templateId)!;
|
||||
const a2 = sceneOf().objects.find((o) => o.parentId === place.objectId && o.templateId === object(a.objectId).templateId)!.id;
|
||||
return { project, stack, sceneId: scene.asset.id, spriteId: sprite.asset.id, a: a.objectId, b: b.objectId, a2, object, container, templateOfA };
|
||||
}
|
||||
|
||||
describe('template link', () => {
|
||||
it('resolves the container and template for instance objects only', () => {
|
||||
const { project, sceneId, a, a2, object, container, templateOfA } = setup();
|
||||
const scene = findScene(project, sceneId)!;
|
||||
const link = templateLinkOf(project, scene, object(a2));
|
||||
expect(link?.asset.id).toBe(container().id);
|
||||
expect(link?.template).toBe(templateOfA());
|
||||
expect(templateLinkOf(project, scene, object(a))?.template).toBe(templateOfA());
|
||||
const containerObject = scene.objects.find((o) => o.assetId === container().id)!;
|
||||
expect(templateLinkOf(project, scene, containerObject)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('position apply / revert', () => {
|
||||
it('applies one axis to the template; instances still at the template value follow', () => {
|
||||
const { stack, sceneId, a, a2, object, templateOfA } = setup();
|
||||
expect(templateOfA().position).toEqual({ x: 45, y: 50, z: 0 });
|
||||
// a2 was moved by hand on x → it keeps that; on y it still matches the template.
|
||||
stack.execute(new MoveSceneObjectCommand(sceneId, a2, { x: 0, y: 50, z: 0 }));
|
||||
stack.execute(new MoveSceneObjectCommand(sceneId, a, { x: 60, y: 70, z: 0 }));
|
||||
|
||||
stack.execute(new ApplyPositionToContainerCommand(sceneId, a, 'x'));
|
||||
expect(templateOfA().position.x).toBe(60);
|
||||
expect(object(a2).position.x).toBe(0); // modified on its own → untouched
|
||||
stack.execute(new ApplyPositionToContainerCommand(sceneId, a, 'y'));
|
||||
expect(templateOfA().position.y).toBe(70);
|
||||
expect(object(a2).position.y).toBe(70); // was at the template value → follows
|
||||
|
||||
stack.undo();
|
||||
expect(templateOfA().position.y).toBe(50);
|
||||
expect(object(a2).position.y).toBe(50);
|
||||
stack.undo();
|
||||
expect(templateOfA().position.x).toBe(45);
|
||||
expect(object(a).position.x).toBe(60); // the source object itself is not changed by apply
|
||||
});
|
||||
|
||||
it('reverts an axis (or the whole position) to the template', () => {
|
||||
const { stack, sceneId, a, object } = setup();
|
||||
stack.execute(new MoveSceneObjectCommand(sceneId, a, { x: 1, y: 2, z: 3 }));
|
||||
stack.execute(new RevertPositionToTemplateCommand(sceneId, a, 'z'));
|
||||
expect(object(a).position).toEqual({ x: 1, y: 2, z: 0 });
|
||||
stack.execute(new RevertPositionToTemplateCommand(sceneId, a));
|
||||
expect(object(a).position).toEqual({ x: 45, y: 50, z: 0 });
|
||||
stack.undo();
|
||||
expect(object(a).position).toEqual({ x: 1, y: 2, z: 0 });
|
||||
});
|
||||
|
||||
it('rejects objects that are not part of a container', () => {
|
||||
const { project, stack, sceneId, spriteId } = setup();
|
||||
const loose = new PlaceAssetCommand(sceneId, spriteId, { x: 0, y: 0 });
|
||||
stack.execute(loose);
|
||||
expect(() => stack.execute(new ApplyPositionToContainerCommand(sceneId, loose.objectId))).toThrow(/not part of a placed container/);
|
||||
expect(findScene(project, sceneId)!.objects.find((o) => o.id === loose.objectId)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('override apply / revert', () => {
|
||||
it('applies an override to the container template and to instances that had no own value', () => {
|
||||
const { stack, sceneId, a, a2, object, templateOfA } = setup();
|
||||
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
|
||||
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
|
||||
expect(templateOfA().overrides.width).toBe(77);
|
||||
expect(object(a2).overrides.width).toBe(77);
|
||||
expect(object(a).overrides.width).toBe(77);
|
||||
stack.undo();
|
||||
expect(templateOfA().overrides.width).toBeUndefined();
|
||||
expect(object(a2).overrides.width).toBeUndefined();
|
||||
expect(object(a).overrides.width).toBe(77);
|
||||
});
|
||||
|
||||
it('leaves instances alone that already had their own override', () => {
|
||||
const { stack, sceneId, a, a2, object, templateOfA } = setup();
|
||||
stack.execute(new SetOverrideCommand(sceneId, a2, 'width', 5));
|
||||
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
|
||||
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
|
||||
expect(templateOfA().overrides.width).toBe(77);
|
||||
expect(object(a2).overrides.width).toBe(5);
|
||||
});
|
||||
|
||||
it('applies an override to the asset, dropping the override on object and template', () => {
|
||||
const { project, stack, sceneId, spriteId, a, a2, object, templateOfA } = setup();
|
||||
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
|
||||
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
|
||||
stack.execute(new ApplyOverrideToAssetCommand(sceneId, a, 'width'));
|
||||
const sprite = () => findAsset(project, spriteId)!;
|
||||
expect(sprite().props.width).toBe(77);
|
||||
expect(object(a).overrides.width).toBeUndefined();
|
||||
expect(templateOfA().overrides.width).toBeUndefined();
|
||||
expect(object(a2).overrides.width).toBeUndefined(); // shared the template value → follows the asset now
|
||||
stack.undo();
|
||||
expect(sprite().props.width).toBe(100);
|
||||
expect(object(a).overrides.width).toBe(77);
|
||||
expect(templateOfA().overrides.width).toBe(77);
|
||||
expect(object(a2).overrides.width).toBe(77);
|
||||
});
|
||||
|
||||
it('applies to the asset for a plain object too', () => {
|
||||
const { project, stack, sceneId, spriteId, object } = setup();
|
||||
const loose = new PlaceAssetCommand(sceneId, spriteId, { x: 0, y: 0 });
|
||||
stack.execute(loose);
|
||||
stack.execute(new SetOverrideCommand(sceneId, loose.objectId, 'height', 33));
|
||||
stack.execute(new ApplyOverrideToAssetCommand(sceneId, loose.objectId, 'height'));
|
||||
expect(findAsset(project, spriteId)!.props.height).toBe(33);
|
||||
expect(object(loose.objectId).overrides.height).toBeUndefined();
|
||||
expect(() => stack.execute(new ApplyOverrideToAssetCommand(sceneId, loose.objectId, 'nope'))).toThrow(/not defined/);
|
||||
});
|
||||
|
||||
it('reverts an override to the template value (or removes it when the template has none)', () => {
|
||||
const { stack, sceneId, a, a2, object, templateOfA } = setup();
|
||||
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
|
||||
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
|
||||
stack.execute(new SetOverrideCommand(sceneId, a2, 'width', 5));
|
||||
stack.execute(new RevertOverrideToTemplateCommand(sceneId, a2, 'width'));
|
||||
expect(object(a2).overrides.width).toBe(77);
|
||||
expect(templateOfA().overrides.width).toBe(77);
|
||||
stack.execute(new SetOverrideCommand(sceneId, a2, 'height', 9));
|
||||
stack.execute(new RevertOverrideToTemplateCommand(sceneId, a2, 'height'));
|
||||
expect(object(a2).overrides.height).toBeUndefined();
|
||||
stack.undo();
|
||||
expect(object(a2).overrides.height).toBe(9);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user