Pick the target of an object variable in the Scene View (#276)
Choosing the target of an object variable meant finding its name in a dropdown. With many objects, or several with the same sprite, the name alone does not say which one it is. The eyedropper lets you click the object where you see it instead. - Every object dropdown gets an eyedropper button next to it. This covers variables, struct fields and list entries, in the Inspector and in the Variables tab. - The button opens the Scene View or the container tab the object lives in. A strip above the view names the field and the current candidate and offers Take and Cancel. The cursor is a crosshair. - A click picks a candidate and writes nothing yet. The candidate gets its own cyan frame, the selection stays on the object with the variable. - Clicking follows the selection rule. `pickOn` in `src/viewport/pick.ts` uses `pressOn` and only counts objects the dropdown offers. A click on empty space keeps the candidate. - Enter or Take writes through the same handlers as the dropdown. That is one undo step, or none if nothing changes. - Esc, Cancel, a right click in the view or a second click on the button cancel without writing. So do a tab switch, another selection, undo, redo, play and loading a project. - After Take or Cancel the editor goes back to the tab the pick came from. - While the eyedropper runs, the view does not drag, marquee, paint, show handles or open its context menu. Panning and zooming stay. Found on the way: - Started from the Variables tab, the click missed the object. The Scene View is hidden while that tab is open, and the layout skips hidden children. The strip appeared then, the view moved down by 32 px but kept its old height. `Editor.display()` now lays the row out again after showing it. Tests (on melektaus-atx): - unit pick and object-pick: 27 passed, 13 new - gate: 196 files, 2844 tests, CHECK EXIT 0. Two more come from svg-icons, which checks every icon twice. - e2e 57: ALL PASSED, 12 checks, 5 new (57e to 57g) - Counter-checks work. Without the layout fix 57g fails. Picking objects the dropdown does not offer fails 2 tests. Writing on cancel fails 2 more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42647d309b
commit
ded71a92c9
24 files changed
+777
-34
No files matched your search
@@ -4,9 +4,14 @@
|
||||
* run reads the object itself. Removing that object drops the reference; undo brings it back. On the
|
||||
* behavior asset there is no scene, so the row still says "(null)".
|
||||
*
|
||||
* The eyedropper beside the dropdown (#276) picks in the scene view instead: over two stacked
|
||||
* objects a click takes the one in front and a second click the one behind, Enter writes it, Escape,
|
||||
* a right-click and a second press of the eyedropper leave it as it was. From a list item in a
|
||||
* Variables tab it goes to the scene view and comes back with Take.
|
||||
*
|
||||
* Everything it adds carries an `or57` id and goes again at the end.
|
||||
*/
|
||||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
|
||||
export async function objectReferences(t) {
|
||||
const { check, json, evaluate, mutate, click, press, typeText, sleep, shot, select, undo, stop } = t;
|
||||
@@ -120,6 +125,149 @@ export async function objectReferences(t) {
|
||||
JSON.stringify({ dropped, back }),
|
||||
);
|
||||
|
||||
// 57e. The eyedropper (#276): two objects stacked on a lane of their own, the target back to
|
||||
// None, and a list of one object item for 57g.
|
||||
const Y = LANES.eyedropper;
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
const scene = __gm.state.activeScene;
|
||||
${MAKE_OBJECT}
|
||||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||||
scene.objects.push(make('or57-back', sprite.id, '', ${Y}, 'Back 57'));
|
||||
scene.objects.push(make('or57-front', sprite.id, '', ${Y}, 'Front 57'));
|
||||
const aim = p.assets.find((a) => a.id === 'or57-aim');
|
||||
aim.variables.push({ key: 'crowd', label: 'crowd', kind: 'list', default: [] });
|
||||
aim.props.crowd = [];
|
||||
const obj = scene.objects.find((o) => o.id === 'or57-obj');
|
||||
obj.overrides.target = { $object: '' };
|
||||
obj.overrides.crowd = [{ $object: '' }];
|
||||
`);
|
||||
await select({ kind: 'object', holderId: sceneId, objectId: 'or57-obj' });
|
||||
await evaluate(`__gm.lookAt(0, ${Y})`);
|
||||
await sleep(200);
|
||||
const pick = () => json('__gm.objectPick()');
|
||||
const eyedropper = () => json("__gm.inspectorEyedropper('target')");
|
||||
const inInspector = async (text) => {
|
||||
const box = await json('__gm.inspectorRect()');
|
||||
const hits = (await json(`__gm.locateAll(${JSON.stringify(text)})`)) ?? [];
|
||||
return hits.some((p) => p.x >= box.x && p.x <= box.x + box.w && p.y >= box.y && p.y <= box.y + box.h);
|
||||
};
|
||||
const spot = await json("__gm.objectScreen('or57-front')");
|
||||
const stack = await json(`__gm.sceneStack(${spot.x}, ${spot.y})`);
|
||||
|
||||
await press_(await eyedropper(), 'the eyedropper of target');
|
||||
const started = await pick();
|
||||
const lit = (await eyedropper())?.lit;
|
||||
check(
|
||||
'the eyedropper starts in the scene view, lit, and the bar over it names the field',
|
||||
started.picking &&
|
||||
started.tab === 'scene' &&
|
||||
lit === true &&
|
||||
started.candidate === null &&
|
||||
started.strip?.text === 'Target for Aim 57 · target: click an object',
|
||||
JSON.stringify({ started, lit }),
|
||||
);
|
||||
|
||||
await click(spot.x, spot.y);
|
||||
const first = await pick();
|
||||
await click(spot.x, spot.y);
|
||||
const second = await pick();
|
||||
await shot('03-eyedropper');
|
||||
const empty = await json('__gm.emptySpot()');
|
||||
await click(empty.x, empty.y);
|
||||
const afterEmpty = await pick();
|
||||
check(
|
||||
'a click takes the object in front, a second one the object behind, empty space keeps it, nothing is written',
|
||||
JSON.stringify(stack) === JSON.stringify(['or57-front', 'or57-back']) &&
|
||||
first.candidate === 'or57-front' &&
|
||||
second.candidate === 'or57-back' &&
|
||||
second.outline === 'or57-back' &&
|
||||
second.strip?.text === 'Target for Aim 57 · target: Back 57' &&
|
||||
afterEmpty.candidate === 'or57-back' &&
|
||||
afterEmpty.picking &&
|
||||
JSON.stringify(await target()) === JSON.stringify({ $object: '' }),
|
||||
JSON.stringify({ stack, first, second, afterEmpty }),
|
||||
);
|
||||
|
||||
await press('Enter');
|
||||
const taken = await pick();
|
||||
const written = await target();
|
||||
const shown = await inInspector('Back 57');
|
||||
const litAfter = (await eyedropper())?.lit;
|
||||
await undo();
|
||||
const undone = await target();
|
||||
check(
|
||||
'Enter writes the candidate, the row shows it, the eyedropper goes off, one undo takes it back',
|
||||
!taken.picking &&
|
||||
taken.ended === 'take' &&
|
||||
taken.strip === null &&
|
||||
JSON.stringify(written) === JSON.stringify({ $object: 'or57-back' }) &&
|
||||
shown &&
|
||||
litAfter === false &&
|
||||
JSON.stringify(undone) === JSON.stringify({ $object: '' }),
|
||||
JSON.stringify({ taken, written, shown, litAfter, undone }),
|
||||
);
|
||||
|
||||
// 57f. Escape, a right-click in the view and a second press of the eyedropper leave it as it was.
|
||||
const cancels = [];
|
||||
for (const [how, leave] of [
|
||||
['Escape', () => press('Escape')],
|
||||
['right-click', () => click(spot.x, spot.y, 'right')],
|
||||
['eyedropper', async () => press_(await eyedropper(), 'the eyedropper, again')],
|
||||
]) {
|
||||
await press_(await eyedropper(), 'the eyedropper of target');
|
||||
await click(spot.x, spot.y);
|
||||
const before = (await pick()).candidate;
|
||||
await leave();
|
||||
const after = await pick();
|
||||
const menu = (await json('__gm.overlayTexts()')) ?? [];
|
||||
cancels.push({ how, before, picking: after.picking, ended: after.ended, menu, target: await target() });
|
||||
}
|
||||
check(
|
||||
'Escape, a right-click and a second press of the eyedropper end it without writing',
|
||||
cancels.every(
|
||||
(c) =>
|
||||
c.before === 'or57-front' &&
|
||||
!c.picking &&
|
||||
c.ended === 'cancel' &&
|
||||
c.menu.length === 0 &&
|
||||
JSON.stringify(c.target) === JSON.stringify({ $object: '' }),
|
||||
),
|
||||
JSON.stringify(cancels),
|
||||
);
|
||||
|
||||
// 57g. A list item in a Variables tab: the eyedropper goes to the scene view, Take writes the item
|
||||
// and brings the tab back.
|
||||
await evaluate(
|
||||
`__gm.state.openValues({ kind: 'list', slot: { kind: 'object', holderId: ${JSON.stringify(sceneId)}, objectId: 'or57-obj', key: 'crowd' } })`,
|
||||
);
|
||||
await sleep(200);
|
||||
const tabShown = (await pick()).tab;
|
||||
const inspectorLeft = (await json('__gm.inspectorRect()')).x;
|
||||
const inTab = ((await json("__gm.locateIcons('eyedropper')")) ?? []).filter((p) => p.x < inspectorLeft);
|
||||
await press_(inTab[0], 'the eyedropper of the list item');
|
||||
const fromList = await pick();
|
||||
await click(spot.x, spot.y);
|
||||
const listCandidate = (await pick()).candidate;
|
||||
const take = (await pick()).strip?.take;
|
||||
await press_(take, 'Take');
|
||||
const backIn = await pick();
|
||||
const crowd = await json(
|
||||
"__gm.state.activeScene.objects.find((o) => o.id === 'or57-obj')?.overrides.crowd ?? null",
|
||||
);
|
||||
check(
|
||||
'from a list item in a Variables tab it picks in the scene view, and Take writes it and goes back',
|
||||
tabShown === 'variables' &&
|
||||
inTab.length === 1 &&
|
||||
fromList.tab === 'scene' &&
|
||||
fromList.strip?.text === 'Target for Aim 57 · crowd · [0]: click an object' &&
|
||||
listCandidate === 'or57-front' &&
|
||||
backIn.ended === 'take' &&
|
||||
backIn.tab === 'variables' &&
|
||||
JSON.stringify(crowd) === JSON.stringify([{ $object: 'or57-front' }]),
|
||||
JSON.stringify({ tabShown, inTab, fromList, listCandidate, backIn, crowd }),
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await mutate(`
|
||||
|
||||
Reference in new issue
Block a user