Perf: minor drawing speedup (#3464)
Tiny changes as I walk through freehand code. These would only really make a difference on pages with many freehand shapes. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` — Improving existing features ### Release Notes - Improve performance of draw shapes.
This commit is contained in:
parent
b5fab15c6d
commit
a6d2ab05d2
6 changed files with 2369 additions and 53 deletions
|
@ -97,7 +97,7 @@ export class Drawing extends StateNode {
|
|||
this.mergeNextPoint = false
|
||||
}
|
||||
|
||||
this.updateShapes()
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
this.updateShapes()
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
|
||||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
|
@ -137,7 +137,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.updateShapes()
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
|
||||
override onExit? = () => {
|
||||
|
@ -281,7 +281,7 @@ export class Drawing extends StateNode {
|
|||
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
||||
}
|
||||
|
||||
private updateShapes() {
|
||||
private updateDrawingShape() {
|
||||
const { initialShape } = this
|
||||
const { inputs } = this.editor
|
||||
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
Vec,
|
||||
VecLike,
|
||||
assert,
|
||||
average,
|
||||
precise,
|
||||
shortAngleDist,
|
||||
toDomPrecision,
|
||||
} from '@tldraw/editor'
|
||||
import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'
|
||||
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
||||
import { getStrokePoints } from './getStrokePoints'
|
||||
import { setStrokePointRadii } from './setStrokePointRadii'
|
||||
|
@ -36,17 +28,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|||
|
||||
const result: StrokePoint[][] = []
|
||||
let currentPartition: StrokePoint[] = [points[0]]
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const prevPoint = points[i - 1]
|
||||
const thisPoint = points[i]
|
||||
const nextPoint = points[i + 1]
|
||||
const prevAngle = Vec.Angle(prevPoint.point, thisPoint.point)
|
||||
const nextAngle = Vec.Angle(thisPoint.point, nextPoint.point)
|
||||
// acuteness is a normalized representation of how acute the angle is.
|
||||
// 1 is an infinitely thin wedge
|
||||
// 0 is a straight line
|
||||
const acuteness = Math.abs(shortAngleDist(prevAngle, nextAngle)) / Math.PI
|
||||
if (acuteness > 0.8) {
|
||||
let prevV = Vec.Sub(points[1].point, points[0].point).uni()
|
||||
let nextV: Vec
|
||||
let dpr: number
|
||||
let prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint
|
||||
for (let i = 1, n = points.length; i < n - 1; i++) {
|
||||
prevPoint = points[i - 1]
|
||||
thisPoint = points[i]
|
||||
nextPoint = points[i + 1]
|
||||
|
||||
nextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()
|
||||
dpr = Vec.Dpr(prevV, nextV)
|
||||
prevV = nextV
|
||||
|
||||
if (dpr < -0.8) {
|
||||
// always treat such acute angles as elbows
|
||||
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
||||
const elbowPoint = {
|
||||
|
@ -59,19 +54,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|||
continue
|
||||
}
|
||||
currentPartition.push(thisPoint)
|
||||
if (acuteness < 0.25) {
|
||||
// this is not an elbow, bail out
|
||||
|
||||
if (dpr > 0.7) {
|
||||
// Not an elbow
|
||||
continue
|
||||
}
|
||||
|
||||
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
||||
// away from it's neighbors
|
||||
const avgRadius = (prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3
|
||||
const incomingNormalizedDist = Vec.Dist(prevPoint.point, thisPoint.point) / avgRadius
|
||||
const outgoingNormalizedDist = Vec.Dist(thisPoint.point, nextPoint.point) / avgRadius
|
||||
// angular dist is a normalized representation of how far away the point is from it's neighbors
|
||||
// away from it's neighbors, angular dist is a normalized representation of how far away the point is from it's neighbors
|
||||
// (normalized by the radius)
|
||||
const angularDist = incomingNormalizedDist + outgoingNormalizedDist
|
||||
if (angularDist < 1.5) {
|
||||
if (
|
||||
(Vec.Dist2(prevPoint.point, thisPoint.point) + Vec.Dist2(thisPoint.point, nextPoint.point)) /
|
||||
((prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3) ** 2 <
|
||||
1.5
|
||||
) {
|
||||
// if this point is kinda close to its neighbors and it has a reasonably
|
||||
// acute angle, it's probably a hard elbow
|
||||
currentPartition.push(thisPoint)
|
||||
|
@ -89,11 +85,13 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|||
function cleanUpPartition(partition: StrokePoint[]) {
|
||||
// clean up start of partition (remove points that are too close to the start)
|
||||
const startPoint = partition[0]
|
||||
let nextPoint: StrokePoint
|
||||
while (partition.length > 2) {
|
||||
const nextPoint = partition[1]
|
||||
const dist = Vec.Dist(startPoint.point, nextPoint.point)
|
||||
const avgRadius = (startPoint.radius + nextPoint.radius) / 2
|
||||
if (dist < avgRadius * 0.5) {
|
||||
nextPoint = partition[1]
|
||||
if (
|
||||
Vec.Dist2(startPoint.point, nextPoint.point) <
|
||||
(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2
|
||||
) {
|
||||
partition.splice(1, 1)
|
||||
} else {
|
||||
break
|
||||
|
@ -101,11 +99,13 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
|||
}
|
||||
// clean up end of partition in the same fashion
|
||||
const endPoint = partition[partition.length - 1]
|
||||
let prevPoint: StrokePoint
|
||||
while (partition.length > 2) {
|
||||
const prevPoint = partition[partition.length - 2]
|
||||
const dist = Vec.Dist(endPoint.point, prevPoint.point)
|
||||
const avgRadius = (endPoint.radius + prevPoint.radius) / 2
|
||||
if (dist < avgRadius * 0.5) {
|
||||
prevPoint = partition[partition.length - 2]
|
||||
if (
|
||||
Vec.Dist2(endPoint.point, prevPoint.point) <
|
||||
(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2
|
||||
) {
|
||||
partition.splice(partition.length - 2, 1)
|
||||
} else {
|
||||
break
|
||||
|
@ -115,13 +115,14 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
|||
if (partition.length > 1) {
|
||||
partition[0] = {
|
||||
...partition[0],
|
||||
vector: Vec.FromAngle(Vec.Angle(partition[1].point, partition[0].point)),
|
||||
vector: Vec.Sub(partition[0].point, partition[1].point).uni(),
|
||||
}
|
||||
partition[partition.length - 1] = {
|
||||
...partition[partition.length - 1],
|
||||
vector: Vec.FromAngle(
|
||||
Vec.Angle(partition[partition.length - 1].point, partition[partition.length - 2].point)
|
||||
),
|
||||
vector: Vec.Sub(
|
||||
partition[partition.length - 2].point,
|
||||
partition[partition.length - 1].point
|
||||
).uni(),
|
||||
}
|
||||
}
|
||||
return partition
|
||||
|
|
1287
packages/tldraw/src/test/__snapshots__/drawing.test.ts.snap
Normal file
1287
packages/tldraw/src/test/__snapshots__/drawing.test.ts.snap
Normal file
File diff suppressed because it is too large
Load diff
1006
packages/tldraw/src/test/drawing.data.ts
Normal file
1006
packages/tldraw/src/test/drawing.data.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
|||
import { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
|
||||
import { TestEditor } from './TestEditor'
|
||||
import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
|
@ -260,3 +261,22 @@ for (const toolType of ['draw', 'highlight'] as const) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
it('Draws a bunch', () => {
|
||||
editor.setCurrentTool('draw').setCamera({ x: 0, y: 0, z: 1 })
|
||||
|
||||
const [first, ...rest] = TEST_DRAW_SHAPE_SCREEN_POINTS
|
||||
editor.pointerMove(first.x, first.y).pointerDown()
|
||||
|
||||
for (const point of rest) {
|
||||
editor.pointerMove(point.x, point.y)
|
||||
}
|
||||
|
||||
editor.pointerUp()
|
||||
editor.selectAll()
|
||||
|
||||
const shape = { ...editor.getLastCreatedShape() }
|
||||
// @ts-expect-error
|
||||
delete shape.id
|
||||
expect(shape).toMatchSnapshot('draw shape')
|
||||
})
|
||||
|
|
|
@ -34,15 +34,17 @@ export function measureAverageDuration(
|
|||
const start = performance.now()
|
||||
const result = originalMethod.apply(this, args)
|
||||
const end = performance.now()
|
||||
const value = averages.get(descriptor.value)!
|
||||
const length = end - start
|
||||
const total = value.total + length
|
||||
const count = value.count + 1
|
||||
averages.set(descriptor.value, { total, count })
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
|
||||
)
|
||||
if (length !== 0) {
|
||||
const value = averages.get(descriptor.value)!
|
||||
const total = value.total + length
|
||||
const count = value.count + 1
|
||||
averages.set(descriptor.value, { total, count })
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
averages.set(descriptor.value, { total: 0, count: 0 })
|
||||
|
|
Loading…
Reference in a new issue