Preview inspector edits once per frame, not per input (#274)
Scrubbing a number in the inspector and dragging a colour stop of a gradient told the editor about every input. Pixi repeats each pointer move shortly after, so both gestures paid for two previews per move. The issue asked to measure first and to build only from 10 % per move. The measurement showed about 50 %. - previewingHandlers still writes the model on every input but requests the preview through a FramePreview of its own. The commit cancels a waiting preview first. - The preview handler of the GradientBar in shapeSection.ts does the same. - Wider than the issue describes: the row handlers also serve typing, the arrow keys and the colour picker. All of them preview once per frame now. Measured on stein2, fixture project with 120 extra objects, 120 moves 8 ms apart, three runs each: - Scrub: 236 project events before, 51 to 61 after. 45 to 50 ms per move before, 22 to 27 after. - Colour stop: 238 project events before, 50 to 51 after. 44 to 49 ms per move before, 22 after. - One preview still costs 13 to 17 ms. That price per frame is what remains. Finding, not fixed here: in the pixel art tab the palette colour and the opacity of a layer still preview on every input (PixelArtSide.ts), and so do the head fields of the curve tab. Neither was measured. Tests: - Gate: 198 files, 2907 tests (2903 + 4), exit 0 - e2e, full run: 1616 checks passed (1615 + 1), exit 0 - Smokes: 24 / 19 / 21, exit 0 - Four new unit tests in tests/editor/frame-preview.test.ts, two for a row and two for a colour stop - One new e2e check in module 02: a held scrub gave 10 project events in 12 frames, and the value is there before the release - Four unit counter-probes fail, 1 to 2 tests each. They ran on stein2, not in the container. - The browser counter-probe fails the new check with 50 events in 16 frames Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c1cc80bb45
commit
c4a9280140
5 files changed
+163
-9
No files matched your search
@@ -2,14 +2,22 @@
|
||||
* A drag writes the model on every pointer move but tells the editor once per frame (#213): the
|
||||
* model event rebuilds panels and pictures, and a mouse reports faster than the screen draws.
|
||||
*/
|
||||
import type { Application } from 'pixi.js';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { GraphAsset } from '../../src/core/model/Asset';
|
||||
import { type Application, CanvasTextMetrics } from 'pixi.js';
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildAsset } from '../../src/core/commands/assetCommands';
|
||||
import type { Command } from '../../src/core/commands/Command';
|
||||
import type { GraphAsset, VectorArtAsset, VectorGradient } from '../../src/core/model/Asset';
|
||||
import { createEmptyProject } from '../../src/core/model/Project';
|
||||
import { FramePreview } from '../../src/editor/framePreview';
|
||||
import { GraphDrags } from '../../src/editor/graph/graphDrags';
|
||||
import type { GraphCanvasHost } from '../../src/editor/graph/graphHost';
|
||||
import type { EditorState } from '../../src/editor/state';
|
||||
import type { InspectorHost } from '../../src/editor/inspector/host';
|
||||
import { previewingHandlers } from '../../src/editor/inspector/previewHandlers';
|
||||
import { buildShapeSection, currentGradientBar } from '../../src/editor/inspector/shapeSection';
|
||||
import { EditorState } from '../../src/editor/state';
|
||||
import { VStack } from '../../src/ui';
|
||||
import { initRenderLoop, resetRenderLoop } from '../../src/ui/render';
|
||||
import { PaintCache } from '../../src/viewport/vectorArtPicture';
|
||||
|
||||
/** One frame of the render loop, the way the ticker runs it. */
|
||||
function frames(): () => void {
|
||||
@@ -117,3 +125,115 @@ describe('dragging a node in the graph', () => {
|
||||
expect(state.preview).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('editing an inspector row (#274)', () => {
|
||||
beforeEach(() => resetRenderLoop());
|
||||
|
||||
function rig() {
|
||||
const state = fakeState();
|
||||
const target = { x: 1 };
|
||||
const commit = vi.fn((value: number) => ({ value }) as unknown as Command);
|
||||
const handlers = previewingHandlers<typeof target, number>(
|
||||
{ state: state as unknown as EditorState },
|
||||
{ target: () => target, read: (t) => t.x, write: (t, value) => (t.x = value), commit },
|
||||
);
|
||||
return { state, target, commit, handlers };
|
||||
}
|
||||
|
||||
it('writes every input into the model and previews once a frame', () => {
|
||||
const tick = frames();
|
||||
const { state, target, handlers } = rig();
|
||||
for (let i = 2; i <= 6; i++) {
|
||||
handlers.onInput(i);
|
||||
expect(target.x).toBe(i);
|
||||
}
|
||||
expect(state.preview).not.toHaveBeenCalled();
|
||||
tick();
|
||||
expect(state.preview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('commits from the value before the scrub and leaves nothing for the next frame', () => {
|
||||
const tick = frames();
|
||||
const { state, target, commit, handlers } = rig();
|
||||
handlers.onInput(5);
|
||||
handlers.onChange(7);
|
||||
expect(target.x).toBe(1);
|
||||
expect(commit).toHaveBeenCalledWith(7);
|
||||
expect(state.execute).toHaveBeenCalledTimes(1);
|
||||
tick();
|
||||
expect(state.preview).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dragging a colour stop of a gradient (#274)', () => {
|
||||
beforeAll(() => {
|
||||
CanvasTextMetrics.measureText = (() => ({ width: 10, height: 10, lines: [], lineWidths: [] })) as never;
|
||||
PaintCache.prototype.gradient = (() => null) as never;
|
||||
});
|
||||
beforeEach(() => resetRenderLoop());
|
||||
|
||||
function rig() {
|
||||
const state = new EditorState();
|
||||
state.loadProject(createEmptyProject('p', 'P'), 1);
|
||||
const asset = buildAsset('vectorart', 'Drawing') as VectorArtAsset;
|
||||
asset.shapes.push({
|
||||
id: 's',
|
||||
name: 'Sky',
|
||||
fill: 0x0000ff,
|
||||
opacity: 1,
|
||||
rings: [],
|
||||
paint: {
|
||||
type: 'linear',
|
||||
from: { x: 0, y: 0 },
|
||||
to: { x: 100, y: 0 },
|
||||
stops: [
|
||||
{ offset: 0, color: 0x0000ff, alpha: 1 },
|
||||
{ offset: 1, color: 0xffffff, alpha: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
state.project.assets.push(asset);
|
||||
const host: InspectorHost = {
|
||||
state,
|
||||
body: new VStack(),
|
||||
fields: new Map(),
|
||||
nameField: null,
|
||||
project: () => state.project,
|
||||
liveVariables: () => null,
|
||||
};
|
||||
buildShapeSection(host, asset.id, 's', ['s']);
|
||||
const bar = currentGradientBar('fill')!;
|
||||
// 100 px between the outermost markers, which stand 6 px in from either end.
|
||||
bar.resize(112, 32);
|
||||
const pointer = (type: string, x: number) =>
|
||||
bar.emit(type, { button: 0, global: { x, y: 25 }, stopPropagation: () => undefined });
|
||||
const offsets = () => (asset.shapes[0]!.paint as VectorGradient).stops.map((stop) => stop.offset);
|
||||
return { state, preview: vi.spyOn(state, 'preview'), pointer, offsets };
|
||||
}
|
||||
|
||||
it('moves the stop with every pointer move and previews once a frame', () => {
|
||||
const tick = frames();
|
||||
const { preview, pointer, offsets } = rig();
|
||||
pointer('pointerdown', 6);
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
pointer('globalpointermove', 6 + i * 10);
|
||||
expect(offsets()[0]).toBeCloseTo(i / 10);
|
||||
}
|
||||
expect(preview).not.toHaveBeenCalled();
|
||||
tick();
|
||||
expect(preview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('leaves nothing for the frame after the release, and the drag is one undo step', () => {
|
||||
const tick = frames();
|
||||
const { state, preview, pointer, offsets } = rig();
|
||||
pointer('pointerdown', 6);
|
||||
pointer('globalpointermove', 56);
|
||||
pointer('pointerup', 56);
|
||||
expect(offsets()[0]).toBeCloseTo(0.5);
|
||||
tick();
|
||||
expect(preview).not.toHaveBeenCalled();
|
||||
state.undo();
|
||||
expect(offsets()[0]).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user