Multiselection and container assets: create container from selection, template instances
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
91ee2feadd
commit
0b78382bcc
26 files changed
+897
-114
No files matched your search
+105
@@ -664,6 +664,111 @@ try {
|
||||
await sleep(200);
|
||||
await shot('22-trigger-collider');
|
||||
|
||||
// 16. Multi-selection. Reset both crates to known spots, then Ctrl-click the second row in
|
||||
// the hierarchy: both are selected, the inspector shows the multi view.
|
||||
await evaluate(`(() => {
|
||||
const objs = __gm.state.activeScene.objects;
|
||||
const o1 = objs.find(o => o.id === 'o1'); const o2 = objs.find(o => o.id === 'o2');
|
||||
o1.position = { x: -150, y: 0, z: 0 }; o1.parentId = '';
|
||||
o2.position = { x: 120, y: -80, z: 0 }; o2.parentId = '';
|
||||
objs.find(o => o.id === 'oc2').overrides = { width: 140 };
|
||||
__gm.state.select(null);
|
||||
__gm.state.preview();
|
||||
})()`);
|
||||
await sleep(200);
|
||||
const clickMod = async (x, y, modifiers) => {
|
||||
await mouse('mouseMoved', x, y);
|
||||
await mouse('mousePressed', x, y, 'left', { modifiers });
|
||||
await mouse('mouseReleased', x, y, 'left', { modifiers });
|
||||
await sleep(150);
|
||||
};
|
||||
await clickText('Red crate');
|
||||
const blueRow3 = await locate('Blue crate');
|
||||
await clickMod(blueRow3.x, blueRow3.y, 2); // Ctrl
|
||||
s = JSON.parse(await stateJson());
|
||||
check('Ctrl-click in the hierarchy adds to the selection', s.selection?.kind === 'object' && JSON.stringify(s.selection.objectIds) === '["o1","o2"]' && s.selection.objectId === 'o2', JSON.stringify(s.selection));
|
||||
check('inspector shows the multi-selection', Boolean(await evaluate(`__gm.locate('2 OBJECTS SELECTED')`)));
|
||||
await shot('23-multi-selected');
|
||||
await clickMod(blueRow3.x, blueRow3.y, 2);
|
||||
s = JSON.parse(await stateJson());
|
||||
check('Ctrl-click again removes the object from the selection', s.selection?.kind === 'object' && JSON.stringify(s.selection.objectIds) === '["o1"]', JSON.stringify(s.selection));
|
||||
await clickMod(blueRow3.x, blueRow3.y, 2);
|
||||
|
||||
// 16a. Create container: both crates (with their colliders and the Mover) become the content
|
||||
// of a new container asset; the scene gets one container object at the selection's
|
||||
// centre (-15,-40), the crates hang below it with recomputed local positions, world
|
||||
// positions are unchanged, and the container is selected.
|
||||
const assetsBeforeContainer = await evaluate('__gm.state.project.assets.length');
|
||||
const objectsBeforeContainer = await evaluate('__gm.state.activeScene.objects.length');
|
||||
await clickText('Create container');
|
||||
await sleep(250);
|
||||
s = JSON.parse(await stateJson());
|
||||
const containerObj = s.selection?.kind === 'object' ? JSON.parse(await objectJson(s.selection.objectId)) : null;
|
||||
const containerAsset = containerObj ? JSON.parse(await evaluate(`JSON.stringify(__gm.state.project.assets.find(a => a.id === '${containerObj.assetId}'))`)) : null;
|
||||
check('Create container adds one container asset and one placed container, selected', containerAsset?.type === 'container' && s.selection.objectIds.length === 1 && (await evaluate('__gm.state.project.assets.length')) === assetsBeforeContainer + 1 && (await evaluate('__gm.state.activeScene.objects.length')) === objectsBeforeContainer + 1, JSON.stringify({ containerObj, type: containerAsset?.type }));
|
||||
check('the container sits at the centre of the selection', containerObj && containerObj.parentId === '' && containerObj.position.x === -15 && containerObj.position.y === -40 && containerObj.position.z === 0, JSON.stringify(containerObj?.position));
|
||||
let o1c = JSON.parse(await objectJson('o1'));
|
||||
const o2c = JSON.parse(await objectJson('o2'));
|
||||
check('the crates are re-parented below the container with local positions relative to its centre', o1c.parentId === containerObj?.id && o1c.position.x === -135 && o1c.position.y === 40 && o2c.parentId === containerObj?.id && o2c.position.x === 135 && o2c.position.y === -40, JSON.stringify({ o1: o1c.position, o2: o2c.position }));
|
||||
check('the template holds the whole subtree (2 crates, 2 colliders, the Mover)', containerAsset?.objects.length === 5 && containerAsset.objects.filter((t) => t.parentId === '').length === 2 && o1c.templateId && containerAsset.objects.some((t) => t.id === o1c.templateId), JSON.stringify(containerAsset?.objects.map((t) => [t.assetId, t.parentId === '' ? 'root' : 'child'])));
|
||||
const orderC = JSON.parse(await evaluate('JSON.stringify(__gm.hierarchyOrder())'));
|
||||
const cAt = orderC.indexOf(containerObj?.id);
|
||||
check('hierarchy lists the container with its content indented below', cAt >= 0 && JSON.stringify(orderC.slice(cAt)) === JSON.stringify([containerObj.id, 'o1', 'o4', 'oc1', 'o2', 'oc2']), JSON.stringify(orderC));
|
||||
check('container symbol is drawn in the overlay layer', containerObj && (await evaluate(`__gm.layerOf('${containerObj.id}')`)) === 'overlay');
|
||||
await shot('24-container-created');
|
||||
|
||||
// 16b. Dragging the container symbol in the viewport moves the whole group; undo restores it.
|
||||
const cScreen = JSON.parse(await evaluate(`JSON.stringify(__gm.objectScreen('${containerObj.id}'))`));
|
||||
await drag(cScreen.x, cScreen.y, cScreen.x + 100, cScreen.y);
|
||||
const containerMoved = JSON.parse(await objectJson(containerObj.id));
|
||||
o1c = JSON.parse(await objectJson('o1'));
|
||||
check('dragging the container moves the group (children keep their local position)', containerMoved.position.x === 85 && o1c.position.x === -135, JSON.stringify({ container: containerMoved.position, o1: o1c.position }));
|
||||
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 });
|
||||
await sleep(150);
|
||||
check('undo moves the container back', JSON.parse(await objectJson(containerObj.id)).position.x === -15);
|
||||
|
||||
// 16c. Rubber band on empty space around the red crate (world x −230…−90, y −50…50): picks
|
||||
// the objects whose position lies inside – the crate and its collider, not the Mover
|
||||
// (at y = 110), not the camera (at the origin).
|
||||
await drag(center.x - 230, center.y - 50, center.x - 90, center.y + 50);
|
||||
s = JSON.parse(await stateJson());
|
||||
check('rubber-band selection picks the objects inside it', s.selection?.kind === 'object' && JSON.stringify([...s.selection.objectIds].sort()) === '["o1","oc1"]', JSON.stringify(s.selection));
|
||||
await shot('25-marquee');
|
||||
|
||||
// 16d. Dragging one of several selected objects moves all of them as one undo step.
|
||||
const redScreen = JSON.parse(await evaluate(`JSON.stringify(__gm.objectScreen('o1'))`));
|
||||
await evaluate(`__gm.state.select({ kind: 'object', sceneId: 'scene1', objectId: 'o2', objectIds: ['o1', 'o2'] })`);
|
||||
await drag(redScreen.x, redScreen.y + 20, redScreen.x + 50, redScreen.y + 20);
|
||||
o1c = JSON.parse(await objectJson('o1'));
|
||||
const o2d = JSON.parse(await objectJson('o2'));
|
||||
check('dragging a multi-selection moves every selected object', o1c.position.x === -85 && o2d.position.x === 185, JSON.stringify({ o1: o1c.position, o2: o2d.position }));
|
||||
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 });
|
||||
await sleep(150);
|
||||
o1c = JSON.parse(await objectJson('o1'));
|
||||
check('one undo restores both', o1c.position.x === -135 && JSON.parse(await objectJson('o2')).position.x === 135);
|
||||
|
||||
// 16e. Delete with a multi-selection removes all of them (with subtrees) as one step.
|
||||
const objectsBeforeDelete = await evaluate('__gm.state.activeScene.objects.length');
|
||||
await evaluate(`__gm.state.select({ kind: 'object', sceneId: 'scene1', objectId: 'o2', objectIds: ['o1', 'o2'] })`);
|
||||
await keyPress('Delete', 'Delete', { windowsVirtualKeyCode: 46 });
|
||||
await sleep(150);
|
||||
check('Delete removes the whole multi-selection with its subtrees', (await evaluate('__gm.state.activeScene.objects.length')) === objectsBeforeDelete - 5);
|
||||
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 });
|
||||
await sleep(150);
|
||||
check('undo restores all of them', (await evaluate('__gm.state.activeScene.objects.length')) === objectsBeforeDelete);
|
||||
|
||||
// 16f. Placing the container asset again from the asset panel creates a second instance:
|
||||
// the root plus 5 content objects with fresh ids, the crate copy at the same offset.
|
||||
await clickText(containerAsset.name, 'right');
|
||||
await sleep(150);
|
||||
await clickText('Place in scene');
|
||||
await sleep(250);
|
||||
const afterPlace = JSON.parse(await evaluate('JSON.stringify(__gm.state.activeScene.objects)'));
|
||||
const secondRoot = afterPlace.find((o) => o.assetId === containerAsset.id && o.id !== containerObj.id);
|
||||
const secondCrate = secondRoot && afterPlace.find((o) => o.parentId === secondRoot.id && o.templateId === o1c.templateId);
|
||||
check('placing the container asset instantiates its content below a new container object', afterPlace.length === objectsBeforeDelete + 6 && secondCrate && secondCrate.position.x === -135 && secondCrate.position.y === 40, JSON.stringify({ count: afterPlace.length, secondCrate }));
|
||||
await shot('26-container-placed');
|
||||
|
||||
check('no exceptions during run', !consoleLog.some((l) => l.startsWith('EXCEPTION')), consoleLog.filter((l) => l.startsWith('EXCEPTION')).slice(0, 3).join(' | '));
|
||||
ws.close();
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user