Place assets by dragging them from the panel into the view
The context menu entry is gone; dragging replaces it entirely. The drop target is the view under the pointer, not the active tab, so the same gesture works for scenes and for container templates. While dragging, an outline shows the footprint the object will have plus a crosshair on its origin, turning red when the drop would be refused. Preview and command share one function (placementRejection), so the outline cannot promise something the drop then rejects. A left-press used to select the list row immediately. Since the container tab follows the selection, it switched to the very container being dragged and every drop would have nested it inside itself. The list now selects on release without a drag: picking something up is not the same as choosing it.
This commit is contained in:
1 parent
14562cf157
commit
75d377af39
18 files changed
+545
-77
No files matched your search
+67
-7
@@ -1,15 +1,75 @@
|
||||
/** The asset panel: placing from the context menu, creating through "+", and resetting an asset value. */
|
||||
/** The asset panel: placing by dragging into the view, creating through "+", and resetting an asset value. */
|
||||
export async function assets(t) {
|
||||
const { check, sleep, evaluate, json, shot, click, clickText, undo, select, objects, asset } = t;
|
||||
const {
|
||||
check,
|
||||
sleep,
|
||||
evaluate,
|
||||
json,
|
||||
shot,
|
||||
click,
|
||||
clickText,
|
||||
mouse,
|
||||
press,
|
||||
redo,
|
||||
undo,
|
||||
select,
|
||||
objects,
|
||||
asset,
|
||||
} = t;
|
||||
|
||||
// 8. Context menu on the Crate asset → menu shows "Place in scene".
|
||||
// 8. Context menu on the Crate asset. Placing left the menu when it became a drag.
|
||||
await clickText('Crate', 'right');
|
||||
check('asset context menu opens', Boolean(await evaluate(`__gm.locate('Place in scene')`)));
|
||||
check('asset context menu opens', Boolean(await evaluate(`__gm.locate('Delete…')`)));
|
||||
check('placing is no longer a menu item', !(await evaluate(`__gm.locate('Place in scene')`)));
|
||||
await shot('11-context-menu');
|
||||
await clickText('Place in scene');
|
||||
await sleep(100);
|
||||
await press('Escape');
|
||||
await sleep(150);
|
||||
|
||||
// 8b. Drag the Crate asset from the panel into the scene view. Driven step by step rather than
|
||||
// with `drag()`, because the preview only exists while the button is still down.
|
||||
const from = await json("__gm.assetRow('crate')");
|
||||
const centre = await json('__gm.viewportCenter()');
|
||||
const to = { x: centre.x + 70, y: centre.y - 50 };
|
||||
await mouse('mouseMoved', from.x, from.y);
|
||||
await mouse('mousePressed', from.x, from.y);
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const x = from.x + ((to.x - from.x) * i) / 8;
|
||||
const y = from.y + ((to.y - from.y) * i) / 8;
|
||||
await mouse('mouseMoved', x, y, 'left', { buttons: 1 });
|
||||
await sleep(16);
|
||||
}
|
||||
const preview = await json('__gm.dropPreview()');
|
||||
check(
|
||||
'dragging an asset over the view previews where it would land',
|
||||
Boolean(preview) && preview.assetId === 'crate' && preview.rejection === '',
|
||||
JSON.stringify(preview),
|
||||
);
|
||||
await shot('11b-drop-preview');
|
||||
await mouse('mouseReleased', to.x, to.y);
|
||||
await sleep(200);
|
||||
|
||||
const count = (await objects()).length;
|
||||
check('placing from the menu adds an object', count === 8, `objects=${count}`);
|
||||
check('dropping the asset adds an object', count === 8, `objects=${count}`);
|
||||
const placed = await json(
|
||||
'(() => { const id = __gm.state.selectedObjectIds[0]; return __gm.sceneObjects().find(o => o.id === id) ?? null; })()',
|
||||
);
|
||||
// The preview is a promise: whatever it drew has to be where the object then sits, or the
|
||||
// outline was decoration rather than a prediction.
|
||||
check(
|
||||
'the object lands where the preview promised, and is selected',
|
||||
Boolean(placed) &&
|
||||
placed.assetId === 'crate' &&
|
||||
placed.position.x === preview.x &&
|
||||
placed.position.y === preview.y,
|
||||
JSON.stringify({ placed: placed && placed.position, preview }),
|
||||
);
|
||||
check('the preview is gone once the asset is placed', (await json('__gm.dropPreview()')) === null);
|
||||
await undo();
|
||||
const afterUndo = (await objects()).length;
|
||||
check('one undo takes the dropped object back out', afterUndo === 7, `objects=${afterUndo}`);
|
||||
await redo();
|
||||
await sleep(150);
|
||||
check('redo puts it back', (await objects()).length === 8);
|
||||
|
||||
// 9. Create a new asset through the + menu.
|
||||
await clickText('+');
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function containers(t) {
|
||||
click,
|
||||
clickText,
|
||||
drag,
|
||||
dragAsset,
|
||||
locate,
|
||||
press,
|
||||
typeText,
|
||||
@@ -220,9 +221,8 @@ export async function containers(t) {
|
||||
|
||||
// 16f. Placing the container asset again adds exactly ONE stored object – and five more that
|
||||
// the editor lists. Against the old copying model the stored count would jump by six.
|
||||
await clickText(containerAsset.name, 'right');
|
||||
await clickText('Place in scene');
|
||||
await sleep(150);
|
||||
const spot = await json('__gm.viewportCenter()');
|
||||
await dragAsset(containerAsset.id, spot.x + 90, spot.y + 60);
|
||||
const instanceB = (await storedObjects()).find(
|
||||
(o) => o.assetId === containerAsset.id && o.id !== instanceA.id,
|
||||
);
|
||||
@@ -353,9 +353,7 @@ export async function containers(t) {
|
||||
|
||||
// 16j. Unpack: the content is written into the scene as ordinary objects under the very same
|
||||
// ids, the instance stops following the template – and now its structure *can* be changed.
|
||||
await clickText(containerAsset.name, 'right');
|
||||
await clickText('Place in scene');
|
||||
await sleep(150);
|
||||
await dragAsset(containerAsset.id, spot.x - 90, spot.y + 60);
|
||||
const instanceC = (await storedObjects()).find(
|
||||
(o) => o.assetId === containerAsset.id && o.id !== instanceA.id && o.id !== instanceB.id,
|
||||
);
|
||||
|
||||
+49
-29
@@ -23,10 +23,11 @@ export async function containerTab(t) {
|
||||
click,
|
||||
clickText,
|
||||
drag,
|
||||
dragAsset,
|
||||
mouse,
|
||||
press,
|
||||
undo,
|
||||
state,
|
||||
asset,
|
||||
object,
|
||||
objects,
|
||||
storedObjects,
|
||||
@@ -135,22 +136,15 @@ export async function containerTab(t) {
|
||||
);
|
||||
await setDeviation(`delete edits['${root.id}']`);
|
||||
|
||||
// 22d. Placing goes through the Assets panel, whose entry follows the visible tab: it adds one
|
||||
// object to the template, not to the scene.
|
||||
const crateName = (await asset('crate')).name;
|
||||
// 22d. An asset dragged from the panel lands in the view it is dropped on: dropping on this
|
||||
// tab adds one object to the template, not to the scene.
|
||||
// What the scene *stores* – that must not move. What it *lists* has to grow by one per placed
|
||||
// instance instead, since every one of them resolves the new template object too.
|
||||
const sceneStored = (await storedObjects()).length;
|
||||
const sceneListed = (await objects()).length;
|
||||
const instanceCount = Object.keys(await instanceXs()).length;
|
||||
await clickText(crateName, 'right');
|
||||
check(
|
||||
'the asset menu offers Place in container while the tab is open',
|
||||
Boolean(await evaluate("__gm.locate('Place in container')")) &&
|
||||
!(await evaluate("__gm.locate('Place in scene')")),
|
||||
);
|
||||
await clickText('Place in container');
|
||||
await sleep(300);
|
||||
const canvas = await json('__gm.container.center()');
|
||||
await dragAsset('crate', canvas.x + 40, canvas.y + 40);
|
||||
const grown = await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`);
|
||||
s = await state();
|
||||
check(
|
||||
@@ -230,13 +224,6 @@ export async function containerTab(t) {
|
||||
(await json('__gm.hierarchyOrder()')).includes('otex'),
|
||||
JSON.stringify(await json('__gm.hierarchyOrder()')),
|
||||
);
|
||||
await clickText(crateName, 'right');
|
||||
check(
|
||||
'and back on the Scene tab the same entry reads Place in scene',
|
||||
Boolean(await evaluate("__gm.locate('Place in scene')")) &&
|
||||
!(await evaluate("__gm.locate('Place in container')")),
|
||||
);
|
||||
await press('Escape');
|
||||
|
||||
// 22h. Switching to another container is a plain asset selection – the same thing the header's
|
||||
// picker does. The hierarchy has to follow it, title and rows both.
|
||||
@@ -261,17 +248,51 @@ export async function containerTab(t) {
|
||||
);
|
||||
await shot('33-container-switched');
|
||||
|
||||
// 22i. Nesting: a right-click must not select, or picking the container to place would load it
|
||||
// into the tab first – you would then be placing it into itself.
|
||||
const nested = await json("__gm.state.project.assets.find(a => a.id === 'c2').objects");
|
||||
await clickText(NAME, 'right');
|
||||
// 22i2. Dragging the open container onto its own template would nest it in itself. The drag
|
||||
// has to say so while it hovers – an outline that only turns out to be a lie on release
|
||||
// is worse than none – and the drop has to change nothing.
|
||||
const canvas2 = await json('__gm.container.center()');
|
||||
const own = await json("__gm.assetRow('c2')");
|
||||
await mouse('mouseMoved', own.x, own.y);
|
||||
await mouse('mousePressed', own.x, own.y);
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
await mouse(
|
||||
'mouseMoved',
|
||||
own.x + ((canvas2.x - own.x) * i) / 8,
|
||||
own.y + ((canvas2.y - own.y) * i) / 8,
|
||||
'left',
|
||||
{
|
||||
buttons: 1,
|
||||
},
|
||||
);
|
||||
await sleep(16);
|
||||
}
|
||||
const refused = await json('__gm.container.dropPreview()');
|
||||
check(
|
||||
'right-clicking another container leaves the open one alone',
|
||||
'a container dragged over its own template is refused while it hovers',
|
||||
Boolean(refused) && /nests forever/.test(refused.rejection ?? ''),
|
||||
JSON.stringify(refused),
|
||||
);
|
||||
// Picking it up selects it; the tab must not follow that, or the target would swap mid-drag.
|
||||
check(
|
||||
'and the tab keeps showing the container being dropped into',
|
||||
(await json('__gm.container.current()')) === 'c2',
|
||||
JSON.stringify({ shown: await json('__gm.container.current()') }),
|
||||
);
|
||||
await clickText('Place in container');
|
||||
await sleep(300);
|
||||
await shot('33b-container-cycle-refused');
|
||||
const ownBefore = await json("__gm.state.project.assets.find(a => a.id === 'c2').objects");
|
||||
await mouse('mouseReleased', canvas2.x, canvas2.y);
|
||||
await sleep(200);
|
||||
const ownAfter = await json("__gm.state.project.assets.find(a => a.id === 'c2').objects");
|
||||
check(
|
||||
'releasing it changes nothing',
|
||||
ownAfter.length === ownBefore.length,
|
||||
JSON.stringify({ before: ownBefore.length, after: ownAfter.length }),
|
||||
);
|
||||
|
||||
// 22i. Nesting: another container, though, may go in – and the tab it lands in stays put.
|
||||
const nested = await json("__gm.state.project.assets.find(a => a.id === 'c2').objects");
|
||||
await dragAsset(containerId, canvas2.x - 40, canvas2.y + 40);
|
||||
const nestedAfter = await json("__gm.state.project.assets.find(a => a.id === 'c2').objects");
|
||||
check(
|
||||
'and placing it nests it inside that one',
|
||||
@@ -289,9 +310,8 @@ export async function containerTab(t) {
|
||||
await sleep(300);
|
||||
const storedBefore = (await storedObjects()).length;
|
||||
const listedBefore = (await objects()).length;
|
||||
await clickText('Spare box', 'right');
|
||||
await clickText('Place in scene');
|
||||
await sleep(300);
|
||||
const spot = await json('__gm.viewportCenter()');
|
||||
await dragAsset('c2', spot.x + 80, spot.y + 70);
|
||||
const outer = (await storedObjects()).find((o) => o.assetId === 'c2');
|
||||
const deep = innerTemplate.map((o) => instanceChild(outer.id, innerObject.id, o.id));
|
||||
const listedIds = (await objects()).map((o) => o.id);
|
||||
|
||||
@@ -174,6 +174,33 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
||||
await sleep(150);
|
||||
};
|
||||
|
||||
/**
|
||||
* Drags an asset out of the Assets panel onto a point in a view – the only way to place one
|
||||
* since the context menu lost its entry. By id, not by text: the container picker shows the
|
||||
* same name as the row. Unlike `drag()` it presses and moves in separate events on purpose,
|
||||
* because the panel turns the press into a drag only once it has travelled.
|
||||
*/
|
||||
const dragAsset = async (assetId, x, y, steps = 8) => {
|
||||
const from = await json(`__gm.assetRow(${JSON.stringify(assetId)})`);
|
||||
if (!from) throw new Error(`no asset row for "${assetId}" – is the panel scrolled past it?`);
|
||||
await mouse('mouseMoved', from.x, from.y);
|
||||
await mouse('mousePressed', from.x, from.y);
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
await mouse(
|
||||
'mouseMoved',
|
||||
from.x + ((x - from.x) * i) / steps,
|
||||
from.y + ((y - from.y) * i) / steps,
|
||||
'left',
|
||||
{
|
||||
buttons: 1,
|
||||
},
|
||||
);
|
||||
await sleep(16);
|
||||
}
|
||||
await mouse('mouseReleased', x, y);
|
||||
await sleep(200);
|
||||
};
|
||||
|
||||
// --- keyboard ------------------------------------------------------------------------------
|
||||
|
||||
const keyEvent = (type, name, modifiers) => {
|
||||
@@ -291,6 +318,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
||||
click,
|
||||
clickText,
|
||||
drag,
|
||||
dragAsset,
|
||||
press,
|
||||
holdKey,
|
||||
typeText,
|
||||
|
||||
Reference in new issue
Block a user