Ctrl- and shift-click only added single rows to the selection. With many objects spread over the hierarchy, picking a run of them meant clicking each one. Shift-click now selects everything between the anchor and the clicked row, as in a file explorer. - The anchor is the row last clicked without shift, plain or with ctrl. A second shift-click redraws the range from the same anchor. - Ctrl+shift-click adds the range to the current selection instead of replacing it. - A collapsed group inside the range brings all its members. Children hidden under a collapsed parent stay out, since they are not shown. - Without an anchor from the hierarchy, the primary object picked in the scene view serves as anchor. With no selection at all, shift-click adds the single row as before. - The range rule is a pure function in hierarchyRange.ts, so it is tested without a browser. HierarchyPanel keeps the anchor. Not checked in the browser: ctrl+shift-click and the anchor taken from the scene view. Only the code path covers them. Tests: - ./check.sh: 178 files, 2557 tests, CHECK EXIT 0 - e2e 1 + 52: ALL PASSED, 23 checks - 5 new unit tests in tests/editor/hierarchy-range.test.ts - New e2e check 52h with real clicks - Counter-check: without the range, 52h fails with ["o2","o5"] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { GroupObjectsCommand } from '../../src/core/commands/groupCommands';
|
|
import { groupRowId, hierarchyRows } from '../../src/core/model/hierarchy';
|
|
import { hierarchyRange } from '../../src/editor/panels/hierarchyRange';
|
|
import { makeSpriteScene, place } from '../helpers';
|
|
|
|
/** Top-level a, b, c, d, e with k under a; b and d form the group "Crates": a [b d] c e. */
|
|
function grouped() {
|
|
const f = makeSpriteScene();
|
|
const [a, b, c, d, e] = [0, 1, 2, 3, 4].map((i) => place(f, f.spriteId, { x: i * 10, y: 0 }));
|
|
const k = place(f, f.spriteId, { x: 0, y: 10 }, a);
|
|
const group = new GroupObjectsCommand(f.sceneId, [b!, d!], 'Crates');
|
|
f.stack.execute(group);
|
|
const row = groupRowId(group.groupId);
|
|
const names: Record<string, string> = { [a!]: 'a', [b!]: 'b', [c!]: 'c', [d!]: 'd', [e!]: 'e', [k]: 'k' };
|
|
/** The row ids the panel shows with these rows unfolded. */
|
|
const rows = (open: string[]) =>
|
|
hierarchyRows(f.sceneOf(), new Set(open)).map((r) =>
|
|
r.kind === 'group' ? groupRowId(r.group.id) : r.object.id,
|
|
);
|
|
const range = (open: string[], anchor: string, clicked: string) =>
|
|
hierarchyRange(f.sceneOf(), rows(open), anchor, clicked)
|
|
.map((id) => names[id] ?? '?')
|
|
.sort()
|
|
.join(' ');
|
|
return { a: a!, b: b!, c: c!, d: d!, e: e!, k, row, range };
|
|
}
|
|
|
|
describe('Shift-click range in the hierarchy', () => {
|
|
it('takes every shown row between anchor and click, in either direction', () => {
|
|
const f = grouped();
|
|
expect(f.range([f.a, f.row], f.a, f.c)).toBe('a b c d k');
|
|
expect(f.range([f.a, f.row], f.c, f.a)).toBe('a b c d k');
|
|
expect(f.range([f.a, f.row], f.b, f.b)).toBe('b');
|
|
});
|
|
|
|
it('brings in all members of a folded group, but no children folded under a parent', () => {
|
|
const f = grouped();
|
|
expect(f.range([], f.a, f.c)).toBe('a b c d');
|
|
});
|
|
|
|
it('starts inside an open group and runs out of it', () => {
|
|
const f = grouped();
|
|
expect(f.range([f.row], f.d, f.e)).toBe('c d e');
|
|
});
|
|
|
|
it('can start or end on a group row', () => {
|
|
const f = grouped();
|
|
expect(f.range([], f.row, f.e)).toBe('b c d e');
|
|
expect(f.range([f.row], f.a, f.row)).toBe('a b d');
|
|
});
|
|
|
|
it('gives nothing when a row is not shown', () => {
|
|
const f = grouped();
|
|
expect(f.range([], f.k, f.c)).toBe('');
|
|
});
|
|
});
|