Scene hierarchy: parentId with local positions, tree panel, reparenting and Parent node
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
551a50ffc9
commit
26d5edc496
22 files changed
+729
-166
No files matched your search
+78
-2
@@ -85,8 +85,8 @@ try {
|
||||
});
|
||||
|
||||
const evaluate = async (expression) => (await cdp.send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true })).result.value;
|
||||
const locate = async (text) => {
|
||||
const p = await evaluate(`window.__gm && window.__gm.locate(${JSON.stringify(text)})`);
|
||||
const locate = async (text, last = false) => {
|
||||
const p = await evaluate(`window.__gm && window.__gm.locate(${JSON.stringify(text)}, ${last})`);
|
||||
if (!p) throw new Error(`widget with text "${text}" not found`);
|
||||
return p;
|
||||
};
|
||||
@@ -426,9 +426,17 @@ try {
|
||||
|
||||
// 13e. Play always starts on the Scene tab; switching back to Graph while playing shows
|
||||
// the live highlight: hold ArrowRight → the exec edge is hot.
|
||||
const fpsEdit = await evaluate('__gm.fps()');
|
||||
check('fps readout is measured in edit mode', typeof fpsEdit === 'number' && fpsEdit > 0, `fps=${fpsEdit}`);
|
||||
// Single evaluate: the readout refreshes twice a second, so read value and label together.
|
||||
const fpsLabel = await evaluate(`!!__gm.locate(__gm.fps() + ' fps')`);
|
||||
check('fps label is visible in the toolbar', fpsLabel === true);
|
||||
await clickText('▶ Play');
|
||||
await sleep(200);
|
||||
check('play switches to the scene tab', (await evaluate('__gm.tab()')) === 'scene');
|
||||
await sleep(600);
|
||||
const fpsPlay = await evaluate('__gm.fps()');
|
||||
check('fps readout keeps running in play mode', typeof fpsPlay === 'number' && fpsPlay > 0, `fps=${fpsPlay}`);
|
||||
await evaluate(`__gm.showTab('graph')`);
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 });
|
||||
@@ -465,6 +473,74 @@ try {
|
||||
check('graph editor rebinds when a behavior is selected again', (await evaluate(`__gm.graph.current()`)) === 'mover');
|
||||
await evaluate(`__gm.showTab('scene')`);
|
||||
|
||||
// 14. Hierarchy / parenting. Give both crates unique names, then drag the blue crate's row
|
||||
// onto the red crate's row: it becomes a child, keeps its world position (local becomes
|
||||
// the difference) and is listed right below its parent.
|
||||
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.name = 'Red crate'; o1.position = { x: -150, y: 0, z: 0 };
|
||||
o2.name = 'Blue crate'; o2.position = { x: 120, y: -80, z: 0 }; o2.parentId = '';
|
||||
__gm.state.select(null);
|
||||
__gm.state.preview();
|
||||
})()`);
|
||||
await sleep(200);
|
||||
const redRow = await locate('Red crate');
|
||||
const blueRow = await locate('Blue crate');
|
||||
await drag(blueRow.x, blueRow.y, redRow.x, redRow.y);
|
||||
await sleep(200);
|
||||
const objectJson = (id) => evaluate(`JSON.stringify(__gm.state.activeScene.objects.find(o => o.id === '${id}'))`);
|
||||
let o2 = JSON.parse(await objectJson('o2'));
|
||||
check('dropping a row onto another parents it, keeping the world position', o2.parentId === 'o1' && o2.position.x === 270 && o2.position.y === -80, JSON.stringify(o2));
|
||||
const order = JSON.parse(await evaluate(`JSON.stringify(__gm.hierarchyOrder())`));
|
||||
check('children are listed directly below their parent', order.indexOf('o2') === order.indexOf('o1') + 1 && order.indexOf('o4') === order.indexOf('o1') + 2, JSON.stringify(order));
|
||||
await shot('20-parented');
|
||||
|
||||
// 14a. Moving the parent (X field of the red crate) drags the child along in world space.
|
||||
await clickText('Red crate');
|
||||
const xField = await locate('X');
|
||||
await click(xField.x + 160, xField.y);
|
||||
await keyPress('a', 'KeyA', { modifiers: 2, windowsVirtualKeyCode: 65 });
|
||||
await typeText('-50');
|
||||
await keyPress('Enter', 'Enter', { windowsVirtualKeyCode: 13 });
|
||||
await sleep(200);
|
||||
o2 = JSON.parse(await objectJson('o2'));
|
||||
const worldOfO2 = await evaluate(`(() => { const s = __gm.state.activeScene; const child = s.objects.find(x => x.id === 'o2'); const parent = s.objects.find(x => x.id === child.parentId); return JSON.stringify({ x: child.position.x + parent.position.x, y: child.position.y + parent.position.y }); })()`);
|
||||
check('moving the parent moves the child in world space (local unchanged)', o2.position.x === 270 && worldOfO2 === '{"x":220,"y":-80}', `${JSON.stringify(o2.position)} world=${worldOfO2}`);
|
||||
|
||||
// 14b. Play: entities mirror the hierarchy; undo restores parent and position.
|
||||
await evaluate(`__gm.state.select(null)`);
|
||||
await keyPress('Enter', 'Enter', { modifiers: 2, windowsVirtualKeyCode: 13 });
|
||||
await sleep(300);
|
||||
const playWorld = await evaluate('JSON.stringify(__gm.world())');
|
||||
const e2 = JSON.parse(playWorld).entities.find((e) => e.objectId === 'o2');
|
||||
check('play instantiates the parent link and world positions', e2 && e2.parent === 'o1' && e2.worldX === 220 && e2.x === 270, JSON.stringify(e2));
|
||||
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
|
||||
await sleep(200);
|
||||
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 }); // undo move
|
||||
await keyPress('z', 'KeyZ', { modifiers: 2, windowsVirtualKeyCode: 90 }); // undo parent
|
||||
await sleep(200);
|
||||
o2 = JSON.parse(await objectJson('o2'));
|
||||
check('undo restores top level and the original local position', o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80, JSON.stringify(o2));
|
||||
|
||||
// 14c. Parent via the inspector menu, then "Unparent" from the hierarchy context menu:
|
||||
// the world position is kept both ways.
|
||||
await clickText('Blue crate');
|
||||
await clickText('None ▾');
|
||||
await sleep(150);
|
||||
const menuEntry = await locate('Red crate', true); // the popup is drawn last; the hierarchy row has the same text
|
||||
await click(menuEntry.x, menuEntry.y);
|
||||
await sleep(200);
|
||||
o2 = JSON.parse(await objectJson('o2'));
|
||||
check('inspector Parent menu parents the object', o2.parentId === 'o1' && o2.position.x === 270, JSON.stringify(o2));
|
||||
const blueRow2 = await locate('Blue crate');
|
||||
await click(blueRow2.x, blueRow2.y, 'right');
|
||||
await sleep(150);
|
||||
await clickText('Unparent (move to top level)');
|
||||
await sleep(200);
|
||||
o2 = JSON.parse(await objectJson('o2'));
|
||||
check('Unparent moves the object to the top level, keeping its world position', o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80, JSON.stringify(o2));
|
||||
|
||||
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) {
|
||||
|
||||
+5
-4
@@ -45,10 +45,11 @@ const project = {
|
||||
name: 'Level 1',
|
||||
props: { backgroundColor: 0x22252c },
|
||||
objects: [
|
||||
{ id: 'o1', assetId: 'crate', name: '', position: { x: -150, y: 0, z: 0 }, overrides: {}, ownerId: '' },
|
||||
{ id: 'o2', assetId: 'crate', name: 'Blue crate', position: { x: 120, y: -80, z: 0 }, overrides: { width: 140, color: 0x4488ff }, ownerId: '' },
|
||||
{ id: 'o3', assetId: 'cam', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, ownerId: '' },
|
||||
{ id: 'o4', assetId: 'mover', name: '', position: { x: -150, y: 110, z: 0 }, overrides: {}, ownerId: 'o1' },
|
||||
{ id: 'o1', assetId: 'crate', name: '', position: { x: -150, y: 0, z: 0 }, overrides: {}, parentId: '' },
|
||||
{ id: 'o2', assetId: 'crate', name: 'Blue crate', position: { x: 120, y: -80, z: 0 }, overrides: { width: 140, color: 0x4488ff }, parentId: '' },
|
||||
{ id: 'o3', assetId: 'cam', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: '' },
|
||||
// The mover is parented to the red crate (its owner); its position is local to it.
|
||||
{ id: 'o4', assetId: 'mover', name: '', position: { x: 0, y: 110, z: 0 }, overrides: {}, parentId: 'o1' },
|
||||
],
|
||||
},
|
||||
{ id: 'crate', type: 'sprite', name: 'Crate', props: { width: 100, height: 60, color: 0xe0553f } },
|
||||
|
||||
Reference in new issue
Block a user