perf(editor): use repaint previews while resizing

This commit is contained in:
Danila Poyarkov 2026-05-13 05:38:14 +03:00
parent 1242fa7086
commit 8cf51ffefd
4 changed files with 142 additions and 15 deletions

View file

@ -359,13 +359,29 @@ export class SceneGraph {
])
updateNodePositionPreview(id: string, x: number, y: number): void {
this.updateNodePreview(id, { x, y })
}
updateNodePreview(id: string, changes: Partial<SceneNode>): void {
const node = this.nodes.get(id)
if (!node) return
if (node.x === x && node.y === y) return
this.absPosCache.clear()
if ((Object.keys(changes) as (keyof SceneNode)[]).every((key) => node[key] === changes[key])) {
return
}
const affectsLayout = Object.keys(changes).some((k) => SceneGraph.LAYOUT_AFFECTING_KEYS.has(k))
if (affectsLayout) this.absPosCache.clear()
if (
node.type === 'TEXT' &&
node.textPicture &&
Object.keys(changes).some((k) => SceneGraph.TEXT_PICTURE_KEYS.has(k))
) {
node.textPicture = null
}
if (changes.vectorNetwork) {
changes = { ...changes, vectorNetwork: normalizeVectorNetwork(changes.vectorNetwork) }
}
this.positionPreviewVersion++
node.x = x
node.y = y
Object.assign(node, changes)
}
updateNode(id: string, changes: Partial<SceneNode>): void {

View file

@ -19,7 +19,7 @@ import { handleDrawMove, handleDrawUp } from '#vue/shared/input/draw'
import { handleMoveMove, handleMoveUp } from '#vue/shared/input/move'
import { handleNodeEditMove } from '#vue/shared/input/node-edit'
import { setupPanZoom } from '#vue/shared/input/pan-zoom'
import { applyResize } from '#vue/shared/input/resize'
import { applyResize, commitResizePreview } from '#vue/shared/input/resize'
import { updateHoverCursor } from '#vue/shared/input/select'
import { useSpaceHeld } from '#vue/shared/input/space-key'
import type { DragState } from '#vue/shared/input/types'
@ -178,7 +178,7 @@ export function useCanvasInput(
else if (d.type === 'text-select') {
drag.value = null
return
} else if (d.type === 'resize') editor.commitResize(d.nodeId, d.origRect)
} else if (d.type === 'resize') commitResizePreview(d, editor)
else if (d.type === 'pen-drag') {
const penState = editor.state.penState as
| (typeof editor.state.penState & {

View file

@ -7,13 +7,7 @@ import { calculateResizeRect } from '#vue/shared/input/resize/rect'
import { scaleVectorNetworkForResize } from '#vue/shared/input/resize/vector'
import type { DragResize } from '#vue/shared/input/types'
export function applyResize(
d: DragResize,
cx: number,
cy: number,
constrain: boolean,
editor: Editor
) {
function resizeChanges(d: DragResize, cx: number, cy: number, constrain: boolean) {
const { origRect } = d
const newRect = calculateResizeRect(d.handle, origRect, cx - d.startX, cy - d.startY, constrain)
@ -27,6 +21,31 @@ export function applyResize(
newRect.height
)
if (resizedVectorNetwork) changes.vectorNetwork = resizedVectorNetwork
editor.updateNode(d.nodeId, changes)
return changes
}
export function applyResize(
d: DragResize,
cx: number,
cy: number,
constrain: boolean,
editor: Editor
) {
editor.graph.updateNodePreview(d.nodeId, resizeChanges(d, cx, cy, constrain))
editor.requestRepaint()
}
export function commitResizePreview(d: DragResize, editor: Editor) {
const node = editor.graph.getNode(d.nodeId)
if (!node) return
const finalChanges: Partial<SceneNode> = {
x: node.x,
y: node.y,
width: node.width,
height: node.height
}
if (node.vectorNetwork) finalChanges.vectorNetwork = node.vectorNetwork
editor.graph.updateNodePreview(d.nodeId, d.origRect)
editor.updateNode(d.nodeId, finalChanges)
editor.commitResize(d.nodeId, d.origRect)
}

View file

@ -0,0 +1,92 @@
import { expect, test } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
test('resizing uses repaint-only previews until mouseup', async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
canvas.errors.length = 0
await canvas.clearCanvas()
const id = await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.state.zoom = 1
store.state.panX = 0
store.state.panY = 0
const node = store.graph.createNode('FRAME', store.state.currentPageId, {
name: 'Resizable Card',
x: 100,
y: 100,
width: 240,
height: 140,
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }]
})
for (let i = 0; i < 12; i++) {
store.graph.createNode('TEXT', node.id, {
name: `Label ${i}`,
text: `Label ${i}`,
x: 16,
y: 16 + i * 18,
width: 160,
height: 16
})
}
store.select([node.id])
store.requestRender()
return node.id
})
await canvas.waitForRender()
await page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const originalStoreUpdate = store.updateNode.bind(store)
const originalGraphUpdate = store.graph.updateNode.bind(store.graph)
let storeUpdateCount = 0
let graphUpdateCount = 0
let repaintCount = 0
store.updateNode = ((nodeId, changes) => {
storeUpdateCount++
return originalStoreUpdate(nodeId, changes)
}) as typeof store.updateNode
store.graph.updateNode = ((nodeId, changes) => {
graphUpdateCount++
return originalGraphUpdate(nodeId, changes)
}) as typeof store.graph.updateNode
store.onEditorEvent('repaint:requested', () => {
repaintCount++
})
Object.assign(window, {
__openPencilResizeCounters: () => ({ storeUpdateCount, graphUpdateCount, repaintCount })
})
})
await canvas.drag(340, 240, 420, 290, 12)
const counters = await page.evaluate(() => {
const getCounters = (
window as typeof window & {
__openPencilResizeCounters?: () => {
storeUpdateCount: number
graphUpdateCount: number
repaintCount: number
}
}
).__openPencilResizeCounters
return getCounters?.() ?? null
})
const node = await page.evaluate((nodeId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getNode(nodeId)
}, id)
expect(counters?.storeUpdateCount).toBeLessThanOrEqual(1)
expect(counters?.graphUpdateCount).toBeLessThanOrEqual(1)
expect(counters?.repaintCount).toBeGreaterThan(0)
expect(node?.width).toBeGreaterThan(240)
expect(node?.height).toBeGreaterThan(140)
canvas.assertNoErrors()
})