Clicks in the scene view pick what is drawn under the cursor (#203)
Pixi's own hit test decided what a click in the scene view selected. A tilemap caught clicks over its whole area, even on empty cells and over sprites sorted above one of its layers. A press always grabbed the topmost object, so a hidden selected object could not be dragged, and there was no way to reach an object underneath another one. - New pure rule in src/viewport/pick.ts. It lists the objects under the cursor front to back, in the order they are drawn. - A tilemap counts only where it has a tile. Its depth there is the Z of the highest layer with a tile at that spot. An empty cell is empty space, so a click clears the selection or starts a marquee. - Pressing on any selected object, hidden ones included, drags the selection. Otherwise the press selects the topmost object and drags it, as before. - A click without drag on a selected object selects the next one underneath. After the lowest it starts again at the top. - New debug hook __gm.sceneStack(x, y) returns that list. Decisions: - With a paint tool the selected tilemap paints anywhere on its grid, even under a sprite. With empty cells as empty space an empty cell could not be painted otherwise. - Icons such as colliders and cameras stay in front. A second click now reaches the sprite under a collider. Tests: - ./check.sh: 142 files, 2090 tests, exit 0 (was 141 / 2080) - 10 new unit tests in tests/viewport/pick.test.ts - GM_STEPS=39,40: ALL PASSED, 77 checks - GM_STEPS=1-16,29,39-43: ALL PASSED, 626 checks - 4 new checks in block 40q: sprite over a lower layer, empty cell, cycling crate, map, crate, dragging the hidden selected crate. - Counter test: whole-area tilemap hits and always-topmost presses fail 4 unit tests and all 4 new checks. - Check total 1290 (1286 + 4), computed, not yet counted by CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d9c71c2ce7
commit
ad2a2d7921
6 files changed
+344
-23
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { colourDistance } from '../lib/harness.mjs';
|
||||
import { LANES, openTab, pickAssetType } from './lib/scaffold.mjs';
|
||||
import { LANES, MAKE_OBJECT, openTab, pickAssetType } from './lib/scaffold.mjs';
|
||||
import { quadColour } from './39-tile.mjs';
|
||||
|
||||
/**
|
||||
@@ -670,6 +670,92 @@ export async function tilemap(t) {
|
||||
await undo();
|
||||
await settle();
|
||||
|
||||
// 40q. A click picks what is drawn under the pointer (#203). A crate of its own sits on the 3 × 3
|
||||
// block at z −1, below the map's 0; with the block's layer sorted in at −5 the crate is drawn
|
||||
// in front, so it is what a click there takes, although the map's grid spans the spot.
|
||||
const CRATE = 'tm-pick-crate';
|
||||
const blockLayer = `__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(made.id)}).layers.find((l) => l.cells['1,1'])`;
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
${MAKE_OBJECT}
|
||||
const crate = make(${JSON.stringify(CRATE)}, 'crate', '', ${LANE - 24}, 'Pick crate');
|
||||
crate.position = { x: -72, y: ${LANE - 24}, z: -1 };
|
||||
scene.objects.push(crate);
|
||||
${blockLayer}.z = -5;`);
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await settle();
|
||||
const cellPoint = (x, y) => json(`__gm.tilemap.cellAt(${JSON.stringify(MAP_OBJECT)}, ${x}, ${y})`);
|
||||
const stackAt = (p) => json(`__gm.sceneStack(${p.x}, ${p.y})`);
|
||||
const selected = () => json('__gm.state.selectedObjectIds');
|
||||
const onBlock = await cellPoint(1, 1);
|
||||
const stackBelow = await stackAt(onBlock);
|
||||
await click(onBlock.x, onBlock.y);
|
||||
await settle();
|
||||
const pickedBelow = await selected();
|
||||
check(
|
||||
'a sprite drawn over a map layer sorted in below it is what a click there picks',
|
||||
JSON.stringify(stackBelow) === JSON.stringify([CRATE, MAP_OBJECT]) &&
|
||||
JSON.stringify(pickedBelow) === JSON.stringify([CRATE]),
|
||||
JSON.stringify({ stackBelow, pickedBelow }),
|
||||
);
|
||||
|
||||
// An empty cell is empty space: the click there takes nothing and clears the selection.
|
||||
const onEmpty = await cellPoint(COLUMNS - 1, ROWS - 1);
|
||||
const stackEmpty = await stackAt(onEmpty);
|
||||
await click(onEmpty.x, onEmpty.y);
|
||||
await settle();
|
||||
const pickedEmpty = await selected();
|
||||
check(
|
||||
'a click on an empty cell of the map selects nothing',
|
||||
stackEmpty.length === 0 && pickedEmpty.length === 0,
|
||||
JSON.stringify({ stackEmpty, pickedEmpty }),
|
||||
);
|
||||
|
||||
// Clicking what is selected passes on to the next one below, and from the bottom round to the top.
|
||||
const cycle = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const p = await cellPoint(1, 1);
|
||||
await click(p.x, p.y);
|
||||
await settle();
|
||||
cycle.push((await selected()).join(','));
|
||||
}
|
||||
check(
|
||||
'clicking the selected object again selects the one below it, then the top one again',
|
||||
JSON.stringify(cycle) === JSON.stringify([CRATE, MAP_OBJECT, CRATE]),
|
||||
JSON.stringify(cycle),
|
||||
);
|
||||
|
||||
// With the layer sorted in above the crate the map is in front – and a drag on the selected crate
|
||||
// still moves the crate, not the map on top of it.
|
||||
await mutate(`${blockLayer}.z = 5;`);
|
||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [CRATE], objectId: CRATE });
|
||||
await settle();
|
||||
const onTop = await cellPoint(1, 1);
|
||||
const stackAbove = await stackAt(onTop);
|
||||
const crateBefore = (await t.object(CRATE)).position;
|
||||
const mapUnder = (await t.object(MAP_OBJECT)).position;
|
||||
await drag(onTop.x, onTop.y, onTop.x + 40, onTop.y);
|
||||
await settle();
|
||||
const crateAfter = (await t.object(CRATE)).position;
|
||||
const mapUnderAfter = (await t.object(MAP_OBJECT)).position;
|
||||
const pickedDrag = await selected();
|
||||
check(
|
||||
'a drag on a selected sprite hidden under the map moves the sprite and leaves the map',
|
||||
JSON.stringify(stackAbove) === JSON.stringify([MAP_OBJECT, CRATE]) &&
|
||||
Math.round(crateAfter.x - crateBefore.x) === 40 &&
|
||||
JSON.stringify(mapUnderAfter) === JSON.stringify(mapUnder) &&
|
||||
JSON.stringify(pickedDrag) === JSON.stringify([CRATE]),
|
||||
JSON.stringify({ stackAbove, crateBefore, crateAfter, mapUnder, mapUnderAfter, pickedDrag }),
|
||||
);
|
||||
await undo();
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => o.id !== ${JSON.stringify(CRATE)});
|
||||
delete ${blockLayer}.z;`);
|
||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [MAP_OBJECT], objectId: MAP_OBJECT });
|
||||
await settle();
|
||||
|
||||
// 40o. The grid grows at the left edge (#136): the cells of both layers move one column in, the
|
||||
// origin moves half a cell to the left, and on screen the painted cells stay where they were.
|
||||
const allCells = () =>
|
||||
|
||||
Reference in new issue
Block a user