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
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { DrawItem } from '../../src/viewport/items';
|
||||
import { pressOn, stackAt } from '../../src/viewport/pick';
|
||||
|
||||
/** Only what the pick order reads: the type, the `z`, and a map's layers with their own `z`. */
|
||||
function item(
|
||||
id: string,
|
||||
type: DrawItem['type'],
|
||||
z: number,
|
||||
layers: { id: string; z: number }[] = [],
|
||||
): DrawItem {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
z,
|
||||
tilemap: type === 'tilemap' ? { layers: layers.map((l) => ({ ...l, tiles: [] })) } : null,
|
||||
} as unknown as DrawItem;
|
||||
}
|
||||
|
||||
describe('what lies under the pointer (#203)', () => {
|
||||
const map = item('map', 'tilemap', 0, [
|
||||
{ id: 'ground', z: -5 },
|
||||
{ id: 'roofs', z: 5 },
|
||||
]);
|
||||
const hero = item('hero', 'sprite', -1);
|
||||
|
||||
it('puts a sprite in front of a map whose only tile there is on a layer below it', () => {
|
||||
expect(
|
||||
stackAt(
|
||||
[map, hero],
|
||||
[
|
||||
{ id: 'map', layers: ['ground'] },
|
||||
{ id: 'hero', layers: null },
|
||||
],
|
||||
),
|
||||
).toEqual(['hero', 'map']);
|
||||
});
|
||||
|
||||
it('puts the map in front where a layer above the sprite has a tile', () => {
|
||||
expect(
|
||||
stackAt(
|
||||
[map, hero],
|
||||
[
|
||||
{ id: 'map', layers: ['ground', 'roofs'] },
|
||||
{ id: 'hero', layers: null },
|
||||
],
|
||||
),
|
||||
).toEqual(['map', 'hero']);
|
||||
});
|
||||
|
||||
it('leaves out a map without a tile under the pointer', () => {
|
||||
expect(
|
||||
stackAt(
|
||||
[map, hero],
|
||||
[
|
||||
{ id: 'map', layers: [] },
|
||||
{ id: 'hero', layers: null },
|
||||
],
|
||||
),
|
||||
).toEqual(['hero']);
|
||||
});
|
||||
|
||||
it('orders sprites by z, ties in hierarchy order, with symbols in front of all', () => {
|
||||
const items = [
|
||||
item('a', 'sprite', 2),
|
||||
item('b', 'sprite', 0),
|
||||
item('c', 'sprite', 2),
|
||||
item('box', 'collider', -9),
|
||||
];
|
||||
const hits = items.map((i) => ({ id: i.id, layers: null }));
|
||||
expect(stackAt(items, hits)).toEqual(['box', 'c', 'a', 'b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('what a press picks (#203)', () => {
|
||||
const stack = ['top', 'middle', 'bottom'];
|
||||
|
||||
it('selects the topmost when nothing selected is under the pointer', () => {
|
||||
expect(pressOn(stack, ['elsewhere'], 'elsewhere')).toEqual({ select: 'top', onClick: null });
|
||||
});
|
||||
|
||||
it('keeps a selection hidden under others, so a drag moves it', () => {
|
||||
expect(pressOn(stack, ['bottom'], 'bottom')?.select).toBeNull();
|
||||
});
|
||||
|
||||
it('passes a click on to the next one below, and round to the top again', () => {
|
||||
expect(pressOn(stack, ['top'], 'top')?.onClick).toBe('middle');
|
||||
expect(pressOn(stack, ['middle'], 'middle')?.onClick).toBe('bottom');
|
||||
expect(pressOn(stack, ['bottom'], 'bottom')?.onClick).toBe('top');
|
||||
});
|
||||
|
||||
it('goes on from the primary one of a group', () => {
|
||||
expect(pressOn(stack, ['top', 'middle'], 'middle')).toEqual({ select: null, onClick: 'bottom' });
|
||||
});
|
||||
|
||||
it('changes nothing on a click when the selected object is alone there', () => {
|
||||
expect(pressOn(['only'], ['only'], 'only')).toEqual({ select: null, onClick: null });
|
||||
});
|
||||
|
||||
it('is nothing on empty space', () => {
|
||||
expect(pressOn([], ['top'], 'top')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user