perf(editor): use repaint previews while resizing
This commit is contained in:
parent
1242fa7086
commit
8cf51ffefd
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 & {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
92
tests/e2e/editor/resize-performance.spec.ts
Normal file
92
tests/e2e/editor/resize-performance.spec.ts
Normal 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()
|
||||
})
|
||||
Loading…
Reference in a new issue