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.mergeNextPoint = false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateShapes()
|
this.updateDrawingShape()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ export class Drawing extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.updateShapes()
|
this.updateDrawingShape()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||||
|
@ -137,7 +137,7 @@ export class Drawing extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateShapes()
|
this.updateDrawingShape()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit? = () => {
|
override onExit? = () => {
|
||||||
|
@ -281,7 +281,7 @@ export class Drawing extends StateNode {
|
||||||
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateShapes() {
|
private updateDrawingShape() {
|
||||||
const { initialShape } = this
|
const { initialShape } = this
|
||||||
const { inputs } = this.editor
|
const { inputs } = this.editor
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import {
|
import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'
|
||||||
Vec,
|
|
||||||
VecLike,
|
|
||||||
assert,
|
|
||||||
average,
|
|
||||||
precise,
|
|
||||||
shortAngleDist,
|
|
||||||
toDomPrecision,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
||||||
import { getStrokePoints } from './getStrokePoints'
|
import { getStrokePoints } from './getStrokePoints'
|
||||||
import { setStrokePointRadii } from './setStrokePointRadii'
|
import { setStrokePointRadii } from './setStrokePointRadii'
|
||||||
|
@ -36,17 +28,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
||||||
|
|
||||||
const result: StrokePoint[][] = []
|
const result: StrokePoint[][] = []
|
||||||
let currentPartition: StrokePoint[] = [points[0]]
|
let currentPartition: StrokePoint[] = [points[0]]
|
||||||
for (let i = 1; i < points.length - 1; i++) {
|
let prevV = Vec.Sub(points[1].point, points[0].point).uni()
|
||||||
const prevPoint = points[i - 1]
|
let nextV: Vec
|
||||||
const thisPoint = points[i]
|
let dpr: number
|
||||||
const nextPoint = points[i + 1]
|
let prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint
|
||||||
const prevAngle = Vec.Angle(prevPoint.point, thisPoint.point)
|
for (let i = 1, n = points.length; i < n - 1; i++) {
|
||||||
const nextAngle = Vec.Angle(thisPoint.point, nextPoint.point)
|
prevPoint = points[i - 1]
|
||||||
// acuteness is a normalized representation of how acute the angle is.
|
thisPoint = points[i]
|
||||||
// 1 is an infinitely thin wedge
|
nextPoint = points[i + 1]
|
||||||
// 0 is a straight line
|
|
||||||
const acuteness = Math.abs(shortAngleDist(prevAngle, nextAngle)) / Math.PI
|
nextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()
|
||||||
if (acuteness > 0.8) {
|
dpr = Vec.Dpr(prevV, nextV)
|
||||||
|
prevV = nextV
|
||||||
|
|
||||||
|
if (dpr < -0.8) {
|
||||||
// always treat such acute angles as elbows
|
// always treat such acute angles as elbows
|
||||||
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
||||||
const elbowPoint = {
|
const elbowPoint = {
|
||||||
|
@ -59,19 +54,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
currentPartition.push(thisPoint)
|
currentPartition.push(thisPoint)
|
||||||
if (acuteness < 0.25) {
|
|
||||||
// this is not an elbow, bail out
|
if (dpr > 0.7) {
|
||||||
|
// Not an elbow
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
||||||
// away 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
|
||||||
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
|
|
||||||
// (normalized by the radius)
|
// (normalized by the radius)
|
||||||
const angularDist = incomingNormalizedDist + outgoingNormalizedDist
|
if (
|
||||||
if (angularDist < 1.5) {
|
(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
|
// if this point is kinda close to its neighbors and it has a reasonably
|
||||||
// acute angle, it's probably a hard elbow
|
// acute angle, it's probably a hard elbow
|
||||||
currentPartition.push(thisPoint)
|
currentPartition.push(thisPoint)
|
||||||
|
@ -89,11 +85,13 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
||||||
function cleanUpPartition(partition: StrokePoint[]) {
|
function cleanUpPartition(partition: StrokePoint[]) {
|
||||||
// clean up start of partition (remove points that are too close to the start)
|
// clean up start of partition (remove points that are too close to the start)
|
||||||
const startPoint = partition[0]
|
const startPoint = partition[0]
|
||||||
|
let nextPoint: StrokePoint
|
||||||
while (partition.length > 2) {
|
while (partition.length > 2) {
|
||||||
const nextPoint = partition[1]
|
nextPoint = partition[1]
|
||||||
const dist = Vec.Dist(startPoint.point, nextPoint.point)
|
if (
|
||||||
const avgRadius = (startPoint.radius + nextPoint.radius) / 2
|
Vec.Dist2(startPoint.point, nextPoint.point) <
|
||||||
if (dist < avgRadius * 0.5) {
|
(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2
|
||||||
|
) {
|
||||||
partition.splice(1, 1)
|
partition.splice(1, 1)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
@ -101,11 +99,13 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
||||||
}
|
}
|
||||||
// clean up end of partition in the same fashion
|
// clean up end of partition in the same fashion
|
||||||
const endPoint = partition[partition.length - 1]
|
const endPoint = partition[partition.length - 1]
|
||||||
|
let prevPoint: StrokePoint
|
||||||
while (partition.length > 2) {
|
while (partition.length > 2) {
|
||||||
const prevPoint = partition[partition.length - 2]
|
prevPoint = partition[partition.length - 2]
|
||||||
const dist = Vec.Dist(endPoint.point, prevPoint.point)
|
if (
|
||||||
const avgRadius = (endPoint.radius + prevPoint.radius) / 2
|
Vec.Dist2(endPoint.point, prevPoint.point) <
|
||||||
if (dist < avgRadius * 0.5) {
|
(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2
|
||||||
|
) {
|
||||||
partition.splice(partition.length - 2, 1)
|
partition.splice(partition.length - 2, 1)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
@ -115,13 +115,14 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
||||||
if (partition.length > 1) {
|
if (partition.length > 1) {
|
||||||
partition[0] = {
|
partition[0] = {
|
||||||
...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] = {
|
||||||
...partition[partition.length - 1],
|
...partition[partition.length - 1],
|
||||||
vector: Vec.FromAngle(
|
vector: Vec.Sub(
|
||||||
Vec.Angle(partition[partition.length - 1].point, partition[partition.length - 2].point)
|
partition[partition.length - 2].point,
|
||||||
),
|
partition[partition.length - 1].point
|
||||||
|
).uni(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return partition
|
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 { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
|
||||||
|
|
||||||
jest.useFakeTimers()
|
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 start = performance.now()
|
||||||
const result = originalMethod.apply(this, args)
|
const result = originalMethod.apply(this, args)
|
||||||
const end = performance.now()
|
const end = performance.now()
|
||||||
const value = averages.get(descriptor.value)!
|
|
||||||
const length = end - start
|
const length = end - start
|
||||||
const total = value.total + length
|
if (length !== 0) {
|
||||||
const count = value.count + 1
|
const value = averages.get(descriptor.value)!
|
||||||
averages.set(descriptor.value, { total, count })
|
const total = value.total + length
|
||||||
// eslint-disable-next-line no-console
|
const count = value.count + 1
|
||||||
console.log(
|
averages.set(descriptor.value, { total, count })
|
||||||
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
|
// eslint-disable-next-line no-console
|
||||||
)
|
console.log(
|
||||||
|
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
|
||||||
|
)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
averages.set(descriptor.value, { total: 0, count: 0 })
|
averages.set(descriptor.value, { total: 0, count: 0 })
|
||||||
|
|
Loading…
Reference in a new issue