Duplicate an asset from its right-click menu (#297)
A new asset that starts out like an existing one (a death animation
next to "Break", a second slime) had to be built again by hand. Now
the asset panel's right-click menu has Duplicate.
- The copy lands right below the original and is named the way a new
asset would be ("Crate 2"). It is selected and its tab opens where
it has one. A copied scene does not become the active one.
- One undo step takes it out again. A redo brings back the same id.
- The copy is a deep copy: values, graph, tracks, layers and objects
are its own, and editing it leaves the original alone.
- Frames of a texture or of pixel art get fresh ids, because a sprite
names a frame across the whole document.
- Objects of a scene or container get fresh ids the way pasted ones
do. Parents and object references among them follow the copy.
- Whatever pointed at the original keeps pointing at the original: a
sprite wearing it, an animation's root, a placed object.
Tests:
- 3 new unit tests (DuplicateAssetCommand). One counter-check run:
without renaming the references the scene test fails.
- e2e 04: 2 new checks with the real menu (copy named "Crate 2" right
behind the Crate and selected, one undo removes it).
- Gate: 221 files, 3445 tests, CHECK EXIT 0.
- e2e total 1703 is computed (1701 + 2). Full suite left to CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0dda6a012a
commit
4f4c149211
5 files changed
+128
-3
No files matched your search
@@ -0,0 +1,62 @@
|
||||
/** Duplicate in an asset's menu (#297): a copy right behind the original that shares nothing with it. */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand, DuplicateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import type { BehaviorAsset, PixelArtAsset, SceneAsset } from '../../src/core/model/Asset';
|
||||
import { objectReference } from '../../src/core/model/objectRefs';
|
||||
import { findAsset } from '../../src/core/model/Project';
|
||||
import { makeBehavior, makeSpriteScene, place } from '../helpers';
|
||||
|
||||
describe('duplicating an asset', () => {
|
||||
it('puts a copy of a scene behind it, with fresh object ids that still point at each other', () => {
|
||||
const f = makeSpriteScene();
|
||||
const behavior = makeBehavior(f, 'Watcher');
|
||||
f.stack.execute(new AddVariableCommand(behavior.id, 'target', 'object'));
|
||||
const crate = place(f, f.spriteId, { x: 10, y: 0 });
|
||||
const watcher = place(f, behavior.id, { x: 0, y: 0 }, crate);
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, watcher, 'target', objectReference(crate)));
|
||||
const before = structuredClone(f.sceneOf());
|
||||
|
||||
const command = new DuplicateAssetCommand(f.sceneId);
|
||||
f.stack.execute(command);
|
||||
const copy = findAsset(f.project, command.copyId) as SceneAsset;
|
||||
expect(f.project.assets.map((a) => a.name)).toEqual(['Level', 'Level 2', 'Box', 'Watcher']);
|
||||
expect(copy.objects).toHaveLength(2);
|
||||
const [copiedCrate, copiedWatcher] = copy.objects;
|
||||
expect([crate, watcher]).not.toContain(copiedCrate!.id);
|
||||
expect([crate, watcher]).not.toContain(copiedWatcher!.id);
|
||||
expect(copiedWatcher!.parentId).toBe(copiedCrate!.id);
|
||||
expect(copiedWatcher!.overrides.target).toEqual(objectReference(copiedCrate!.id));
|
||||
expect(f.sceneOf()).toEqual(before);
|
||||
|
||||
f.stack.undo();
|
||||
expect(findAsset(f.project, command.copyId)).toBeUndefined();
|
||||
f.stack.redo();
|
||||
expect(f.project.assets[1]?.id).toBe(command.copyId);
|
||||
});
|
||||
|
||||
it('gives the frames of pixel art new ids, since a sprite names a frame across the document', () => {
|
||||
const f = makeSpriteScene();
|
||||
const create = new CreateAssetCommand('pixelart', 'Slime');
|
||||
f.stack.execute(create);
|
||||
const original = create.asset as PixelArtAsset;
|
||||
const command = new DuplicateAssetCommand(original.id);
|
||||
f.stack.execute(command);
|
||||
const copy = findAsset(f.project, command.copyId) as PixelArtAsset;
|
||||
expect(copy.name).toBe('Slime 2');
|
||||
expect(copy.frames[0]?.id).not.toBe(original.frames[0]?.id);
|
||||
expect(copy.layers).toEqual(original.layers);
|
||||
});
|
||||
|
||||
it('copies a graph by value, so editing the copy leaves the original alone', () => {
|
||||
const f = makeSpriteScene();
|
||||
const behavior = makeBehavior(f, 'Mover');
|
||||
const command = new DuplicateAssetCommand(behavior.id);
|
||||
f.stack.execute(command);
|
||||
const copy = findAsset(f.project, command.copyId) as BehaviorAsset;
|
||||
expect(copy.graph).toEqual(behavior.graph);
|
||||
copy.graph.nodes.push({ ...copy.graph.nodes[0]!, id: 'extra' });
|
||||
expect(behavior.graph.nodes.some((n) => n.id === 'extra')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user