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
@@ -8,6 +8,7 @@ import {
|
||||
import type { Command } from '../../core/commands/Command';
|
||||
import type { PropertyValue } from '../../core/model/Properties';
|
||||
import { type ValuePath, withValueAt } from '../../core/properties/valuePath';
|
||||
import { FramePreview } from '../framePreview';
|
||||
import type { EditorState, ListSlot } from '../state';
|
||||
import { type LiveVariables, resolveList } from '../list/slot';
|
||||
import type { FieldHandlers } from './fieldFactory';
|
||||
@@ -84,6 +85,7 @@ export interface PreviewedValue<Target, Model = PropertyValue> {
|
||||
/**
|
||||
* Row handlers that preview into the model and restore the pre-preview value (or its absence, via
|
||||
* `remove`) before committing, since a command records whatever it finds at execute time as its undo target.
|
||||
* The model is written on every input, the preview event waits for the frame (#274).
|
||||
*/
|
||||
export function previewingHandlers<Target, Model = PropertyValue>(
|
||||
host: { state: EditorState },
|
||||
@@ -92,6 +94,7 @@ export function previewingHandlers<Target, Model = PropertyValue>(
|
||||
const toModel = spec.toModel ?? ((input: PropertyValue) => input as unknown as Model);
|
||||
let previewed = false;
|
||||
let before: Model | undefined;
|
||||
const perFrame = new FramePreview(host.state, 'inspector-input');
|
||||
return {
|
||||
onInput: (input) => {
|
||||
const target = spec.target();
|
||||
@@ -101,9 +104,10 @@ export function previewingHandlers<Target, Model = PropertyValue>(
|
||||
before = spec.read(target);
|
||||
}
|
||||
spec.write(target, toModel(input, target));
|
||||
host.state.preview();
|
||||
perFrame.request();
|
||||
},
|
||||
onChange: (input) => {
|
||||
perFrame.cancel();
|
||||
const target = spec.target();
|
||||
if (!target) return;
|
||||
if (previewed) {
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { Button, HStack, Theme } from '../../ui';
|
||||
import { removeShapes } from '../actions';
|
||||
import { FramePreview } from '../framePreview';
|
||||
import { type EditorState, shapeSelection } from '../state';
|
||||
import { GradientBar } from '../vectorart/GradientBar';
|
||||
import { ShapeList } from '../vectorart/ShapeList';
|
||||
@@ -627,6 +628,7 @@ function gradientRows(host: InspectorHost, assetId: string, shapeId: string, par
|
||||
const shape = currentShape(host, assetId, shapeId);
|
||||
return shape ? gradientIn(shape, part) : undefined;
|
||||
};
|
||||
const perFrame = new FramePreview(host.state, 'gradient-stop');
|
||||
const bar = new GradientBar(host.state, {
|
||||
gradient: gradientOf,
|
||||
selected: () => pickedStop(assetId, shapeId, part, gradientOf()),
|
||||
@@ -640,9 +642,10 @@ function gradientRows(host: InspectorHost, assetId: string, shapeId: string, par
|
||||
const shape = currentShape(host, assetId, shapeId);
|
||||
if (!shape) return;
|
||||
applyPreview(shape, withGradient(shape, part, gradient));
|
||||
host.state.preview();
|
||||
perFrame.request();
|
||||
},
|
||||
commit: (before, after, label) => {
|
||||
perFrame.cancel();
|
||||
const shape = currentShape(host, assetId, shapeId);
|
||||
if (!shape) return;
|
||||
applyPreview(shape, withGradient(shape, part, before));
|
||||
|
||||
Reference in new issue
Block a user