Layer, frame and shape lists all reorder by drag and drop
Reordering shapes by dragging worked well in the vector tab. Pixel art layers and frames and tilemap layers still used up/down buttons. All three lists now share one widget built from the shape list. - SortableList in the UI toolkit, with the drag rules next to it in src/ui/sortable.ts. A row has an optional thumbnail, a name, a hint on the right and an eye toggle. Every drop is one undo step. - The shape list is a thin wrapper around it. shapeRows.ts is gone, its tests moved to the new rules. - Pixel art layers show the layer alone on the current frame, frames show the whole frame. Up/Down, Hide, Earlier and Later are gone. - Tilemap layers get the eye instead of the checkbox. The per-row up, down and delete buttons are replaced by Delete layer next to Add layer. - A row is selected on release instead of press. Selecting rebuilt the inspector or tilemap panel and cancelled a drag that started on a row not yet selected. Tests: - ./check.sh: 138 files, 2052 tests, exit 0 (before 137 / 2051). - GM_STEPS=39,40,47,48 (fresh seed): ALL PASSED, 154 checks. - 5 new e2e checks. Check total 1264 in the README is computed (1259 + 5), CI never measured 1259. - Counter-checks bite: dropMove ignoring reversed fails a unit test, a tilemap drop not applied fails the drag check, a layer thumbnail showing all layers fails the thumbnail check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8adef83f23
commit
5b9b57fd19
14 files changed
+699
-326
No files matched your search
@@ -33,12 +33,9 @@ import {
|
||||
setPaintKind,
|
||||
shapeSignature,
|
||||
} from '../../src/editor/inspector/shapeSection';
|
||||
import { shapeDropGap, shapeDropMove, shapeRowOrder } from '../../src/editor/vectorart/shapeRows';
|
||||
import { EditorState, shapeSelection } from '../../src/editor/state';
|
||||
import { PaintCache } from '../../src/viewport/vectorArtPicture';
|
||||
|
||||
const ROW = 30;
|
||||
|
||||
/**
|
||||
* These rows really are built, and a widget repaints itself one microtask after a value goes into
|
||||
* it – text width included, which Pixi measures on a canvas of the browser. The suite runs on node,
|
||||
@@ -101,36 +98,6 @@ function widgets<T extends Widget>(host: InspectorHost, type: new (...args: neve
|
||||
|
||||
const texts = (host: InspectorHost): string[] => widgets(host, Label).map((label) => label.text);
|
||||
|
||||
describe('shape list order', () => {
|
||||
it('shows the drawing order upside down – the shape drawn last is the top row', () => {
|
||||
expect(shapeRowOrder(['bottom', 'middle', 'top'])).toEqual(['top', 'middle', 'bottom']);
|
||||
});
|
||||
|
||||
it('drops into the gap the pointer is nearest to', () => {
|
||||
expect(shapeDropGap(3, 2, ROW)).toBe(0);
|
||||
expect(shapeDropGap(3, 20, ROW)).toBe(1);
|
||||
expect(shapeDropGap(3, 44, ROW)).toBe(1);
|
||||
expect(shapeDropGap(3, 46, ROW)).toBe(2);
|
||||
// Outside the list there is no gap: a drop above it is the first, below it the last.
|
||||
expect(shapeDropGap(3, 400, ROW)).toBe(3);
|
||||
expect(shapeDropGap(3, -40, ROW)).toBe(0);
|
||||
});
|
||||
|
||||
it('turns a drop into a move of the model, the top row being the last shape', () => {
|
||||
// Three shapes, so the rows are the model indices [2, 1, 0]: the top row dropped at the
|
||||
// bottom gap becomes the first shape of the model, the one drawn first.
|
||||
expect(shapeDropMove(3, 0, 3)).toEqual({ from: 2, to: 0 });
|
||||
expect(shapeDropMove(3, 2, 0)).toEqual({ from: 0, to: 2 });
|
||||
expect(shapeDropMove(3, 1, 0)).toEqual({ from: 1, to: 2 });
|
||||
});
|
||||
|
||||
it('is no move when the row lands where it already is', () => {
|
||||
expect(shapeDropMove(3, 1, 1)).toBeNull();
|
||||
expect(shapeDropMove(3, 1, 2)).toBeNull();
|
||||
expect(shapeDropMove(3, 5, 1)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shape rows', () => {
|
||||
it('shows fill, opacity and an outline switch, and no line rows without an outline', () => {
|
||||
const { host, assetId, ids } = withShapes(1);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* The arithmetic of the sortable list shared by the shape, pixel art and tilemap lists: which row
|
||||
* shows which item, which gap a drop falls into, and what move that makes in the owner's order.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { dropGap, dropMove, rowOrder } from '../../src/ui/sortable';
|
||||
|
||||
const ROW = 30;
|
||||
|
||||
describe('sortable list', () => {
|
||||
it('shows a layer list upside down – the item drawn last is the top row', () => {
|
||||
expect(rowOrder(['bottom', 'middle', 'top'], true)).toEqual(['top', 'middle', 'bottom']);
|
||||
expect(rowOrder(['first', 'second'], false)).toEqual(['first', 'second']);
|
||||
});
|
||||
|
||||
it('drops into the gap the pointer is nearest to', () => {
|
||||
expect(dropGap(3, 2, ROW)).toBe(0);
|
||||
expect(dropGap(3, 20, ROW)).toBe(1);
|
||||
expect(dropGap(3, 44, ROW)).toBe(1);
|
||||
expect(dropGap(3, 46, ROW)).toBe(2);
|
||||
// Outside the list there is no gap: a drop above it is the first, below it the last.
|
||||
expect(dropGap(3, 400, ROW)).toBe(3);
|
||||
expect(dropGap(3, -40, ROW)).toBe(0);
|
||||
});
|
||||
|
||||
it('turns a drop into a move of the model, the top row being the last item when reversed', () => {
|
||||
// Three items, so the rows are the model indices [2, 1, 0]: the top row dropped at the
|
||||
// bottom gap becomes the first item of the model, the one drawn first.
|
||||
expect(dropMove(3, 0, 3, true)).toEqual({ from: 2, to: 0 });
|
||||
expect(dropMove(3, 2, 0, true)).toEqual({ from: 0, to: 2 });
|
||||
expect(dropMove(3, 1, 0, true)).toEqual({ from: 1, to: 2 });
|
||||
});
|
||||
|
||||
it('keeps the indices as they are in a list shown in model order', () => {
|
||||
// Frames: the first row is the first frame. The top row dropped at the bottom becomes the last.
|
||||
expect(dropMove(3, 0, 3, false)).toEqual({ from: 0, to: 2 });
|
||||
expect(dropMove(3, 2, 0, false)).toEqual({ from: 2, to: 0 });
|
||||
expect(dropMove(3, 0, 2, false)).toEqual({ from: 0, to: 1 });
|
||||
});
|
||||
|
||||
it('is no move when the row lands where it already is', () => {
|
||||
for (const reversed of [true, false]) {
|
||||
expect(dropMove(3, 1, 1, reversed)).toBeNull();
|
||||
expect(dropMove(3, 1, 2, reversed)).toBeNull();
|
||||
expect(dropMove(3, 5, 1, reversed)).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user