) => {
- const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, e.shiftKey, true)
+ if (!minimapRef.current) return
+ const point = minimapRef.current.minimapScreenPointToPagePoint(
+ e.clientX,
+ e.clientY,
+ e.shiftKey,
+ true
+ )
if (rPointing.current) {
- if (minimap.isInViewport) {
- const delta = minimap.originPagePoint.clone().sub(minimap.originPageCenter)
+ if (minimapRef.current.isInViewport) {
+ const delta = minimapRef.current.originPagePoint
+ .clone()
+ .sub(minimapRef.current.originPageCenter)
editor.centerOnPoint(Vec.Sub(point, delta))
return
}
@@ -115,7 +129,7 @@ export function DefaultMinimap() {
editor.centerOnPoint(point)
}
- const pagePoint = minimap.getPagePoint(e.clientX, e.clientY)
+ const pagePoint = minimapRef.current.getPagePoint(e.clientX, e.clientY)
const screenPoint = editor.pageToScreen(pagePoint)
@@ -130,7 +144,7 @@ export function DefaultMinimap() {
editor.dispatch(info)
},
- [editor, minimap]
+ [editor]
)
const onWheel = React.useCallback(
@@ -150,73 +164,16 @@ export function DefaultMinimap() {
[editor]
)
- // Update the minimap's dpr when the dpr changes
- useQuickReactor(
- 'update when dpr changes',
- () => {
- const dpr = devicePixelRatio.get()
- minimap.setDpr(dpr)
+ const isDarkMode = useIsDarkMode()
- const canvas = rCanvas.current as HTMLCanvasElement
- const rect = canvas.getBoundingClientRect()
- const width = rect.width * dpr
- const height = rect.height * dpr
-
- // These must happen in order
- canvas.width = width
- canvas.height = height
- minimap.canvasScreenBounds.set(rect.x, rect.y, width, height)
-
- minimap.cvs = rCanvas.current
- },
- [devicePixelRatio, minimap]
- )
-
- useQuickReactor(
- 'minimap render when pagebounds or collaborators changes',
- () => {
- const shapeIdsOnCurrentPage = editor.getCurrentPageShapeIds()
- const commonBoundsOfAllShapesOnCurrentPage = editor.getCurrentPageBounds()
- const viewportPageBounds = editor.getViewportPageBounds()
-
- const _dpr = devicePixelRatio.get() // dereference
-
- minimap.contentPageBounds = commonBoundsOfAllShapesOnCurrentPage
- ? Box.Expand(commonBoundsOfAllShapesOnCurrentPage, viewportPageBounds)
- : viewportPageBounds
-
- minimap.updateContentScreenBounds()
-
- // All shape bounds
-
- const allShapeBounds = [] as (Box & { id: TLShapeId })[]
-
- shapeIdsOnCurrentPage.forEach((id) => {
- let pageBounds = editor.getShapePageBounds(id) as Box & { id: TLShapeId }
- if (!pageBounds) return
-
- const pageMask = editor.getShapeMask(id)
-
- if (pageMask) {
- const intersection = intersectPolygonPolygon(pageMask, pageBounds.corners)
- if (!intersection) {
- return
- }
- pageBounds = Box.FromPoints(intersection) as Box & { id: TLShapeId }
- }
-
- if (pageBounds) {
- pageBounds.id = id // kinda dirty but we want to include the id here
- allShapeBounds.push(pageBounds)
- }
- })
-
- minimap.pageBounds = allShapeBounds
- minimap.collaborators = presences.get()
- minimap.render()
- },
- [editor, minimap]
- )
+ React.useEffect(() => {
+ // need to wait a tick for next theme css to be applied
+ // otherwise the minimap will render with the wrong colors
+ setTimeout(() => {
+ minimapRef.current?.updateColors()
+ minimapRef.current?.render()
+ })
+ }, [isDarkMode])
return (
diff --git a/packages/tldraw/src/lib/ui/components/Minimap/MinimapManager.ts b/packages/tldraw/src/lib/ui/components/Minimap/MinimapManager.ts
index eeef0fd7f..3e7757b15 100644
--- a/packages/tldraw/src/lib/ui/components/Minimap/MinimapManager.ts
+++ b/packages/tldraw/src/lib/ui/components/Minimap/MinimapManager.ts
@@ -1,114 +1,159 @@
import {
Box,
+ ComputedCache,
Editor,
- PI2,
- TLInstancePresence,
- TLShapeId,
+ TLShape,
Vec,
+ atom,
clamp,
+ computed,
+ react,
uniqueId,
} from '@tldraw/editor'
+import { getRgba } from './getRgba'
+import { BufferStuff, appendVertices, setupWebGl } from './minimap-webgl-setup'
+import { pie, rectangle, roundedRectangle } from './minimap-webgl-shapes'
export class MinimapManager {
- constructor(public editor: Editor) {}
-
- dpr = 1
-
- colors = {
- shapeFill: 'rgba(144, 144, 144, .1)',
- selectFill: '#2f80ed',
- viewportFill: 'rgba(144, 144, 144, .1)',
+ disposables = [] as (() => void)[]
+ close = () => this.disposables.forEach((d) => d())
+ gl: ReturnType
+ shapeGeometryCache: ComputedCache
+ constructor(
+ public editor: Editor,
+ public readonly elem: HTMLCanvasElement
+ ) {
+ this.gl = setupWebGl(elem)
+ this.shapeGeometryCache = editor.store.createComputedCache('webgl-geometry', (r: TLShape) => {
+ const bounds = editor.getShapeMaskedPageBounds(r.id)
+ if (!bounds) return null
+ const arr = new Float32Array(12)
+ rectangle(arr, 0, bounds.x, bounds.y, bounds.w, bounds.h)
+ return arr
+ })
+ this.colors = this._getColors()
+ this.disposables.push(this._listenForCanvasResize(), react('minimap render', this.render))
}
- id = uniqueId()
- cvs: HTMLCanvasElement | null = null
- pageBounds: (Box & { id: TLShapeId })[] = []
- collaborators: TLInstancePresence[] = []
+ private _getColors() {
+ const style = getComputedStyle(this.editor.getContainer())
- canvasScreenBounds = new Box()
- canvasPageBounds = new Box()
+ return {
+ shapeFill: getRgba(style.getPropertyValue('--color-text-3').trim()),
+ selectFill: getRgba(style.getPropertyValue('--color-selected').trim()),
+ viewportFill: getRgba(style.getPropertyValue('--color-muted-1').trim()),
+ }
+ }
- contentPageBounds = new Box()
- contentScreenBounds = new Box()
+ private colors: ReturnType
+ // this should be called after dark/light mode changes have propagated to the dom
+ updateColors() {
+ this.colors = this._getColors()
+ }
+
+ readonly id = uniqueId()
+ @computed
+ getDpr() {
+ return this.editor.getInstanceState().devicePixelRatio
+ }
+
+ @computed
+ getContentPageBounds() {
+ const viewportPageBounds = this.editor.getViewportPageBounds()
+ const commonShapeBounds = this.editor.getCurrentPageBounds()
+ return commonShapeBounds
+ ? Box.Expand(commonShapeBounds, viewportPageBounds)
+ : viewportPageBounds
+ }
+
+ @computed
+ getContentScreenBounds() {
+ const contentPageBounds = this.getContentPageBounds()
+ const topLeft = this.editor.pageToScreen(contentPageBounds.point)
+ const bottomRight = this.editor.pageToScreen(
+ new Vec(contentPageBounds.maxX, contentPageBounds.maxY)
+ )
+ return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
+ }
+
+ private _getCanvasBoundingRect() {
+ const { x, y, width, height } = this.elem.getBoundingClientRect()
+ return new Box(x, y, width, height)
+ }
+
+ private readonly canvasBoundingClientRect = atom('canvasBoundingClientRect', new Box())
+
+ getCanvasScreenBounds() {
+ return this.canvasBoundingClientRect.get()
+ }
+
+ private _listenForCanvasResize() {
+ const observer = new ResizeObserver(() => {
+ const rect = this._getCanvasBoundingRect()
+ this.canvasBoundingClientRect.set(rect)
+ })
+ observer.observe(this.elem)
+ return () => observer.disconnect()
+ }
+
+ @computed
+ getCanvasSize() {
+ const rect = this.canvasBoundingClientRect.get()
+ const dpr = this.getDpr()
+ return new Vec(rect.width * dpr, rect.height * dpr)
+ }
+
+ @computed
+ getCanvasClientPosition() {
+ return this.canvasBoundingClientRect.get().point
+ }
originPagePoint = new Vec()
originPageCenter = new Vec()
isInViewport = false
- debug = false
+ /** Get the canvas's true bounds converted to page bounds. */
+ @computed getCanvasPageBounds() {
+ const canvasScreenBounds = this.getCanvasScreenBounds()
+ const contentPageBounds = this.getContentPageBounds()
- setDpr(dpr: number) {
- this.dpr = +dpr.toFixed(2)
- }
+ const aspectRatio = canvasScreenBounds.width / canvasScreenBounds.height
- updateContentScreenBounds = () => {
- const { contentScreenBounds, contentPageBounds: content, canvasScreenBounds: canvas } = this
-
- let { x, y, w, h } = contentScreenBounds
-
- if (content.w > content.h) {
- const sh = canvas.w / (content.w / content.h)
- if (sh > canvas.h) {
- x = (canvas.w - canvas.w * (canvas.h / sh)) / 2
- y = 0
- w = canvas.w * (canvas.h / sh)
- h = canvas.h
- } else {
- x = 0
- y = (canvas.h - sh) / 2
- w = canvas.w
- h = sh
- }
- } else if (content.w < content.h) {
- const sw = canvas.h / (content.h / content.w)
- x = (canvas.w - sw) / 2
- y = 0
- w = sw
- h = canvas.h
- } else {
- x = canvas.h / 2
- y = 0
- w = canvas.h
- h = canvas.h
+ let targetWidth = contentPageBounds.width
+ let targetHeight = targetWidth / aspectRatio
+ if (targetHeight < contentPageBounds.height) {
+ targetHeight = contentPageBounds.height
+ targetWidth = targetHeight * aspectRatio
}
- contentScreenBounds.set(x, y, w, h)
+ const box = new Box(0, 0, targetWidth, targetHeight)
+ box.center = contentPageBounds.center
+ return box
}
- /** Get the canvas's true bounds converted to page bounds. */
- updateCanvasPageBounds = () => {
- const { canvasPageBounds, canvasScreenBounds, contentPageBounds, contentScreenBounds } = this
-
- canvasPageBounds.set(
- 0,
- 0,
- contentPageBounds.width / (contentScreenBounds.width / canvasScreenBounds.width),
- contentPageBounds.height / (contentScreenBounds.height / canvasScreenBounds.height)
- )
-
- canvasPageBounds.center = contentPageBounds.center
+ @computed getCanvasPageBoundsArray() {
+ const { x, y, w, h } = this.getCanvasPageBounds()
+ return new Float32Array([x, y, w, h])
}
- getScreenPoint = (x: number, y: number) => {
- const { canvasScreenBounds } = this
+ getPagePoint = (clientX: number, clientY: number) => {
+ const canvasPageBounds = this.getCanvasPageBounds()
+ const canvasScreenBounds = this.getCanvasScreenBounds()
- const screenX = (x - canvasScreenBounds.minX) * this.dpr
- const screenY = (y - canvasScreenBounds.minY) * this.dpr
+ // first offset the canvas position
+ let x = clientX - canvasScreenBounds.x
+ let y = clientY - canvasScreenBounds.y
- return { x: screenX, y: screenY }
- }
+ // then multiply by the ratio between the page and screen bounds
+ x *= canvasPageBounds.width / canvasScreenBounds.width
+ y *= canvasPageBounds.height / canvasScreenBounds.height
- getPagePoint = (x: number, y: number) => {
- const { contentPageBounds, contentScreenBounds, canvasPageBounds } = this
+ // then add the canvas page bounds' offset
+ x += canvasPageBounds.minX
+ y += canvasPageBounds.minY
- const { x: screenX, y: screenY } = this.getScreenPoint(x, y)
-
- return new Vec(
- canvasPageBounds.minX + (screenX * contentPageBounds.width) / contentScreenBounds.width,
- canvasPageBounds.minY + (screenY * contentPageBounds.height) / contentScreenBounds.height,
- 1
- )
+ return new Vec(x, y, 1)
}
minimapScreenPointToPagePoint = (
@@ -123,13 +168,13 @@ export class MinimapManager {
let { x: px, y: py } = this.getPagePoint(x, y)
if (clampToBounds) {
- const shapesPageBounds = this.editor.getCurrentPageBounds()
+ const shapesPageBounds = this.editor.getCurrentPageBounds() ?? new Box()
const vpPageBounds = viewportPageBounds
- const minX = (shapesPageBounds?.minX ?? 0) - vpPageBounds.width / 2
- const maxX = (shapesPageBounds?.maxX ?? 0) + vpPageBounds.width / 2
- const minY = (shapesPageBounds?.minY ?? 0) - vpPageBounds.height / 2
- const maxY = (shapesPageBounds?.maxY ?? 0) + vpPageBounds.height / 2
+ const minX = shapesPageBounds.minX - vpPageBounds.width / 2
+ const maxX = shapesPageBounds.maxX + vpPageBounds.width / 2
+ const minY = shapesPageBounds.minY - vpPageBounds.height / 2
+ const maxY = shapesPageBounds.maxY + vpPageBounds.height / 2
const lx = Math.max(0, minX + vpPageBounds.width - px)
const rx = Math.max(0, -(maxX - vpPageBounds.width - px))
@@ -171,209 +216,110 @@ export class MinimapManager {
return new Vec(px, py)
}
- updateColors = () => {
- const style = getComputedStyle(this.editor.getContainer())
-
- this.colors = {
- shapeFill: style.getPropertyValue('--color-text-3').trim(),
- selectFill: style.getPropertyValue('--color-selected').trim(),
- viewportFill: style.getPropertyValue('--color-muted-1').trim(),
- }
- }
-
render = () => {
- const { cvs, pageBounds } = this
- this.updateCanvasPageBounds()
+ // make sure we update when dark mode switches
+ const context = this.gl.context
+ const canvasSize = this.getCanvasSize()
- const { editor, canvasScreenBounds, canvasPageBounds, contentPageBounds, contentScreenBounds } =
- this
- const { width: cw, height: ch } = canvasScreenBounds
+ this.gl.setCanvasPageBounds(this.getCanvasPageBoundsArray())
- const selectedShapeIds = new Set(editor.getSelectedShapeIds())
- const viewportPageBounds = editor.getViewportPageBounds()
+ this.elem.width = canvasSize.x
+ this.elem.height = canvasSize.y
+ context.viewport(0, 0, canvasSize.x, canvasSize.y)
- if (!cvs || !pageBounds) {
- return
+ // this affects which color transparent shapes are blended with
+ // during rendering. If we were to invert this any shapes narrower
+ // than 1 px in screen space would have much lower contrast. e.g.
+ // draw shapes on a large canvas.
+ if (this.editor.user.getIsDarkMode()) {
+ context.clearColor(1, 1, 1, 0)
+ } else {
+ context.clearColor(0, 0, 0, 0)
}
- const ctx = cvs.getContext('2d')!
+ context.clear(context.COLOR_BUFFER_BIT)
- if (!ctx) {
- throw new Error('Minimap (shapes): Could not get context')
- }
+ const selectedShapes = new Set(this.editor.getSelectedShapeIds())
- ctx.resetTransform()
- ctx.globalAlpha = 1
- ctx.clearRect(0, 0, cw, ch)
+ const colors = this.colors
+ let selectedShapeOffset = 0
+ let unselectedShapeOffset = 0
- // Transform canvas
+ const ids = this.editor.getCurrentPageShapeIdsSorted()
- const sx = contentScreenBounds.width / contentPageBounds.width
- const sy = contentScreenBounds.height / contentPageBounds.height
+ for (let i = 0, len = ids.length; i < len; i++) {
+ const shapeId = ids[i]
+ const geometry = this.shapeGeometryCache.get(shapeId)
+ if (!geometry) continue
- ctx.translate((cw - contentScreenBounds.width) / 2, (ch - contentScreenBounds.height) / 2)
- ctx.scale(sx, sy)
- ctx.translate(-contentPageBounds.minX, -contentPageBounds.minY)
+ const len = geometry.length
- // shapes
- const shapesPath = new Path2D()
- const selectedPath = new Path2D()
-
- const { shapeFill, selectFill, viewportFill } = this.colors
-
- // When there are many shapes, don't draw rounded rectangles;
- // consider using the shape's size instead.
-
- let pb: Box & { id: TLShapeId }
- for (let i = 0, n = pageBounds.length; i < n; i++) {
- pb = pageBounds[i]
- ;(selectedShapeIds.has(pb.id) ? selectedPath : shapesPath).rect(
- pb.minX,
- pb.minY,
- pb.width,
- pb.height
- )
- }
-
- // Fill the shapes paths
- ctx.fillStyle = shapeFill
- ctx.fill(shapesPath)
-
- // Fill the selected paths
- ctx.fillStyle = selectFill
- ctx.fill(selectedPath)
-
- if (this.debug) {
- // Page bounds
- const commonBounds = Box.Common(pageBounds)
- const { minX, minY, width, height } = commonBounds
- ctx.strokeStyle = 'green'
- ctx.lineWidth = 2 / sx
- ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
- }
-
- // Brush
- {
- const { brush } = editor.getInstanceState()
- if (brush) {
- const { x, y, w, h } = brush
- ctx.beginPath()
- MinimapManager.sharpRect(ctx, x, y, w, h)
- ctx.closePath()
- ctx.fillStyle = viewportFill
- ctx.fill()
+ if (selectedShapes.has(shapeId)) {
+ appendVertices(this.gl.selectedShapes, selectedShapeOffset, geometry)
+ selectedShapeOffset += len
+ } else {
+ appendVertices(this.gl.unselectedShapes, unselectedShapeOffset, geometry)
+ unselectedShapeOffset += len
}
}
- // Viewport
- {
- const { minX, minY, width, height } = viewportPageBounds
-
- ctx.beginPath()
-
- const rx = 12 / sx
- const ry = 12 / sx
- MinimapManager.roundedRect(
- ctx,
- minX,
- minY,
- width,
- height,
- Math.min(width / 4, rx),
- Math.min(height / 4, ry)
- )
- ctx.closePath()
- ctx.fillStyle = viewportFill
- ctx.fill()
-
- if (this.debug) {
- ctx.strokeStyle = 'orange'
- ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
- }
- }
-
- // Show collaborator cursors
-
- // Padding for canvas bounds edges
- const px = 2.5 / sx
- const py = 2.5 / sy
-
- const currentPageId = editor.getCurrentPageId()
-
- let collaborator: TLInstancePresence
- for (let i = 0; i < this.collaborators.length; i++) {
- collaborator = this.collaborators[i]
- if (collaborator.currentPageId !== currentPageId) {
- continue
- }
-
- ctx.beginPath()
- ctx.ellipse(
- clamp(collaborator.cursor.x, canvasPageBounds.minX + px, canvasPageBounds.maxX - px),
- clamp(collaborator.cursor.y, canvasPageBounds.minY + py, canvasPageBounds.maxY - py),
- 5 / sx,
- 5 / sy,
- 0,
- 0,
- PI2
- )
- ctx.fillStyle = collaborator.color
- ctx.fill()
- }
-
- if (this.debug) {
- ctx.lineWidth = 2 / sx
-
- {
- // Minimap Bounds
- const { minX, minY, width, height } = contentPageBounds
- ctx.strokeStyle = 'red'
- ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
- }
-
- {
- // Canvas Bounds
- const { minX, minY, width, height } = canvasPageBounds
- ctx.strokeStyle = 'blue'
- ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
- }
- }
+ this.drawViewport()
+ this.drawShapes(this.gl.unselectedShapes, unselectedShapeOffset, colors.shapeFill)
+ this.drawShapes(this.gl.selectedShapes, selectedShapeOffset, colors.selectFill)
+ this.drawCollaborators()
}
- static roundedRect(
- ctx: CanvasRenderingContext2D | Path2D,
- x: number,
- y: number,
- width: number,
- height: number,
- rx: number,
- ry: number
- ) {
- if (rx < 1 && ry < 1) {
- ctx.rect(x, y, width, height)
- return
- }
-
- ctx.moveTo(x + rx, y)
- ctx.lineTo(x + width - rx, y)
- ctx.quadraticCurveTo(x + width, y, x + width, y + ry)
- ctx.lineTo(x + width, y + height - ry)
- ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height)
- ctx.lineTo(x + rx, y + height)
- ctx.quadraticCurveTo(x, y + height, x, y + height - ry)
- ctx.lineTo(x, y + ry)
- ctx.quadraticCurveTo(x, y, x + rx, y)
+ private drawShapes(stuff: BufferStuff, len: number, color: Float32Array) {
+ this.gl.prepareTriangles(stuff, len)
+ this.gl.setFillColor(color)
+ this.gl.drawTriangles(len)
}
- static sharpRect(
- ctx: CanvasRenderingContext2D | Path2D,
- x: number,
- y: number,
- width: number,
- height: number,
- _rx?: number,
- _ry?: number
- ) {
- ctx.rect(x, y, width, height)
+ private drawViewport() {
+ const viewport = this.editor.getViewportPageBounds()
+ const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
+ const len = roundedRectangle(this.gl.viewport.vertices, viewport, 4 * zoom)
+
+ this.gl.prepareTriangles(this.gl.viewport, len)
+ this.gl.setFillColor(this.colors.viewportFill)
+ this.gl.drawTriangles(len)
+ }
+
+ drawCollaborators() {
+ const collaborators = this.editor.getCollaboratorsOnCurrentPage()
+ if (!collaborators.length) return
+
+ const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
+
+ // just draw a little circle for each collaborator
+ const numSegmentsPerCircle = 20
+ const dataSizePerCircle = numSegmentsPerCircle * 6
+ const totalSize = dataSizePerCircle * collaborators.length
+
+ // expand vertex array if needed
+ if (this.gl.collaborators.vertices.length < totalSize) {
+ this.gl.collaborators.vertices = new Float32Array(totalSize)
+ }
+
+ const vertices = this.gl.collaborators.vertices
+ let offset = 0
+ for (const { cursor } of collaborators) {
+ pie(vertices, {
+ center: Vec.From(cursor),
+ radius: 2 * zoom,
+ offset,
+ numArcSegments: numSegmentsPerCircle,
+ })
+ offset += dataSizePerCircle
+ }
+
+ this.gl.prepareTriangles(this.gl.collaborators, totalSize)
+
+ offset = 0
+ for (const { color } of collaborators) {
+ this.gl.setFillColor(getRgba(color))
+ this.gl.context.drawArrays(this.gl.context.TRIANGLES, offset / 2, dataSizePerCircle / 2)
+ offset += dataSizePerCircle
+ }
}
}
diff --git a/packages/tldraw/src/lib/ui/components/Minimap/getRgba.ts b/packages/tldraw/src/lib/ui/components/Minimap/getRgba.ts
new file mode 100644
index 000000000..43726f6b6
--- /dev/null
+++ b/packages/tldraw/src/lib/ui/components/Minimap/getRgba.ts
@@ -0,0 +1,16 @@
+const memo = {} as Record
+
+export function getRgba(colorString: string) {
+ if (memo[colorString]) {
+ return memo[colorString]
+ }
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext('2d')
+ context!.fillStyle = colorString
+ context!.fillRect(0, 0, 1, 1)
+ const [r, g, b, a] = context!.getImageData(0, 0, 1, 1).data
+ const result = new Float32Array([r / 255, g / 255, b / 255, a / 255])
+
+ memo[colorString] = result
+ return result
+}
diff --git a/packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-setup.ts b/packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-setup.ts
new file mode 100644
index 000000000..0f5585d26
--- /dev/null
+++ b/packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-setup.ts
@@ -0,0 +1,148 @@
+import { roundedRectangleDataSize } from './minimap-webgl-shapes'
+
+export function setupWebGl(canvas: HTMLCanvasElement | null) {
+ if (!canvas) throw new Error('Canvas element not found')
+
+ const context = canvas.getContext('webgl2', {
+ premultipliedAlpha: false,
+ })
+ if (!context) throw new Error('Failed to get webgl2 context')
+
+ const vertexShaderSourceCode = `#version 300 es
+ precision mediump float;
+
+ in vec2 shapeVertexPosition;
+
+ uniform vec4 canvasPageBounds;
+
+ // taken (with thanks) from
+ // https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
+ void main() {
+ // convert the position from pixels to 0.0 to 1.0
+ vec2 zeroToOne = (shapeVertexPosition - canvasPageBounds.xy) / canvasPageBounds.zw;
+
+ // convert from 0->1 to 0->2
+ vec2 zeroToTwo = zeroToOne * 2.0;
+
+ // convert from 0->2 to -1->+1 (clipspace)
+ vec2 clipSpace = zeroToTwo - 1.0;
+
+ gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
+ }`
+
+ const vertexShader = context.createShader(context.VERTEX_SHADER)
+ if (!vertexShader) {
+ throw new Error('Failed to create vertex shader')
+ }
+ context.shaderSource(vertexShader, vertexShaderSourceCode)
+ context.compileShader(vertexShader)
+ if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) {
+ throw new Error('Failed to compile vertex shader')
+ }
+
+ const fragmentShaderSourceCode = `#version 300 es
+ precision mediump float;
+
+ uniform vec4 fillColor;
+ out vec4 outputColor;
+
+ void main() {
+ outputColor = fillColor;
+ }`
+
+ const fragmentShader = context.createShader(context.FRAGMENT_SHADER)
+ if (!fragmentShader) {
+ throw new Error('Failed to create fragment shader')
+ }
+ context.shaderSource(fragmentShader, fragmentShaderSourceCode)
+ context.compileShader(fragmentShader)
+ if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) {
+ throw new Error('Failed to compile fragment shader')
+ }
+
+ const program = context.createProgram()
+ if (!program) {
+ throw new Error('Failed to create program')
+ }
+ context.attachShader(program, vertexShader)
+ context.attachShader(program, fragmentShader)
+ context.linkProgram(program)
+ if (!context.getProgramParameter(program, context.LINK_STATUS)) {
+ throw new Error('Failed to link program')
+ }
+ context.useProgram(program)
+
+ const shapeVertexPositionAttributeLocation = context.getAttribLocation(
+ program,
+ 'shapeVertexPosition'
+ )
+ if (shapeVertexPositionAttributeLocation < 0) {
+ throw new Error('Failed to get shapeVertexPosition attribute location')
+ }
+ context.enableVertexAttribArray(shapeVertexPositionAttributeLocation)
+
+ const canvasPageBoundsLocation = context.getUniformLocation(program, 'canvasPageBounds')
+ const fillColorLocation = context.getUniformLocation(program, 'fillColor')
+
+ const selectedShapesBuffer = context.createBuffer()
+ if (!selectedShapesBuffer) throw new Error('Failed to create buffer')
+
+ const unselectedShapesBuffer = context.createBuffer()
+ if (!unselectedShapesBuffer) throw new Error('Failed to create buffer')
+
+ return {
+ context,
+ selectedShapes: allocateBuffer(context, 1024),
+ unselectedShapes: allocateBuffer(context, 4096),
+ viewport: allocateBuffer(context, roundedRectangleDataSize),
+ collaborators: allocateBuffer(context, 1024),
+
+ prepareTriangles(stuff: BufferStuff, len: number) {
+ context.bindBuffer(context.ARRAY_BUFFER, stuff.buffer)
+ context.bufferData(context.ARRAY_BUFFER, stuff.vertices, context.STATIC_DRAW, 0, len)
+ context.enableVertexAttribArray(shapeVertexPositionAttributeLocation)
+ context.vertexAttribPointer(
+ shapeVertexPositionAttributeLocation,
+ 2,
+ context.FLOAT,
+ false,
+ 0,
+ 0
+ )
+ },
+
+ drawTriangles(len: number) {
+ context.drawArrays(context.TRIANGLES, 0, len / 2)
+ },
+
+ setFillColor(color: Float32Array) {
+ context.uniform4fv(fillColorLocation, color)
+ },
+
+ setCanvasPageBounds(bounds: Float32Array) {
+ context.uniform4fv(canvasPageBoundsLocation, bounds)
+ },
+ }
+}
+
+export type BufferStuff = ReturnType
+
+function allocateBuffer(context: WebGL2RenderingContext, size: number) {
+ const buffer = context.createBuffer()
+ if (!buffer) throw new Error('Failed to create buffer')
+ return { buffer, vertices: new Float32Array(size) }
+}
+
+export function appendVertices(bufferStuff: BufferStuff, offset: number, data: Float32Array) {
+ let len = bufferStuff.vertices.length
+ while (len < offset + data.length) {
+ len *= 2
+ }
+ if (len != bufferStuff.vertices.length) {
+ const newVertices = new Float32Array(len)
+ newVertices.set(bufferStuff.vertices)
+ bufferStuff.vertices = newVertices
+ }
+
+ bufferStuff.vertices.set(data, offset)
+}
diff --git a/packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-shapes.ts b/packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-shapes.ts
new file mode 100644
index 000000000..283e89344
--- /dev/null
+++ b/packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-shapes.ts
@@ -0,0 +1,144 @@
+import { Box, HALF_PI, PI, PI2, Vec } from '@tldraw/editor'
+
+export const numArcSegmentsPerCorner = 10
+
+export const roundedRectangleDataSize =
+ // num triangles in corners
+ 4 * 6 * numArcSegmentsPerCorner +
+ // num triangles in center rect
+ 12 +
+ // num triangles in outer rects
+ 4 * 12
+
+export function pie(
+ array: Float32Array,
+ {
+ center,
+ radius,
+ numArcSegments = 20,
+ startAngle = 0,
+ endAngle = PI2,
+ offset = 0,
+ }: {
+ center: Vec
+ radius: number
+ numArcSegments?: number
+ startAngle?: number
+ endAngle?: number
+ offset?: number
+ }
+) {
+ const angle = (endAngle - startAngle) / numArcSegments
+ let i = offset
+ for (let a = startAngle; a < endAngle; a += angle) {
+ array[i++] = center.x
+ array[i++] = center.y
+ array[i++] = center.x + Math.cos(a) * radius
+ array[i++] = center.y + Math.sin(a) * radius
+ array[i++] = center.x + Math.cos(a + angle) * radius
+ array[i++] = center.y + Math.sin(a + angle) * radius
+ }
+ return array
+}
+
+/** @internal **/
+export function rectangle(
+ array: Float32Array,
+ offset: number,
+ x: number,
+ y: number,
+ w: number,
+ h: number
+) {
+ array[offset++] = x
+ array[offset++] = y
+ array[offset++] = x
+ array[offset++] = y + h
+ array[offset++] = x + w
+ array[offset++] = y
+
+ array[offset++] = x + w
+ array[offset++] = y
+ array[offset++] = x
+ array[offset++] = y + h
+ array[offset++] = x + w
+ array[offset++] = y + h
+}
+
+export function roundedRectangle(data: Float32Array, box: Box, radius: number): number {
+ const numArcSegments = numArcSegmentsPerCorner
+ radius = Math.min(radius, Math.min(box.w, box.h) / 2)
+ // first draw the inner box
+ const innerBox = Box.ExpandBy(box, -radius)
+ if (innerBox.w <= 0 || innerBox.h <= 0) {
+ // just draw a circle
+ pie(data, { center: box.center, radius: radius, numArcSegments: numArcSegmentsPerCorner * 4 })
+ return numArcSegmentsPerCorner * 4 * 6
+ }
+ let offset = 0
+ // draw center rect first
+ rectangle(data, offset, innerBox.minX, innerBox.minY, innerBox.w, innerBox.h)
+ offset += 12
+ // then top rect
+ rectangle(data, offset, innerBox.minX, box.minY, innerBox.w, radius)
+ offset += 12
+ // then right rect
+ rectangle(data, offset, innerBox.maxX, innerBox.minY, radius, innerBox.h)
+ offset += 12
+ // then bottom rect
+ rectangle(data, offset, innerBox.minX, innerBox.maxY, innerBox.w, radius)
+ offset += 12
+ // then left rect
+ rectangle(data, offset, box.minX, innerBox.minY, radius, innerBox.h)
+ offset += 12
+
+ // draw the corners
+
+ // top left
+ pie(data, {
+ numArcSegments,
+ offset,
+ center: innerBox.point,
+ radius,
+ startAngle: PI,
+ endAngle: PI * 1.5,
+ })
+
+ offset += numArcSegments * 6
+
+ // top right
+ pie(data, {
+ numArcSegments,
+ offset,
+ center: Vec.Add(innerBox.point, new Vec(innerBox.w, 0)),
+ radius,
+ startAngle: PI * 1.5,
+ endAngle: PI2,
+ })
+
+ offset += numArcSegments * 6
+
+ // bottom right
+ pie(data, {
+ numArcSegments,
+ offset,
+ center: Vec.Add(innerBox.point, innerBox.size),
+ radius,
+ startAngle: 0,
+ endAngle: HALF_PI,
+ })
+
+ offset += numArcSegments * 6
+
+ // bottom left
+ pie(data, {
+ numArcSegments,
+ offset,
+ center: Vec.Add(innerBox.point, new Vec(0, innerBox.h)),
+ radius,
+ startAngle: HALF_PI,
+ endAngle: PI,
+ })
+
+ return roundedRectangleDataSize
+}
diff --git a/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts b/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts
index f57dbd81f..b26f39ab4 100644
--- a/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts
+++ b/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts
@@ -9,6 +9,8 @@ import {
TLTextShape,
VecLike,
isNonNull,
+ preventDefault,
+ stopEventPropagation,
uniq,
useEditor,
useValue,
@@ -615,24 +617,29 @@ export function useNativeClipboardEvents() {
useEffect(() => {
if (!appIsFocused) return
- const copy = () => {
+ const copy = (e: ClipboardEvent) => {
if (
editor.getSelectedShapeIds().length === 0 ||
editor.getEditingShapeId() !== null ||
disallowClipboardEvents(editor)
- )
+ ) {
return
+ }
+
+ preventDefault(e)
handleNativeOrMenuCopy(editor)
trackEvent('copy', { source: 'kbd' })
}
- function cut() {
+ function cut(e: ClipboardEvent) {
if (
editor.getSelectedShapeIds().length === 0 ||
editor.getEditingShapeId() !== null ||
disallowClipboardEvents(editor)
- )
+ ) {
return
+ }
+ preventDefault(e)
handleNativeOrMenuCopy(editor)
editor.deleteShapes(editor.getSelectedShapeIds())
trackEvent('cut', { source: 'kbd' })
@@ -648,9 +655,9 @@ export function useNativeClipboardEvents() {
}
}
- const paste = (event: ClipboardEvent) => {
+ const paste = (e: ClipboardEvent) => {
if (disablingMiddleClickPaste) {
- event.stopPropagation()
+ stopEventPropagation(e)
return
}
@@ -660,8 +667,8 @@ export function useNativeClipboardEvents() {
if (editor.getEditingShapeId() !== null || disallowClipboardEvents(editor)) return
// First try to use the clipboard data on the event
- if (event.clipboardData && !editor.inputs.shiftKey) {
- handlePasteFromEventClipboardData(editor, event.clipboardData)
+ if (e.clipboardData && !editor.inputs.shiftKey) {
+ handlePasteFromEventClipboardData(editor, e.clipboardData)
} else {
// Or else use the clipboard API
navigator.clipboard.read().then((clipboardItems) => {
@@ -671,6 +678,7 @@ export function useNativeClipboardEvents() {
})
}
+ preventDefault(e)
trackEvent('paste', { source: 'kbd' })
}
diff --git a/packages/tldraw/src/lib/ui/hooks/usePreloadAssets.ts b/packages/tldraw/src/lib/ui/hooks/usePreloadAssets.ts
index 7aa1f23e3..0adc2f9ba 100644
--- a/packages/tldraw/src/lib/ui/hooks/usePreloadAssets.ts
+++ b/packages/tldraw/src/lib/ui/hooks/usePreloadAssets.ts
@@ -56,6 +56,7 @@ function getTypefaces(assetUrls: TLEditorAssetUrls) {
}
}
+/** @public */
export function usePreloadAssets(assetUrls: TLEditorAssetUrls) {
const typefaces = useMemo(() => getTypefaces(assetUrls), [assetUrls])
diff --git a/packages/tldraw/src/test/__snapshots__/drawing.test.ts.snap b/packages/tldraw/src/test/__snapshots__/drawing.test.ts.snap
new file mode 100644
index 000000000..d0450b5e3
--- /dev/null
+++ b/packages/tldraw/src/test/__snapshots__/drawing.test.ts.snap
@@ -0,0 +1,1287 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Draws a bunch: draw shape 1`] = `
+{
+ "index": "a1",
+ "isLocked": false,
+ "meta": {},
+ "opacity": 1,
+ "parentId": "page:page",
+ "props": {
+ "color": "black",
+ "dash": "draw",
+ "fill": "none",
+ "isClosed": false,
+ "isComplete": true,
+ "isPen": false,
+ "segments": [
+ {
+ "points": [
+ {
+ "x": 0,
+ "y": 0,
+ "z": 0.5,
+ },
+ {
+ "x": 1,
+ "y": 0,
+ "z": 0.5,
+ },
+ {
+ "x": 4,
+ "y": 0,
+ "z": 0.5,
+ },
+ {
+ "x": 10,
+ "y": -1,
+ "z": 0.5,
+ },
+ {
+ "x": 19,
+ "y": -4,
+ "z": 0.5,
+ },
+ {
+ "x": 30,
+ "y": -10,
+ "z": 0.5,
+ },
+ {
+ "x": 46,
+ "y": -20,
+ "z": 0.5,
+ },
+ {
+ "x": 61,
+ "y": -30,
+ "z": 0.5,
+ },
+ {
+ "x": 74,
+ "y": -43,
+ "z": 0.5,
+ },
+ {
+ "x": 89,
+ "y": -59,
+ "z": 0.5,
+ },
+ {
+ "x": 102,
+ "y": -77,
+ "z": 0.5,
+ },
+ {
+ "x": 108,
+ "y": -90,
+ "z": 0.5,
+ },
+ {
+ "x": 112,
+ "y": -103,
+ "z": 0.5,
+ },
+ {
+ "x": 117,
+ "y": -119,
+ "z": 0.5,
+ },
+ {
+ "x": 118,
+ "y": -131,
+ "z": 0.5,
+ },
+ {
+ "x": 119,
+ "y": -137,
+ "z": 0.5,
+ },
+ {
+ "x": 119,
+ "y": -145,
+ "z": 0.5,
+ },
+ {
+ "x": 120,
+ "y": -152,
+ "z": 0.5,
+ },
+ {
+ "x": 119,
+ "y": -158,
+ "z": 0.5,
+ },
+ {
+ "x": 117,
+ "y": -163,
+ "z": 0.5,
+ },
+ {
+ "x": 114,
+ "y": -167,
+ "z": 0.5,
+ },
+ {
+ "x": 109,
+ "y": -169,
+ "z": 0.5,
+ },
+ {
+ "x": 103,
+ "y": -170,
+ "z": 0.5,
+ },
+ {
+ "x": 97,
+ "y": -170,
+ "z": 0.5,
+ },
+ {
+ "x": 89,
+ "y": -170,
+ "z": 0.5,
+ },
+ {
+ "x": 80,
+ "y": -166,
+ "z": 0.5,
+ },
+ {
+ "x": 71,
+ "y": -159,
+ "z": 0.5,
+ },
+ {
+ "x": 62,
+ "y": -150,
+ "z": 0.5,
+ },
+ {
+ "x": 54,
+ "y": -138,
+ "z": 0.5,
+ },
+ {
+ "x": 50,
+ "y": -126,
+ "z": 0.5,
+ },
+ {
+ "x": 47,
+ "y": -113,
+ "z": 0.5,
+ },
+ {
+ "x": 46,
+ "y": -99,
+ "z": 0.5,
+ },
+ {
+ "x": 46,
+ "y": -82,
+ "z": 0.5,
+ },
+ {
+ "x": 47,
+ "y": -61,
+ "z": 0.5,
+ },
+ {
+ "x": 53,
+ "y": -41,
+ "z": 0.5,
+ },
+ {
+ "x": 60,
+ "y": -24,
+ "z": 0.5,
+ },
+ {
+ "x": 68,
+ "y": -7,
+ "z": 0.5,
+ },
+ {
+ "x": 79,
+ "y": 12,
+ "z": 0.5,
+ },
+ {
+ "x": 88,
+ "y": 32,
+ "z": 0.5,
+ },
+ {
+ "x": 96,
+ "y": 50,
+ "z": 0.5,
+ },
+ {
+ "x": 103,
+ "y": 69,
+ "z": 0.5,
+ },
+ {
+ "x": 106,
+ "y": 86,
+ "z": 0.5,
+ },
+ {
+ "x": 107,
+ "y": 102,
+ "z": 0.5,
+ },
+ {
+ "x": 107,
+ "y": 120,
+ "z": 0.5,
+ },
+ {
+ "x": 102,
+ "y": 136,
+ "z": 0.5,
+ },
+ {
+ "x": 90,
+ "y": 146,
+ "z": 0.5,
+ },
+ {
+ "x": 74,
+ "y": 154,
+ "z": 0.5,
+ },
+ {
+ "x": 43,
+ "y": 163,
+ "z": 0.5,
+ },
+ {
+ "x": 32,
+ "y": 164,
+ "z": 0.5,
+ },
+ {
+ "x": 21,
+ "y": 164,
+ "z": 0.5,
+ },
+ {
+ "x": 11,
+ "y": 164,
+ "z": 0.5,
+ },
+ {
+ "x": 2,
+ "y": 164,
+ "z": 0.5,
+ },
+ {
+ "x": -7,
+ "y": 162,
+ "z": 0.5,
+ },
+ {
+ "x": -13,
+ "y": 159,
+ "z": 0.5,
+ },
+ {
+ "x": -15,
+ "y": 153,
+ "z": 0.5,
+ },
+ {
+ "x": -15,
+ "y": 147,
+ "z": 0.5,
+ },
+ {
+ "x": -11,
+ "y": 138,
+ "z": 0.5,
+ },
+ {
+ "x": 1,
+ "y": 127,
+ "z": 0.5,
+ },
+ {
+ "x": 15,
+ "y": 112,
+ "z": 0.5,
+ },
+ {
+ "x": 34,
+ "y": 96,
+ "z": 0.5,
+ },
+ {
+ "x": 56,
+ "y": 79,
+ "z": 0.5,
+ },
+ {
+ "x": 81,
+ "y": 58,
+ "z": 0.5,
+ },
+ {
+ "x": 107,
+ "y": 33,
+ "z": 0.5,
+ },
+ {
+ "x": 126,
+ "y": 12,
+ "z": 0.5,
+ },
+ {
+ "x": 145,
+ "y": -10,
+ "z": 0.5,
+ },
+ {
+ "x": 160,
+ "y": -30,
+ "z": 0.5,
+ },
+ {
+ "x": 172,
+ "y": -50,
+ "z": 0.5,
+ },
+ {
+ "x": 185,
+ "y": -73,
+ "z": 0.5,
+ },
+ {
+ "x": 194,
+ "y": -93,
+ "z": 0.5,
+ },
+ {
+ "x": 199,
+ "y": -112,
+ "z": 0.5,
+ },
+ {
+ "x": 202,
+ "y": -127,
+ "z": 0.5,
+ },
+ {
+ "x": 203,
+ "y": -138,
+ "z": 0.5,
+ },
+ {
+ "x": 203,
+ "y": -146,
+ "z": 0.5,
+ },
+ {
+ "x": 201,
+ "y": -152,
+ "z": 0.5,
+ },
+ {
+ "x": 196,
+ "y": -155,
+ "z": 0.5,
+ },
+ {
+ "x": 191,
+ "y": -156,
+ "z": 0.5,
+ },
+ {
+ "x": 186,
+ "y": -157,
+ "z": 0.5,
+ },
+ {
+ "x": 178,
+ "y": -156,
+ "z": 0.5,
+ },
+ {
+ "x": 170,
+ "y": -150,
+ "z": 0.5,
+ },
+ {
+ "x": 164,
+ "y": -140,
+ "z": 0.5,
+ },
+ {
+ "x": 158,
+ "y": -128,
+ "z": 0.5,
+ },
+ {
+ "x": 151,
+ "y": -110,
+ "z": 0.5,
+ },
+ {
+ "x": 144,
+ "y": -89,
+ "z": 0.5,
+ },
+ {
+ "x": 139,
+ "y": -64,
+ "z": 0.5,
+ },
+ {
+ "x": 135,
+ "y": -36,
+ "z": 0.5,
+ },
+ {
+ "x": 132,
+ "y": -7,
+ "z": 0.5,
+ },
+ {
+ "x": 132,
+ "y": 22,
+ "z": 0.5,
+ },
+ {
+ "x": 132,
+ "y": 49,
+ "z": 0.5,
+ },
+ {
+ "x": 133,
+ "y": 74,
+ "z": 0.5,
+ },
+ {
+ "x": 140,
+ "y": 97,
+ "z": 0.5,
+ },
+ {
+ "x": 148,
+ "y": 113,
+ "z": 0.5,
+ },
+ {
+ "x": 156,
+ "y": 124,
+ "z": 0.5,
+ },
+ {
+ "x": 166,
+ "y": 137,
+ "z": 0.5,
+ },
+ {
+ "x": 175,
+ "y": 145,
+ "z": 0.5,
+ },
+ {
+ "x": 183,
+ "y": 150,
+ "z": 0.5,
+ },
+ {
+ "x": 191,
+ "y": 152,
+ "z": 0.5,
+ },
+ {
+ "x": 197,
+ "y": 152,
+ "z": 0.5,
+ },
+ {
+ "x": 205,
+ "y": 151,
+ "z": 0.5,
+ },
+ {
+ "x": 214,
+ "y": 146,
+ "z": 0.5,
+ },
+ {
+ "x": 223,
+ "y": 136,
+ "z": 0.5,
+ },
+ {
+ "x": 230,
+ "y": 125,
+ "z": 0.5,
+ },
+ {
+ "x": 236,
+ "y": 112,
+ "z": 0.5,
+ },
+ {
+ "x": 242,
+ "y": 95,
+ "z": 0.5,
+ },
+ {
+ "x": 247,
+ "y": 78,
+ "z": 0.5,
+ },
+ {
+ "x": 250,
+ "y": 61,
+ "z": 0.5,
+ },
+ {
+ "x": 252,
+ "y": 46,
+ "z": 0.5,
+ },
+ {
+ "x": 253,
+ "y": 37,
+ "z": 0.5,
+ },
+ {
+ "x": 253,
+ "y": 31,
+ "z": 0.5,
+ },
+ {
+ "x": 253,
+ "y": 24,
+ "z": 0.5,
+ },
+ {
+ "x": 251,
+ "y": 20,
+ "z": 0.5,
+ },
+ {
+ "x": 248,
+ "y": 16,
+ "z": 0.5,
+ },
+ {
+ "x": 246,
+ "y": 16,
+ "z": 0.5,
+ },
+ {
+ "x": 243,
+ "y": 16,
+ "z": 0.5,
+ },
+ {
+ "x": 240,
+ "y": 17,
+ "z": 0.5,
+ },
+ {
+ "x": 238,
+ "y": 19,
+ "z": 0.5,
+ },
+ {
+ "x": 236,
+ "y": 26,
+ "z": 0.5,
+ },
+ {
+ "x": 234,
+ "y": 34,
+ "z": 0.5,
+ },
+ {
+ "x": 233,
+ "y": 45,
+ "z": 0.5,
+ },
+ {
+ "x": 232,
+ "y": 56,
+ "z": 0.5,
+ },
+ {
+ "x": 232,
+ "y": 66,
+ "z": 0.5,
+ },
+ {
+ "x": 235,
+ "y": 79,
+ "z": 0.5,
+ },
+ {
+ "x": 241,
+ "y": 91,
+ "z": 0.5,
+ },
+ {
+ "x": 247,
+ "y": 100,
+ "z": 0.5,
+ },
+ {
+ "x": 255,
+ "y": 109,
+ "z": 0.5,
+ },
+ {
+ "x": 260,
+ "y": 113,
+ "z": 0.5,
+ },
+ {
+ "x": 266,
+ "y": 116,
+ "z": 0.5,
+ },
+ {
+ "x": 274,
+ "y": 118,
+ "z": 0.5,
+ },
+ {
+ "x": 280,
+ "y": 118,
+ "z": 0.5,
+ },
+ {
+ "x": 286,
+ "y": 115,
+ "z": 0.5,
+ },
+ {
+ "x": 291,
+ "y": 105,
+ "z": 0.5,
+ },
+ {
+ "x": 296,
+ "y": 93,
+ "z": 0.5,
+ },
+ {
+ "x": 298,
+ "y": 83,
+ "z": 0.5,
+ },
+ {
+ "x": 301,
+ "y": 70,
+ "z": 0.5,
+ },
+ {
+ "x": 303,
+ "y": 58,
+ "z": 0.5,
+ },
+ {
+ "x": 305,
+ "y": 48,
+ "z": 0.5,
+ },
+ {
+ "x": 306,
+ "y": 38,
+ "z": 0.5,
+ },
+ {
+ "x": 307,
+ "y": 31,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 25,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 22,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 20,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 19,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 22,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 27,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 35,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 44,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 51,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 56,
+ "z": 0.5,
+ },
+ {
+ "x": 308,
+ "y": 61,
+ "z": 0.5,
+ },
+ {
+ "x": 309,
+ "y": 66,
+ "z": 0.5,
+ },
+ {
+ "x": 312,
+ "y": 71,
+ "z": 0.5,
+ },
+ {
+ "x": 314,
+ "y": 74,
+ "z": 0.5,
+ },
+ {
+ "x": 317,
+ "y": 75,
+ "z": 0.5,
+ },
+ {
+ "x": 320,
+ "y": 76,
+ "z": 0.5,
+ },
+ {
+ "x": 324,
+ "y": 76,
+ "z": 0.5,
+ },
+ {
+ "x": 329,
+ "y": 73,
+ "z": 0.5,
+ },
+ {
+ "x": 333,
+ "y": 69,
+ "z": 0.5,
+ },
+ {
+ "x": 336,
+ "y": 66,
+ "z": 0.5,
+ },
+ {
+ "x": 339,
+ "y": 62,
+ "z": 0.5,
+ },
+ {
+ "x": 342,
+ "y": 59,
+ "z": 0.5,
+ },
+ {
+ "x": 344,
+ "y": 57,
+ "z": 0.5,
+ },
+ {
+ "x": 346,
+ "y": 55,
+ "z": 0.5,
+ },
+ {
+ "x": 348,
+ "y": 55,
+ "z": 0.5,
+ },
+ {
+ "x": 348,
+ "y": 55,
+ "z": 0.5,
+ },
+ {
+ "x": 349,
+ "y": 55,
+ "z": 0.5,
+ },
+ {
+ "x": 349,
+ "y": 56,
+ "z": 0.5,
+ },
+ {
+ "x": 350,
+ "y": 57,
+ "z": 0.5,
+ },
+ {
+ "x": 351,
+ "y": 59,
+ "z": 0.5,
+ },
+ {
+ "x": 351,
+ "y": 61,
+ "z": 0.5,
+ },
+ {
+ "x": 352,
+ "y": 62,
+ "z": 0.5,
+ },
+ {
+ "x": 352,
+ "y": 63,
+ "z": 0.5,
+ },
+ {
+ "x": 353,
+ "y": 64,
+ "z": 0.5,
+ },
+ {
+ "x": 354,
+ "y": 64,
+ "z": 0.5,
+ },
+ {
+ "x": 355,
+ "y": 64,
+ "z": 0.5,
+ },
+ {
+ "x": 356,
+ "y": 58,
+ "z": 0.5,
+ },
+ {
+ "x": 358,
+ "y": 49,
+ "z": 0.5,
+ },
+ {
+ "x": 360,
+ "y": 40,
+ "z": 0.5,
+ },
+ {
+ "x": 363,
+ "y": 32,
+ "z": 0.5,
+ },
+ {
+ "x": 365,
+ "y": 26,
+ "z": 0.5,
+ },
+ {
+ "x": 367,
+ "y": 19,
+ "z": 0.5,
+ },
+ {
+ "x": 369,
+ "y": 13,
+ "z": 0.5,
+ },
+ {
+ "x": 373,
+ "y": 7,
+ "z": 0.5,
+ },
+ {
+ "x": 376,
+ "y": 3,
+ "z": 0.5,
+ },
+ {
+ "x": 380,
+ "y": 2,
+ "z": 0.5,
+ },
+ {
+ "x": 385,
+ "y": 2,
+ "z": 0.5,
+ },
+ {
+ "x": 390,
+ "y": 2,
+ "z": 0.5,
+ },
+ {
+ "x": 397,
+ "y": 3,
+ "z": 0.5,
+ },
+ {
+ "x": 410,
+ "y": 11,
+ "z": 0.5,
+ },
+ {
+ "x": 424,
+ "y": 23,
+ "z": 0.5,
+ },
+ {
+ "x": 434,
+ "y": 34,
+ "z": 0.5,
+ },
+ {
+ "x": 446,
+ "y": 49,
+ "z": 0.5,
+ },
+ {
+ "x": 456,
+ "y": 64,
+ "z": 0.5,
+ },
+ {
+ "x": 464,
+ "y": 81,
+ "z": 0.5,
+ },
+ {
+ "x": 468,
+ "y": 95,
+ "z": 0.5,
+ },
+ {
+ "x": 470,
+ "y": 116,
+ "z": 0.5,
+ },
+ {
+ "x": 472,
+ "y": 142,
+ "z": 0.5,
+ },
+ {
+ "x": 472,
+ "y": 162,
+ "z": 0.5,
+ },
+ {
+ "x": 468,
+ "y": 178,
+ "z": 0.5,
+ },
+ {
+ "x": 458,
+ "y": 195,
+ "z": 0.5,
+ },
+ {
+ "x": 442,
+ "y": 213,
+ "z": 0.5,
+ },
+ {
+ "x": 423,
+ "y": 230,
+ "z": 0.5,
+ },
+ {
+ "x": 407,
+ "y": 240,
+ "z": 0.5,
+ },
+ {
+ "x": 393,
+ "y": 245,
+ "z": 0.5,
+ },
+ {
+ "x": 377,
+ "y": 250,
+ "z": 0.5,
+ },
+ {
+ "x": 364,
+ "y": 252,
+ "z": 0.5,
+ },
+ {
+ "x": 354,
+ "y": 252,
+ "z": 0.5,
+ },
+ {
+ "x": 346,
+ "y": 248,
+ "z": 0.5,
+ },
+ {
+ "x": 340,
+ "y": 239,
+ "z": 0.5,
+ },
+ {
+ "x": 339,
+ "y": 225,
+ "z": 0.5,
+ },
+ {
+ "x": 339,
+ "y": 198,
+ "z": 0.5,
+ },
+ {
+ "x": 349,
+ "y": 165,
+ "z": 0.5,
+ },
+ {
+ "x": 372,
+ "y": 130,
+ "z": 0.5,
+ },
+ {
+ "x": 403,
+ "y": 89,
+ "z": 0.5,
+ },
+ {
+ "x": 432,
+ "y": 54,
+ "z": 0.5,
+ },
+ {
+ "x": 467,
+ "y": 16,
+ "z": 0.5,
+ },
+ {
+ "x": 504,
+ "y": -21,
+ "z": 0.5,
+ },
+ {
+ "x": 551,
+ "y": -68,
+ "z": 0.5,
+ },
+ {
+ "x": 597,
+ "y": -115,
+ "z": 0.5,
+ },
+ {
+ "x": 619,
+ "y": -138,
+ "z": 0.5,
+ },
+ {
+ "x": 641,
+ "y": -162,
+ "z": 0.5,
+ },
+ {
+ "x": 663,
+ "y": -188,
+ "z": 0.5,
+ },
+ {
+ "x": 675,
+ "y": -203,
+ "z": 0.5,
+ },
+ {
+ "x": 684,
+ "y": -219,
+ "z": 0.5,
+ },
+ {
+ "x": 692,
+ "y": -237,
+ "z": 0.5,
+ },
+ {
+ "x": 693,
+ "y": -244,
+ "z": 0.5,
+ },
+ {
+ "x": 691,
+ "y": -250,
+ "z": 0.5,
+ },
+ {
+ "x": 682,
+ "y": -254,
+ "z": 0.5,
+ },
+ {
+ "x": 664,
+ "y": -256,
+ "z": 0.5,
+ },
+ {
+ "x": 642,
+ "y": -256,
+ "z": 0.5,
+ },
+ {
+ "x": 621,
+ "y": -253,
+ "z": 0.5,
+ },
+ {
+ "x": 589,
+ "y": -240,
+ "z": 0.5,
+ },
+ {
+ "x": 554,
+ "y": -221,
+ "z": 0.5,
+ },
+ {
+ "x": 526,
+ "y": -201,
+ "z": 0.5,
+ },
+ {
+ "x": 502,
+ "y": -182,
+ "z": 0.5,
+ },
+ {
+ "x": 484,
+ "y": -165,
+ "z": 0.5,
+ },
+ {
+ "x": 467,
+ "y": -146,
+ "z": 0.5,
+ },
+ {
+ "x": 456,
+ "y": -131,
+ "z": 0.5,
+ },
+ {
+ "x": 450,
+ "y": -120,
+ "z": 0.5,
+ },
+ {
+ "x": 448,
+ "y": -112,
+ "z": 0.5,
+ },
+ {
+ "x": 448,
+ "y": -107,
+ "z": 0.5,
+ },
+ {
+ "x": 449,
+ "y": -104,
+ "z": 0.5,
+ },
+ {
+ "x": 452,
+ "y": -103,
+ "z": 0.5,
+ },
+ {
+ "x": 458,
+ "y": -102,
+ "z": 0.5,
+ },
+ {
+ "x": 462,
+ "y": -102,
+ "z": 0.5,
+ },
+ {
+ "x": 465,
+ "y": -103,
+ "z": 0.5,
+ },
+ {
+ "x": 470,
+ "y": -104,
+ "z": 0.5,
+ },
+ {
+ "x": 472,
+ "y": -105,
+ "z": 0.5,
+ },
+ {
+ "x": 474,
+ "y": -106,
+ "z": 0.5,
+ },
+ {
+ "x": 475,
+ "y": -106,
+ "z": 0.5,
+ },
+ {
+ "x": 476,
+ "y": -107,
+ "z": 0.5,
+ },
+ {
+ "x": 476,
+ "y": -107,
+ "z": 0.5,
+ },
+ {
+ "x": 477,
+ "y": -107,
+ "z": 0.5,
+ },
+ ],
+ "type": "free",
+ },
+ ],
+ "size": "m",
+ },
+ "rotation": 0,
+ "type": "draw",
+ "typeName": "shape",
+ "x": 511,
+ "y": 234,
+}
+`;
diff --git a/packages/tldraw/src/test/drawing.data.ts b/packages/tldraw/src/test/drawing.data.ts
new file mode 100644
index 000000000..6345b6645
--- /dev/null
+++ b/packages/tldraw/src/test/drawing.data.ts
@@ -0,0 +1,1006 @@
+export const TEST_DRAW_SHAPE_SCREEN_POINTS = [
+ {
+ x: 511,
+ y: 234,
+ },
+ {
+ x: 512,
+ y: 234,
+ },
+ {
+ x: 515,
+ y: 234,
+ },
+ {
+ x: 521,
+ y: 233,
+ },
+ {
+ x: 530,
+ y: 230,
+ },
+ {
+ x: 541,
+ y: 224,
+ },
+ {
+ x: 557,
+ y: 214,
+ },
+ {
+ x: 572,
+ y: 204,
+ },
+ {
+ x: 585,
+ y: 191,
+ },
+ {
+ x: 600,
+ y: 175,
+ },
+ {
+ x: 613,
+ y: 157,
+ },
+ {
+ x: 619,
+ y: 144,
+ },
+ {
+ x: 623,
+ y: 131,
+ },
+ {
+ x: 628,
+ y: 115,
+ },
+ {
+ x: 629,
+ y: 103,
+ },
+ {
+ x: 630,
+ y: 97,
+ },
+ {
+ x: 630,
+ y: 89,
+ },
+ {
+ x: 631,
+ y: 82,
+ },
+ {
+ x: 630,
+ y: 76,
+ },
+ {
+ x: 628,
+ y: 71,
+ },
+ {
+ x: 625,
+ y: 67,
+ },
+ {
+ x: 620,
+ y: 65,
+ },
+ {
+ x: 614,
+ y: 64,
+ },
+ {
+ x: 608,
+ y: 64,
+ },
+ {
+ x: 600,
+ y: 64,
+ },
+ {
+ x: 591,
+ y: 68,
+ },
+ {
+ x: 582,
+ y: 75,
+ },
+ {
+ x: 573,
+ y: 84,
+ },
+ {
+ x: 565,
+ y: 96,
+ },
+ {
+ x: 561,
+ y: 108,
+ },
+ {
+ x: 558,
+ y: 121,
+ },
+ {
+ x: 557,
+ y: 135,
+ },
+ {
+ x: 557,
+ y: 152,
+ },
+ {
+ x: 558,
+ y: 173,
+ },
+ {
+ x: 564,
+ y: 193,
+ },
+ {
+ x: 571,
+ y: 210,
+ },
+ {
+ x: 579,
+ y: 227,
+ },
+ {
+ x: 590,
+ y: 246,
+ },
+ {
+ x: 599,
+ y: 266,
+ },
+ {
+ x: 607,
+ y: 284,
+ },
+ {
+ x: 614,
+ y: 303,
+ },
+ {
+ x: 617,
+ y: 320,
+ },
+ {
+ x: 618,
+ y: 336,
+ },
+ {
+ x: 618,
+ y: 354,
+ },
+ {
+ x: 613,
+ y: 370,
+ },
+ {
+ x: 601,
+ y: 380,
+ },
+ {
+ x: 585,
+ y: 388,
+ },
+ {
+ x: 554,
+ y: 397,
+ },
+ {
+ x: 543,
+ y: 398,
+ },
+ {
+ x: 532,
+ y: 398,
+ },
+ {
+ x: 522,
+ y: 398,
+ },
+ {
+ x: 513,
+ y: 398,
+ },
+ {
+ x: 504,
+ y: 396,
+ },
+ {
+ x: 498,
+ y: 393,
+ },
+ {
+ x: 496,
+ y: 387,
+ },
+ {
+ x: 496,
+ y: 381,
+ },
+ {
+ x: 500,
+ y: 372,
+ },
+ {
+ x: 512,
+ y: 361,
+ },
+ {
+ x: 526,
+ y: 346,
+ },
+ {
+ x: 545,
+ y: 330,
+ },
+ {
+ x: 567,
+ y: 313,
+ },
+ {
+ x: 592,
+ y: 292,
+ },
+ {
+ x: 618,
+ y: 267,
+ },
+ {
+ x: 637,
+ y: 246,
+ },
+ {
+ x: 656,
+ y: 224,
+ },
+ {
+ x: 671,
+ y: 204,
+ },
+ {
+ x: 683,
+ y: 184,
+ },
+ {
+ x: 696,
+ y: 161,
+ },
+ {
+ x: 705,
+ y: 141,
+ },
+ {
+ x: 710,
+ y: 122,
+ },
+ {
+ x: 713,
+ y: 107,
+ },
+ {
+ x: 714,
+ y: 96,
+ },
+ {
+ x: 714,
+ y: 88,
+ },
+ {
+ x: 712,
+ y: 82,
+ },
+ {
+ x: 707,
+ y: 79,
+ },
+ {
+ x: 702,
+ y: 78,
+ },
+ {
+ x: 697,
+ y: 77,
+ },
+ {
+ x: 689,
+ y: 78,
+ },
+ {
+ x: 681,
+ y: 84,
+ },
+ {
+ x: 675,
+ y: 94,
+ },
+ {
+ x: 669,
+ y: 106,
+ },
+ {
+ x: 662,
+ y: 124,
+ },
+ {
+ x: 655,
+ y: 145,
+ },
+ {
+ x: 650,
+ y: 170,
+ },
+ {
+ x: 646,
+ y: 198,
+ },
+ {
+ x: 643,
+ y: 227,
+ },
+ {
+ x: 643,
+ y: 256,
+ },
+ {
+ x: 643,
+ y: 283,
+ },
+ {
+ x: 644,
+ y: 308,
+ },
+ {
+ x: 651,
+ y: 331,
+ },
+ {
+ x: 659,
+ y: 347,
+ },
+ {
+ x: 667,
+ y: 358,
+ },
+ {
+ x: 677,
+ y: 371,
+ },
+ {
+ x: 686,
+ y: 379,
+ },
+ {
+ x: 694,
+ y: 384,
+ },
+ {
+ x: 702,
+ y: 386,
+ },
+ {
+ x: 708,
+ y: 386,
+ },
+ {
+ x: 716,
+ y: 385,
+ },
+ {
+ x: 725,
+ y: 380,
+ },
+ {
+ x: 734,
+ y: 370,
+ },
+ {
+ x: 741,
+ y: 359,
+ },
+ {
+ x: 747,
+ y: 346,
+ },
+ {
+ x: 753,
+ y: 329,
+ },
+ {
+ x: 758,
+ y: 312,
+ },
+ {
+ x: 761,
+ y: 295,
+ },
+ {
+ x: 763,
+ y: 280,
+ },
+ {
+ x: 764,
+ y: 271,
+ },
+ {
+ x: 764,
+ y: 265,
+ },
+ {
+ x: 764,
+ y: 258,
+ },
+ {
+ x: 762,
+ y: 254,
+ },
+ {
+ x: 759,
+ y: 250,
+ },
+ {
+ x: 757,
+ y: 250,
+ },
+ {
+ x: 754,
+ y: 250,
+ },
+ {
+ x: 751,
+ y: 251,
+ },
+ {
+ x: 749,
+ y: 253,
+ },
+ {
+ x: 747,
+ y: 260,
+ },
+ {
+ x: 745,
+ y: 268,
+ },
+ {
+ x: 744,
+ y: 279,
+ },
+ {
+ x: 743,
+ y: 290,
+ },
+ {
+ x: 743,
+ y: 300,
+ },
+ {
+ x: 746,
+ y: 313,
+ },
+ {
+ x: 752,
+ y: 325,
+ },
+ {
+ x: 758,
+ y: 334,
+ },
+ {
+ x: 766,
+ y: 343,
+ },
+ {
+ x: 771,
+ y: 347,
+ },
+ {
+ x: 777,
+ y: 350,
+ },
+ {
+ x: 785,
+ y: 352,
+ },
+ {
+ x: 791,
+ y: 352,
+ },
+ {
+ x: 797,
+ y: 349,
+ },
+ {
+ x: 802,
+ y: 339,
+ },
+ {
+ x: 807,
+ y: 327,
+ },
+ {
+ x: 809,
+ y: 317,
+ },
+ {
+ x: 812,
+ y: 304,
+ },
+ {
+ x: 814,
+ y: 292,
+ },
+ {
+ x: 816,
+ y: 282,
+ },
+ {
+ x: 817,
+ y: 272,
+ },
+ {
+ x: 818,
+ y: 265,
+ },
+ {
+ x: 819,
+ y: 259,
+ },
+ {
+ x: 819,
+ y: 256,
+ },
+ {
+ x: 819,
+ y: 254,
+ },
+ {
+ x: 819,
+ y: 253,
+ },
+ {
+ x: 819,
+ y: 256,
+ },
+ {
+ x: 819,
+ y: 261,
+ },
+ {
+ x: 819,
+ y: 269,
+ },
+ {
+ x: 819,
+ y: 278,
+ },
+ {
+ x: 819,
+ y: 285,
+ },
+ {
+ x: 819,
+ y: 290,
+ },
+ {
+ x: 819,
+ y: 295,
+ },
+ {
+ x: 820,
+ y: 300,
+ },
+ {
+ x: 823,
+ y: 305,
+ },
+ {
+ x: 825,
+ y: 308,
+ },
+ {
+ x: 828,
+ y: 309,
+ },
+ {
+ x: 831,
+ y: 310,
+ },
+ {
+ x: 835,
+ y: 310,
+ },
+ {
+ x: 840,
+ y: 307,
+ },
+ {
+ x: 844,
+ y: 303,
+ },
+ {
+ x: 847,
+ y: 300,
+ },
+ {
+ x: 850,
+ y: 296,
+ },
+ {
+ x: 853,
+ y: 293,
+ },
+ {
+ x: 855,
+ y: 291,
+ },
+ {
+ x: 857,
+ y: 289,
+ },
+ {
+ x: 859,
+ y: 289,
+ },
+ {
+ x: 859,
+ y: 289,
+ },
+ {
+ x: 860,
+ y: 289,
+ },
+ {
+ x: 860,
+ y: 290,
+ },
+ {
+ x: 861,
+ y: 291,
+ },
+ {
+ x: 862,
+ y: 293,
+ },
+ {
+ x: 862,
+ y: 295,
+ },
+ {
+ x: 863,
+ y: 296,
+ },
+ {
+ x: 863,
+ y: 297,
+ },
+ {
+ x: 864,
+ y: 298,
+ },
+ {
+ x: 865,
+ y: 298,
+ },
+ {
+ x: 866,
+ y: 298,
+ },
+ {
+ x: 867,
+ y: 292,
+ },
+ {
+ x: 869,
+ y: 283,
+ },
+ {
+ x: 871,
+ y: 274,
+ },
+ {
+ x: 874,
+ y: 266,
+ },
+ {
+ x: 876,
+ y: 260,
+ },
+ {
+ x: 878,
+ y: 253,
+ },
+ {
+ x: 880,
+ y: 247,
+ },
+ {
+ x: 884,
+ y: 241,
+ },
+ {
+ x: 887,
+ y: 237,
+ },
+ {
+ x: 891,
+ y: 236,
+ },
+ {
+ x: 896,
+ y: 236,
+ },
+ {
+ x: 901,
+ y: 236,
+ },
+ {
+ x: 908,
+ y: 237,
+ },
+ {
+ x: 921,
+ y: 245,
+ },
+ {
+ x: 935,
+ y: 257,
+ },
+ {
+ x: 945,
+ y: 268,
+ },
+ {
+ x: 957,
+ y: 283,
+ },
+ {
+ x: 967,
+ y: 298,
+ },
+ {
+ x: 975,
+ y: 315,
+ },
+ {
+ x: 979,
+ y: 329,
+ },
+ {
+ x: 981,
+ y: 350,
+ },
+ {
+ x: 983,
+ y: 376,
+ },
+ {
+ x: 983,
+ y: 396,
+ },
+ {
+ x: 979,
+ y: 412,
+ },
+ {
+ x: 969,
+ y: 429,
+ },
+ {
+ x: 953,
+ y: 447,
+ },
+ {
+ x: 934,
+ y: 464,
+ },
+ {
+ x: 918,
+ y: 474,
+ },
+ {
+ x: 904,
+ y: 479,
+ },
+ {
+ x: 888,
+ y: 484,
+ },
+ {
+ x: 875,
+ y: 486,
+ },
+ {
+ x: 865,
+ y: 486,
+ },
+ {
+ x: 857,
+ y: 482,
+ },
+ {
+ x: 851,
+ y: 473,
+ },
+ {
+ x: 850,
+ y: 459,
+ },
+ {
+ x: 850,
+ y: 432,
+ },
+ {
+ x: 860,
+ y: 399,
+ },
+ {
+ x: 883,
+ y: 364,
+ },
+ {
+ x: 914,
+ y: 323,
+ },
+ {
+ x: 943,
+ y: 288,
+ },
+ {
+ x: 978,
+ y: 250,
+ },
+ {
+ x: 1015,
+ y: 213,
+ },
+ {
+ x: 1062,
+ y: 166,
+ },
+ {
+ x: 1108,
+ y: 119,
+ },
+ {
+ x: 1130,
+ y: 96,
+ },
+ {
+ x: 1152,
+ y: 72,
+ },
+ {
+ x: 1174,
+ y: 46,
+ },
+ {
+ x: 1186,
+ y: 31,
+ },
+ {
+ x: 1195,
+ y: 15,
+ },
+ {
+ x: 1203,
+ y: -3,
+ },
+ {
+ x: 1204,
+ y: -10,
+ },
+ {
+ x: 1202,
+ y: -16,
+ },
+ {
+ x: 1193,
+ y: -20,
+ },
+ {
+ x: 1175,
+ y: -22,
+ },
+ {
+ x: 1153,
+ y: -22,
+ },
+ {
+ x: 1132,
+ y: -19,
+ },
+ {
+ x: 1100,
+ y: -6,
+ },
+ {
+ x: 1065,
+ y: 13,
+ },
+ {
+ x: 1037,
+ y: 33,
+ },
+ {
+ x: 1013,
+ y: 52,
+ },
+ {
+ x: 995,
+ y: 69,
+ },
+ {
+ x: 978,
+ y: 88,
+ },
+ {
+ x: 967,
+ y: 103,
+ },
+ {
+ x: 961,
+ y: 114,
+ },
+ {
+ x: 959,
+ y: 122,
+ },
+ {
+ x: 959,
+ y: 127,
+ },
+ {
+ x: 960,
+ y: 130,
+ },
+ {
+ x: 963,
+ y: 131,
+ },
+ {
+ x: 969,
+ y: 132,
+ },
+ {
+ x: 973,
+ y: 132,
+ },
+ {
+ x: 976,
+ y: 131,
+ },
+ {
+ x: 981,
+ y: 130,
+ },
+ {
+ x: 983,
+ y: 129,
+ },
+ {
+ x: 985,
+ y: 128,
+ },
+ {
+ x: 986,
+ y: 128,
+ },
+ {
+ x: 987,
+ y: 127,
+ },
+ {
+ x: 987,
+ y: 127,
+ },
+ {
+ x: 988,
+ y: 127,
+ },
+]
diff --git a/packages/tldraw/src/test/drawing.test.ts b/packages/tldraw/src/test/drawing.test.ts
index eb96dcecb..c2041cf95 100644
--- a/packages/tldraw/src/test/drawing.test.ts
+++ b/packages/tldraw/src/test/drawing.test.ts
@@ -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')
+})
diff --git a/packages/utils/src/lib/perf.ts b/packages/utils/src/lib/perf.ts
index e6ac86450..2f9283fd9 100644
--- a/packages/utils/src/lib/perf.ts
+++ b/packages/utils/src/lib/perf.ts
@@ -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 })
diff --git a/scripts/deploy.ts b/scripts/deploy.ts
index 4c39f387d..ff6353c5d 100644
--- a/scripts/deploy.ts
+++ b/scripts/deploy.ts
@@ -6,7 +6,7 @@ import { execSync } from 'child_process'
import { appendFileSync, existsSync, readdirSync, writeFileSync } from 'fs'
import path, { join } from 'path'
import { PassThrough } from 'stream'
-import tar from 'tar'
+import * as tar from 'tar'
import { exec } from './lib/exec'
import { makeEnv } from './lib/makeEnv'
import { nicelog } from './lib/nicelog'
@@ -515,7 +515,7 @@ async function coalesceWithPreviousAssets(assetsDir: string) {
// and it will mess up the inline source viewer on sentry errors.
const out = tar.x({ cwd: assetsDir, 'keep-existing': true })
for await (const chunk of Body?.transformToWebStream() as any as AsyncIterable) {
- out.write(chunk)
+ out.write(Buffer.from(chunk.buffer))
}
out.end()
}
diff --git a/scripts/lib/didAnyPackageChange.ts b/scripts/lib/didAnyPackageChange.ts
index db4ad3872..ce20bba0f 100644
--- a/scripts/lib/didAnyPackageChange.ts
+++ b/scripts/lib/didAnyPackageChange.ts
@@ -18,12 +18,12 @@ async function hasPackageChanged(pkg: PackageDetails) {
}
const publishedTarballPath = `${dirPath}/published-package.tgz`
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
- const publishedManifest = await getTarballManifest(publishedTarballPath)
+ const publishedManifest = getTarballManifestSync(publishedTarballPath)
const localTarballPath = `${dirPath}/local-package.tgz`
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir })
- const localManifest = await getTarballManifest(localTarballPath)
+ const localManifest = getTarballManifestSync(localTarballPath)
return !manifestsAreEqual(publishedManifest, localManifest)
} finally {
@@ -48,34 +48,25 @@ function manifestsAreEqual(a: Record, b: Record)
return true
}
-function getTarballManifest(tarballPath: string): Promise> {
+function getTarballManifestSync(tarballPath: string) {
const manifest: Record = {}
- return new Promise((resolve, reject) =>
- tar.list(
- {
- // @ts-expect-error bad typings
- file: tarballPath,
- onentry: (entry) => {
- entry.on('data', (data) => {
- // we could hash these to reduce memory but it's probably fine
- const existing = manifest[entry.path]
- if (existing) {
- manifest[entry.path] = Buffer.concat([existing, data])
- } else {
- manifest[entry.path] = data
- }
- })
- },
- },
- (err: any) => {
- if (err) {
- reject(err)
+ tar.list({
+ file: tarballPath,
+ onentry: (entry) => {
+ entry.on('data', (data) => {
+ // we could hash these to reduce memory but it's probably fine
+ const existing = manifest[entry.path]
+ if (existing) {
+ manifest[entry.path] = Buffer.concat([existing, data])
} else {
- resolve(manifest)
+ manifest[entry.path] = data
}
- }
- )
- )
+ })
+ },
+ sync: true,
+ })
+
+ return manifest
}
export async function didAnyPackageChange() {
diff --git a/scripts/package.json b/scripts/package.json
index e1bf07393..b49be8566 100644
--- a/scripts/package.json
+++ b/scripts/package.json
@@ -32,7 +32,6 @@
"@aws-sdk/lib-storage": "^3.440.0",
"@types/is-ci": "^3.0.0",
"@types/node": "~20.11",
- "@types/tar": "^6.1.11",
"@typescript-eslint/utils": "^5.59.0",
"ast-types": "^0.14.2",
"cross-fetch": "^3.1.5",
@@ -59,7 +58,7 @@
"@types/tmp": "^0.2.6",
"ignore": "^5.2.4",
"minimist": "^1.2.8",
- "tar": "^6.2.0",
+ "tar": "^7.0.1",
"tmp": "^0.2.3"
}
}
diff --git a/yarn.lock b/yarn.lock
index 0b94e4b30..02650f560 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3680,6 +3680,15 @@ __metadata:
languageName: node
linkType: hard
+"@isaacs/fs-minipass@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@isaacs/fs-minipass@npm:4.0.0"
+ dependencies:
+ minipass: "npm:^7.0.4"
+ checksum: 7444d7a3c9211c27494630e2bff8545e3494a1598624a4871ee7ef3a9e592a61fed3abd85d118f966673bd0b4401c266d45441f89c00c420e9d0cfbf1042dbd5
+ languageName: node
+ linkType: hard
+
"@istanbuljs/load-nyc-config@npm:^1.0.0":
version: 1.1.0
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
@@ -7570,7 +7579,6 @@ __metadata:
"@types/is-ci": "npm:^3.0.0"
"@types/minimist": "npm:^1.2.5"
"@types/node": "npm:~20.11"
- "@types/tar": "npm:^6.1.11"
"@types/tmp": "npm:^0.2.6"
"@typescript-eslint/utils": "npm:^5.59.0"
ast-types: "npm:^0.14.2"
@@ -7589,7 +7597,7 @@ __metadata:
rimraf: "npm:^4.4.0"
semver: "npm:^7.3.8"
svgo: "npm:^3.0.2"
- tar: "npm:^6.2.0"
+ tar: "npm:^7.0.1"
tmp: "npm:^0.2.3"
typescript: "npm:^5.3.3"
languageName: unknown
@@ -8434,16 +8442,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/tar@npm:^6.1.11":
- version: 6.1.11
- resolution: "@types/tar@npm:6.1.11"
- dependencies:
- "@types/node": "npm:*"
- minipass: "npm:^4.0.0"
- checksum: 0d54b8acbd7d2fc43bd1097eef5058604a6b0e3a394cf485038303ca3ef39ecb42451c7dc5a2b9b18420e137ef5b2c76ec504e94c2f45010b2c8e8c3a49d9de7
- languageName: node
- linkType: hard
-
"@types/testing-library__jest-dom@npm:^5.9.1":
version: 5.14.9
resolution: "@types/testing-library__jest-dom@npm:5.14.9"
@@ -10700,6 +10698,13 @@ __metadata:
languageName: node
linkType: hard
+"chownr@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "chownr@npm:3.0.0"
+ checksum: b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c
+ languageName: node
+ linkType: hard
+
"chrome-trace-event@npm:^1.0.2":
version: 1.0.3
resolution: "chrome-trace-event@npm:1.0.3"
@@ -14645,18 +14650,18 @@ __metadata:
languageName: node
linkType: hard
-"glob@npm:^10.2.2, glob@npm:^10.3.10":
- version: 10.3.10
- resolution: "glob@npm:10.3.10"
+"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
+ version: 10.3.12
+ resolution: "glob@npm:10.3.12"
dependencies:
foreground-child: "npm:^3.1.0"
- jackspeak: "npm:^2.3.5"
+ jackspeak: "npm:^2.3.6"
minimatch: "npm:^9.0.1"
- minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
- path-scurry: "npm:^1.10.1"
+ minipass: "npm:^7.0.4"
+ path-scurry: "npm:^1.10.2"
bin:
glob: dist/esm/bin.mjs
- checksum: 38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8
+ checksum: 9e8186abc22dc824b5dd86cefd8e6b5621a72d1be7f68bacc0fd681e8c162ec5546660a6ec0553d6a74757a585e655956c7f8f1a6d24570e8d865c307323d178
languageName: node
linkType: hard
@@ -16275,7 +16280,7 @@ __metadata:
languageName: node
linkType: hard
-"jackspeak@npm:^2.3.5":
+"jackspeak@npm:^2.3.6":
version: 2.3.6
resolution: "jackspeak@npm:2.3.6"
dependencies:
@@ -17721,10 +17726,10 @@ __metadata:
languageName: node
linkType: hard
-"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0":
- version: 10.1.0
- resolution: "lru-cache@npm:10.1.0"
- checksum: 207278d6fa711fb1f94a0835d4d4737441d2475302482a14785b10515e4c906a57ebf9f35bf060740c9560e91c7c1ad5a04fd7ed030972a9ba18bce2a228e95b
+"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
+ version: 10.2.0
+ resolution: "lru-cache@npm:10.2.0"
+ checksum: 502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302
languageName: node
linkType: hard
@@ -19117,7 +19122,7 @@ __metadata:
languageName: node
linkType: hard
-"minipass@npm:^4.0.0, minipass@npm:^4.2.4":
+"minipass@npm:^4.2.4":
version: 4.2.8
resolution: "minipass@npm:4.2.8"
checksum: e148eb6dcb85c980234cad889139ef8ddf9d5bdac534f4f0268446c8792dd4c74f4502479be48de3c1cce2f6450f6da4d0d4a86405a8a12be04c1c36b339569a
@@ -19131,7 +19136,7 @@ __metadata:
languageName: node
linkType: hard
-"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3":
+"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4":
version: 7.0.4
resolution: "minipass@npm:7.0.4"
checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18
@@ -19148,6 +19153,16 @@ __metadata:
languageName: node
linkType: hard
+"minizlib@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "minizlib@npm:3.0.1"
+ dependencies:
+ minipass: "npm:^7.0.4"
+ rimraf: "npm:^5.0.5"
+ checksum: 622cb85f51e5c206a080a62d20db0d7b4066f308cb6ce82a9644da112367c3416ae7062017e631eb7ac8588191cfa4a9a279b8651c399265202b298e98c4acef
+ languageName: node
+ linkType: hard
+
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
version: 0.5.3
resolution: "mkdirp-classic@npm:0.5.3"
@@ -19164,6 +19179,15 @@ __metadata:
languageName: node
linkType: hard
+"mkdirp@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "mkdirp@npm:3.0.1"
+ bin:
+ mkdirp: dist/cjs/src/bin.js
+ checksum: 16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba
+ languageName: node
+ linkType: hard
+
"mlly@npm:^1.1.0, mlly@npm:^1.2.0":
version: 1.5.0
resolution: "mlly@npm:1.5.0"
@@ -20327,13 +20351,13 @@ __metadata:
languageName: node
linkType: hard
-"path-scurry@npm:^1.10.1, path-scurry@npm:^1.6.1":
- version: 1.10.1
- resolution: "path-scurry@npm:1.10.1"
+"path-scurry@npm:^1.10.2, path-scurry@npm:^1.6.1":
+ version: 1.10.2
+ resolution: "path-scurry@npm:1.10.2"
dependencies:
- lru-cache: "npm:^9.1.1 || ^10.0.0"
+ lru-cache: "npm:^10.2.0"
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
- checksum: eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8
+ checksum: a2bbbe8dc284c49dd9be78ca25f3a8b89300e0acc24a77e6c74824d353ef50efbf163e64a69f4330b301afca42d0e2229be0560d6d616ac4e99d48b4062016b1
languageName: node
linkType: hard
@@ -22045,6 +22069,17 @@ __metadata:
languageName: node
linkType: hard
+"rimraf@npm:^5.0.5":
+ version: 5.0.5
+ resolution: "rimraf@npm:5.0.5"
+ dependencies:
+ glob: "npm:^10.3.7"
+ bin:
+ rimraf: dist/esm/bin.mjs
+ checksum: a612c7184f96258b7d1328c486b12ca7b60aa30e04229a08bbfa7e964486deb1e9a1b52d917809311bdc39a808a4055c0f950c0280fba194ba0a09e6f0d404f6
+ languageName: node
+ linkType: hard
+
"rollup-plugin-inject@npm:^3.0.0":
version: 3.0.2
resolution: "rollup-plugin-inject@npm:3.0.2"
@@ -23378,7 +23413,7 @@ __metadata:
languageName: node
linkType: hard
-"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.0":
+"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2":
version: 6.2.1
resolution: "tar@npm:6.2.1"
dependencies:
@@ -23392,6 +23427,20 @@ __metadata:
languageName: node
linkType: hard
+"tar@npm:^7.0.1":
+ version: 7.0.1
+ resolution: "tar@npm:7.0.1"
+ dependencies:
+ "@isaacs/fs-minipass": "npm:^4.0.0"
+ chownr: "npm:^3.0.0"
+ minipass: "npm:^5.0.0"
+ minizlib: "npm:^3.0.1"
+ mkdirp: "npm:^3.0.1"
+ yallist: "npm:^5.0.0"
+ checksum: 6fd89ef8051d12975f66a2f3932a80479bdc6c9f3bcdf04b8b57784e942ed860708ccecf79bcbb30659b14ab52eef2095d2c3af377545ff9df30de28036671dc
+ languageName: node
+ linkType: hard
+
"terminal-link@npm:^2.1.1":
version: 2.1.1
resolution: "terminal-link@npm:2.1.1"
@@ -24964,8 +25013,8 @@ __metadata:
linkType: hard
"vite@npm:^5.0.0":
- version: 5.2.8
- resolution: "vite@npm:5.2.8"
+ version: 5.2.9
+ resolution: "vite@npm:5.2.9"
dependencies:
esbuild: "npm:^0.20.1"
fsevents: "npm:~2.3.3"
@@ -24999,7 +25048,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
- checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1
+ checksum: 26342c8dde540e4161fdad2c9c8f2f0e23567f051c7a40abb8e4796d6c4292fbd118ab7a4ac252515e78c4f99525b557731e6117287b2bccde0ea61d73bcff27
languageName: node
linkType: hard
@@ -25666,6 +25715,13 @@ __metadata:
languageName: node
linkType: hard
+"yallist@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "yallist@npm:5.0.0"
+ checksum: 1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a
+ languageName: node
+ linkType: hard
+
"yaml@npm:2.3.4, yaml@npm:^2.0.0, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.4":
version: 2.3.4
resolution: "yaml@npm:2.3.4"