- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry separated, writeTyped() replaces duplicated typed writes - Comments: one style (JSDoc on non-obvious exports, // for decisions and traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars - Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts, commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe, Input.axis, setMode, titleText, World.worldPosition, unused ctor params) - Editor: Inspector split into inspector/ sections, App.ts into Editor.ts + debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared "Add collider" action, unified type labels/swatches - UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers, Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes - Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections - Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
71 lines
3.8 KiB
JavaScript
71 lines
3.8 KiB
JavaScript
import { same } from '../lib/harness.mjs';
|
||
|
||
/**
|
||
* Hierarchy / parenting. Gives both crates unique names (later steps locate them by these),
|
||
* then parents by drag, by the inspector menu and unparents from the context menu – the world
|
||
* position is kept every time.
|
||
*/
|
||
export async function parenting(t) {
|
||
const { check, sleep, json, shot, click, clickText, drag, locate, typeText, press, undo, mutate, select, object, entity, stop } = t;
|
||
|
||
// 14. 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 mutate(`
|
||
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)`);
|
||
const redRow = await locate('Red crate');
|
||
const blueRow = await locate('Blue crate');
|
||
await drag(blueRow.x, blueRow.y, redRow.x, redRow.y);
|
||
await sleep(100);
|
||
let o2 = await object('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 = await json('(__gm.hierarchyExpandAll(), __gm.hierarchyOrder())');
|
||
// o1's subtree: o2 (list order before o4) with its collider child, then o4, then o1's collider.
|
||
const o1At = order.indexOf('o1');
|
||
check('children are listed directly below their parent', same(order.slice(o1At + 1, o1At + 5), ['o2', 'oc2', 'o4', 'oc1']), 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 press('a', { ctrl: true });
|
||
await typeText('-50');
|
||
await press('Enter');
|
||
await sleep(200);
|
||
o2 = await object('o2');
|
||
const parent = await object('o2').then((o) => object(o.parentId));
|
||
const worldOfO2 = { x: o2.position.x + parent.position.x, y: o2.position.y + parent.position.y };
|
||
check('moving the parent moves the child in world space (local unchanged)', o2.position.x === 270 && same(worldOfO2, { x: 220, y: -80 }), `${JSON.stringify(o2.position)} world=${JSON.stringify(worldOfO2)}`);
|
||
|
||
// 14b. Play: entities mirror the hierarchy; undo restores parent and position.
|
||
await select(null);
|
||
await press('Enter', { ctrl: true });
|
||
await sleep(300);
|
||
const e2 = await entity('o2');
|
||
check('play instantiates the parent link and world positions', e2 && e2.parent === 'o1' && e2.worldX === 220 && e2.x === 270, JSON.stringify(e2));
|
||
await stop();
|
||
await undo(); // move
|
||
await undo(); // parent
|
||
o2 = await object('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.
|
||
await clickText('Blue crate');
|
||
await clickText('None ▾');
|
||
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(100);
|
||
o2 = await object('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 clickText('Unparent (move to top level)');
|
||
await sleep(100);
|
||
o2 = await object('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));
|
||
}
|