Two findings from #269. A reference from a container into a placement nested in it was dead from the start, and deleting an asset left references to the objects it took along showing "(missing)". - A reference is stored the way its holder shows the object. In a container tab that is q~x for content of a nested placement q. - referenceCandidates translates it when the container is placed: in placement r, q~x becomes r~q/x. pathOfId and idOfPath in instance.ts do the mapping, objectChoices offers the same spelling the other way. The rule holds at any depth. - Storing q/x as proposed in #285 was dropped. The container tab would then have had to translate its own ids. - No migration. All projects and snapshots hold three references, none of them with ~ or /. - Create container lets a reference to content of a member placement follow it. It was left dead before. - Paste lets such a reference point into the copy. It kept pointing at the original before. Both go through the new renamedReference. - Delete asset records what every holder shows before the cascade and clears every reference to an object that is gone afterwards, with dropReferencesToGone from #269. The row shows None, undo restores the references (#286). Tests: - Gate: 198 files, 2882 tests (2873 + 9), exit 0 - Unit object-references: 26 passed (17 + 9) - e2e 1-17,57: 566 checks passed, 57 has +57i and +57j - Six unit counter-probes fail between 1 and 3 tests each - The browser counter-probe fails 57i and 57j as described in the issues - e2e total 1609 is computed, not measured in a full run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
214218d1b2
commit
5e21d473a4
9 files changed
+295
-17
No files matched your search
@@ -4,7 +4,7 @@
|
||||
* own copy of it.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { CreateAssetCommand, DeleteAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { CreateContainerCommand, UnpackContainerCommand } from '../../src/core/commands/containerCommands';
|
||||
import { PasteObjectsCommand } from '../../src/core/commands/pasteCommands';
|
||||
import { AddPortsCommand } from '../../src/core/commands/portCommands';
|
||||
@@ -19,7 +19,12 @@ import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/comman
|
||||
import type { BehaviorAsset, ContainerAsset } from '../../src/core/model/Asset';
|
||||
import { copyObjects } from '../../src/core/model/clipboard';
|
||||
import { instanceChildId, resolvedHolder } from '../../src/core/model/instance';
|
||||
import { objectChoices, objectReference, referencedId } from '../../src/core/model/objectRefs';
|
||||
import {
|
||||
objectChoices,
|
||||
objectReference,
|
||||
referenceCandidates,
|
||||
referencedId,
|
||||
} from '../../src/core/model/objectRefs';
|
||||
import { NO_OBJECT } from '../../src/core/model/Properties';
|
||||
import { findAsset, findObjectHolder } from '../../src/core/model/Project';
|
||||
import { makeItem } from '../../src/core/properties/listItems';
|
||||
@@ -355,4 +360,130 @@ describe('removing an object from a container template (#269)', () => {
|
||||
expect(targetOf(f, outerAsset.id, instanceChildId(inner, f.tWatcher))).toEqual({ $object: f.tCrate });
|
||||
expect(targetOf(f, f.level2, nestedWatcher)).toEqual({ $object: f.tBarrel });
|
||||
});
|
||||
|
||||
it('does the same when the asset of the object is deleted (#286)', () => {
|
||||
const f = placedTwice();
|
||||
f.stack.execute(new DeleteAssetCommand(f.spriteId));
|
||||
for (const [sceneId, placed] of f.placements) {
|
||||
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
|
||||
expect(editOf(f, sceneId, placed)).toBeUndefined();
|
||||
}
|
||||
f.stack.undo();
|
||||
for (const [sceneId, placed] of f.placements)
|
||||
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual({ $object: f.tCrate });
|
||||
});
|
||||
});
|
||||
|
||||
describe('a reference into a placement nested in a container (#285)', () => {
|
||||
/**
|
||||
* The Crate made into "Inner", then the Watcher and that placement into "Outer", placed a second
|
||||
* time. A Lookout outside points at the Crate as well.
|
||||
*/
|
||||
function nested() {
|
||||
const f = watching();
|
||||
const lookout = place(f, f.behavior.id, { x: 0, y: 0 });
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, lookout, 'target', objectReference(f.crate)));
|
||||
const inner = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||||
f.stack.execute(inner);
|
||||
const innerPlaced = inner.objectId;
|
||||
const outer = new CreateContainerCommand(f.sceneId, [f.watcherId, innerPlaced]);
|
||||
f.stack.execute(outer);
|
||||
const second = new PlaceAssetCommand(f.sceneId, outer.assetId, { x: 500, y: 0 });
|
||||
f.stack.execute(second);
|
||||
const asset = findAsset(f.project, outer.assetId) as ContainerAsset;
|
||||
const tWatcher = asset.objects.find((o) => o.assetId === f.behavior.id)!.id;
|
||||
const tInner = asset.objects.find((o) => o.assetId === inner.assetId)!.id;
|
||||
const tCrate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!.id;
|
||||
/** The Crate as Outer's own view names it. */
|
||||
const crate = instanceChildId(tInner, tCrate);
|
||||
/** The Crate's copy in a placement of Outer. */
|
||||
const copyIn = (placed: string) => instanceChildId(placed, `${tInner}/${tCrate}`);
|
||||
const placements = [outer.objectId, second.objectId] as const;
|
||||
return { ...f, lookout, inner, innerPlaced, asset, tWatcher, crate, copyIn, placements };
|
||||
}
|
||||
|
||||
it('is picked in the container as its view names it, and every placement reaches its own copy', () => {
|
||||
const f = nested();
|
||||
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', NO_OBJECT));
|
||||
const choice = objectChoices(f.project, f.asset, f.tWatcher).find((c) => c.label === 'Crate')!;
|
||||
expect(choice).toMatchObject({ ref: f.crate, objectId: f.crate });
|
||||
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', objectReference(choice.ref)));
|
||||
const run = startWorld(f);
|
||||
for (const placed of f.placements) {
|
||||
const watcher = run.world.findByObjectId(instanceChildId(placed, f.tWatcher))!;
|
||||
expect(run.world.findReferenced(watcher, f.crate)?.objectId).toBe(f.copyIn(placed));
|
||||
}
|
||||
});
|
||||
|
||||
it('is shown as the Crate on a placement, and offered there with the same name', () => {
|
||||
const f = nested();
|
||||
const [, placed] = f.placements;
|
||||
const choices = objectChoices(f.project, f.sceneOf(), instanceChildId(placed, f.tWatcher));
|
||||
expect(choices.find((c) => c.ref === f.crate)).toMatchObject({
|
||||
objectId: f.copyIn(placed),
|
||||
label: 'Crate',
|
||||
});
|
||||
});
|
||||
|
||||
it('follows its object when the placement around it becomes container content', () => {
|
||||
const f = nested();
|
||||
expect(f.asset.objects.find((o) => o.id === f.tWatcher)!.overrides.target).toEqual({ $object: f.crate });
|
||||
expect(f.object(f.lookout).overrides.target).toEqual({ $object: f.copyIn(f.placements[0]) });
|
||||
const run = startWorld(f);
|
||||
const lookout = run.world.findByObjectId(f.lookout)!;
|
||||
expect(run.world.findReferenced(lookout, f.copyIn(f.placements[0]))?.objectId).toBe(
|
||||
f.copyIn(f.placements[0]),
|
||||
);
|
||||
});
|
||||
|
||||
it('is spelled out as the copy it names when the container is unpacked', () => {
|
||||
const f = nested();
|
||||
const [, placed] = f.placements;
|
||||
f.stack.execute(new UnpackContainerCommand(f.sceneId, placed));
|
||||
expect(f.object(instanceChildId(placed, f.tWatcher)).overrides.target).toEqual({
|
||||
$object: f.copyIn(placed),
|
||||
});
|
||||
});
|
||||
|
||||
it('reaches content nested deeper as well, from content of a placement', () => {
|
||||
expect(referenceCandidates('r~q/x', 'p~z')).toEqual(['r~q/p/z', 'p~z']);
|
||||
expect(referenceCandidates('r~x', 'p~z/y')).toEqual(['r~p/z/y', 'p~z/y']);
|
||||
});
|
||||
|
||||
it('points a pasted copy at the content of the copied placement', () => {
|
||||
const f = watching();
|
||||
const inner = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||||
f.stack.execute(inner);
|
||||
const tCrate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!.id;
|
||||
const both = copyObjects(f.project, f.sceneOf(), [f.watcherId, inner.objectId])!;
|
||||
const paste = new PasteObjectsCommand(f.sceneId, both, {}, { x: 5, y: 0 });
|
||||
f.stack.execute(paste);
|
||||
const copies = paste.rootIds.map((id) => f.object(id));
|
||||
const watcherCopy = copies.find((o) => o.assetId === f.behavior.id)!;
|
||||
const innerCopy = copies.find((o) => o.assetId === inner.assetId)!;
|
||||
expect(watcherCopy.overrides.target).toEqual({ $object: instanceChildId(innerCopy.id, tCrate) });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting an asset (#286)', () => {
|
||||
it('empties a reference to an object placed from it, and undo brings both back', () => {
|
||||
const f = watching();
|
||||
f.stack.execute(new DeleteAssetCommand(f.spriteId));
|
||||
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
|
||||
f.stack.undo();
|
||||
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
|
||||
expect(f.object(f.crate).name).toBe('Crate');
|
||||
});
|
||||
|
||||
it('empties a reference into a placement of a deleted container', () => {
|
||||
const f = watching();
|
||||
const create = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||||
f.stack.execute(create);
|
||||
const named = f.object(f.watcherId).overrides.target;
|
||||
expect(referencedId(named)).toContain(create.objectId);
|
||||
f.stack.execute(new DeleteAssetCommand(create.assetId));
|
||||
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
|
||||
f.stack.undo();
|
||||
expect(f.object(f.watcherId).overrides.target).toEqual(named);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user