From 625f4abc3b90a4873e2e7b4038b6299a2b0d8722 Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Wed, 17 Apr 2024 20:38:31 +0100 Subject: [PATCH 01/11] [fix] allow loading files (#3517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I messed up the schema validator for loading files. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --- packages/tldraw/src/lib/utils/tldr/file.ts | 2 +- packages/validate/api-report.md | 6 ++- packages/validate/api/api.json | 32 +++++++++++++ packages/validate/src/lib/validation.ts | 54 +++++++++++++++++----- 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/packages/tldraw/src/lib/utils/tldr/file.ts b/packages/tldraw/src/lib/utils/tldr/file.ts index bee4c2ac2..8302d53df 100644 --- a/packages/tldraw/src/lib/utils/tldr/file.ts +++ b/packages/tldraw/src/lib/utils/tldr/file.ts @@ -62,7 +62,7 @@ const schemaV2 = T.object({ const tldrawFileValidator: T.Validator = T.object({ tldrawFileFormatVersion: T.nonZeroInteger, - schema: T.union('schemaVersion', { + schema: T.numberUnion('schemaVersion', { 1: schemaV1, 2: schemaV2, }), diff --git a/packages/validate/api-report.md b/packages/validate/api-report.md index 584047196..dbf57dcbb 100644 --- a/packages/validate/api-report.md +++ b/packages/validate/api-report.md @@ -83,6 +83,9 @@ function nullable(validator: Validatable): Validator; // @public const number: Validator; +// @internal (undocumented) +function numberUnion>(key: Key, config: Config): UnionValidator; + // @public function object(config: { readonly [K in keyof Shape]: Validatable; @@ -134,6 +137,7 @@ declare namespace T { jsonDict, dict, union, + numberUnion, model, setEnum, optional, @@ -178,7 +182,7 @@ function union, UnknownValue = never> extends Validator | UnknownValue> { - constructor(key: Key, config: Config, unknownValueValidation: (value: object, variant: string) => UnknownValue); + constructor(key: Key, config: Config, unknownValueValidation: (value: object, variant: string) => UnknownValue, useNumberKeys: boolean); // (undocumented) validateUnknownVariants(unknownValueValidation: (value: object, variant: string) => Unknown): UnionValidator; } diff --git a/packages/validate/api/api.json b/packages/validate/api/api.json index 1bdd27588..0aedb8cb6 100644 --- a/packages/validate/api/api.json +++ b/packages/validate/api/api.json @@ -3027,6 +3027,14 @@ "kind": "Content", "text": "(value: object, variant: string) => UnknownValue" }, + { + "kind": "Content", + "text": ", useNumberKeys: " + }, + { + "kind": "Content", + "text": "boolean" + }, { "kind": "Content", "text": ");" @@ -3059,6 +3067,14 @@ "endIndex": 6 }, "isOptional": false + }, + { + "parameterName": "useNumberKeys", + "parameterTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "isOptional": false } ] }, @@ -4260,6 +4276,14 @@ "kind": "Content", "text": "(value: object, variant: string) => UnknownValue" }, + { + "kind": "Content", + "text": ", useNumberKeys: " + }, + { + "kind": "Content", + "text": "boolean" + }, { "kind": "Content", "text": ");" @@ -4292,6 +4316,14 @@ "endIndex": 6 }, "isOptional": false + }, + { + "parameterName": "useNumberKeys", + "parameterTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "isOptional": false } ] }, diff --git a/packages/validate/src/lib/validation.ts b/packages/validate/src/lib/validation.ts index b9d4d21f3..145746437 100644 --- a/packages/validate/src/lib/validation.ts +++ b/packages/validate/src/lib/validation.ts @@ -394,7 +394,8 @@ export class UnionValidator< constructor( private readonly key: Key, private readonly config: Config, - private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue + private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue, + private readonly useNumberKeys: boolean ) { super( (input) => { @@ -442,11 +443,13 @@ export class UnionValidator< matchingSchema: Validatable | undefined variant: string } { - const variant = getOwnProperty(object, this.key) as keyof Config | undefined - if (typeof variant !== 'string') { + const variant = getOwnProperty(object, this.key) as string & keyof Config + if (!this.useNumberKeys && typeof variant !== 'string') { throw new ValidationError( `Expected a string for key "${this.key}", got ${typeToString(variant)}` ) + } else if (this.useNumberKeys && !Number.isFinite(Number(variant))) { + throw new ValidationError(`Expected a number for key "${this.key}", got "${variant as any}"`) } const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined @@ -456,7 +459,7 @@ export class UnionValidator< validateUnknownVariants( unknownValueValidation: (value: object, variant: string) => Unknown ): UnionValidator { - return new UnionValidator(this.key, this.config, unknownValueValidation) + return new UnionValidator(this.key, this.config, unknownValueValidation, this.useNumberKeys) } } @@ -829,14 +832,41 @@ export function union { - return new UnionValidator(key, config, (unknownValue, unknownVariant) => { - throw new ValidationError( - `Expected one of ${Object.keys(config) - .map((key) => JSON.stringify(key)) - .join(' or ')}, got ${JSON.stringify(unknownVariant)}`, - [key] - ) - }) + return new UnionValidator( + key, + config, + (unknownValue, unknownVariant) => { + throw new ValidationError( + `Expected one of ${Object.keys(config) + .map((key) => JSON.stringify(key)) + .join(' or ')}, got ${JSON.stringify(unknownVariant)}`, + [key] + ) + }, + false + ) +} + +/** + * @internal + */ +export function numberUnion>( + key: Key, + config: Config +): UnionValidator { + return new UnionValidator( + key, + config, + (unknownValue, unknownVariant) => { + throw new ValidationError( + `Expected one of ${Object.keys(config) + .map((key) => JSON.stringify(key)) + .join(' or ')}, got ${JSON.stringify(unknownVariant)}`, + [key] + ) + }, + true + ) } /** From dd0b7b882d6801df7276454e63aecc853e263c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 17 Apr 2024 22:16:40 +0200 Subject: [PATCH 02/11] VS Code 2.0.30 (#3519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bump for the hotfix. ### Change Type - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [x] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --- apps/vscode/extension/CHANGELOG.md | 4 ++++ apps/vscode/extension/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/vscode/extension/CHANGELOG.md b/apps/vscode/extension/CHANGELOG.md index 6550a679f..f90bd5340 100644 --- a/apps/vscode/extension/CHANGELOG.md +++ b/apps/vscode/extension/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.30 + +- Fixes a bug that prevented opening some files. + ## 2.0.29 - Improved note shapes. diff --git a/apps/vscode/extension/package.json b/apps/vscode/extension/package.json index 36ff1f250..f70a6dae6 100644 --- a/apps/vscode/extension/package.json +++ b/apps/vscode/extension/package.json @@ -1,7 +1,7 @@ { "name": "tldraw-vscode", "description": "The tldraw extension for VS Code.", - "version": "2.0.29", + "version": "2.0.30", "private": true, "author": { "name": "tldraw Inc.", From 741ed00bda22f3b445f5996a40ce4bd54a629cc6 Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Thu, 18 Apr 2024 08:57:37 +0100 Subject: [PATCH 03/11] [signia] Smart dirty checking of active computeds (#3516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a huge perf win, and it came to me while procrastinating on making dinner. The idea is that we can skip checking the parents of a computed value if - it is being dereferenced during a reaction cycle - the computed value was not traversed during the current reaction cycle This more than doubles the speed of the webgl minimap render on my machine (from 2ms down to like 0.8ms). This will make the biggest difference for anything that derives a value from a large collection of other computed values where typically only a small amount of them change at one time (e.g. iterating over all the shape page bounds to compile an RBush) Most code paths where we see a big chunk of `haveParentsChanged` in flame graphs should be much faster after this. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` — Improving existing features --- packages/state/src/lib/core/Computed.ts | 13 ++++++++++--- packages/state/src/lib/core/transactions.ts | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/state/src/lib/core/Computed.ts b/packages/state/src/lib/core/Computed.ts index 290f8c97d..2d82490d6 100644 --- a/packages/state/src/lib/core/Computed.ts +++ b/packages/state/src/lib/core/Computed.ts @@ -4,7 +4,7 @@ import { HistoryBuffer } from './HistoryBuffer' import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture' import { GLOBAL_START_EPOCH } from './constants' import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers' -import { getGlobalEpoch } from './transactions' +import { getGlobalEpoch, getIsReacting } from './transactions' import { Child, ComputeDiff, RESET_VALUE, Signal } from './types' import { logComputedGetterWarning } from './warnings' @@ -189,8 +189,15 @@ class __UNSAFE__Computed implements Computed __unsafe__getWithoutCapture(ignoreErrors?: boolean): Value { const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH - if (!isNew && (this.lastCheckedEpoch === getGlobalEpoch() || !haveParentsChanged(this))) { - this.lastCheckedEpoch = getGlobalEpoch() + const globalEpoch = getGlobalEpoch() + + if ( + !isNew && + (this.lastCheckedEpoch === globalEpoch || + (this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < globalEpoch) || + !haveParentsChanged(this)) + ) { + this.lastCheckedEpoch = globalEpoch if (this.error) { if (!ignoreErrors) { throw this.error.thrownValue diff --git a/packages/state/src/lib/core/transactions.ts b/packages/state/src/lib/core/transactions.ts index afb92d7d1..0e3672eee 100644 --- a/packages/state/src/lib/core/transactions.ts +++ b/packages/state/src/lib/core/transactions.ts @@ -70,6 +70,10 @@ export function getGlobalEpoch() { return inst.globalEpoch } +export function getIsReacting() { + return inst.globalIsReacting +} + /** * Collect all of the reactors that need to run for an atom and run them. * From 47070ec10916f4fb03cd30acc1c4efbe22c6a056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 18 Apr 2024 10:01:46 +0200 Subject: [PATCH 04/11] Use computed cache for getting the parent child relationships (#3508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the existing computed cache for parent child relationships instead of creating it. Tiny bit faster, less memory, and simpler. ### Change Type - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [x] `internal` — Does not affect user-facing stuff - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --- packages/editor/src/lib/editor/Editor.ts | 37 +++++------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 8a0ae7235..e4e9fa1bf 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -4556,28 +4556,11 @@ export class Editor extends EventEmitter { * @public */ @computed getCurrentPageShapesSorted(): TLShape[] { - const shapes = this.getCurrentPageShapes().sort(sortByIndex) - const parentChildMap = new Map() const result: TLShape[] = [] - const topLevelShapes: TLShape[] = [] - let shape: TLShape, parent: TLShape | undefined - - for (let i = 0, n = shapes.length; i < n; i++) { - shape = shapes[i] - parent = this.getShape(shape.parentId) - if (parent) { - if (!parentChildMap.has(parent.id)) { - parentChildMap.set(parent.id, []) - } - parentChildMap.get(parent.id)!.push(shape) - } else { - // undefined if parent is a shape - topLevelShapes.push(shape) - } - } + const topLevelShapes = this.getSortedChildIdsForParent(this.getCurrentPageId()) for (let i = 0, n = topLevelShapes.length; i < n; i++) { - pushShapeWithDescendants(topLevelShapes[i], parentChildMap, result) + pushShapeWithDescendants(this, topLevelShapes[i], result) } return result @@ -8875,16 +8858,12 @@ function applyPartialToShape(prev: T, partial?: TLShapePartia return next } -function pushShapeWithDescendants( - shape: TLShape, - parentChildMap: Map, - result: TLShape[] -): void { +function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape[]): void { + const shape = editor.getShape(id) + if (!shape) return result.push(shape) - const children = parentChildMap.get(shape.id) - if (children) { - for (let i = 0, n = children.length; i < n; i++) { - pushShapeWithDescendants(children[i], parentChildMap, result) - } + const childIds = editor.getSortedChildIdsForParent(id) + for (let i = 0, n = childIds.length; i < n; i++) { + pushShapeWithDescendants(editor, childIds[i], result) } } From 1fc68975e2d8f9c3c2842e47c93867514d0a956c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 18 Apr 2024 15:38:57 +0200 Subject: [PATCH 05/11] Fix version (#3521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were using react's version instead of the version of our packages. ### Change Type - [ ] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --- apps/dotcom/src/components/IFrameProtector.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dotcom/src/components/IFrameProtector.tsx b/apps/dotcom/src/components/IFrameProtector.tsx index cc3ea7355..d778fbda6 100644 --- a/apps/dotcom/src/components/IFrameProtector.tsx +++ b/apps/dotcom/src/components/IFrameProtector.tsx @@ -1,5 +1,6 @@ -import { ReactNode, useEffect, useState, version } from 'react' +import { ReactNode, useEffect, useState } from 'react' import { LoadingScreen } from 'tldraw' +import { version } from '../../version' import { useUrl } from '../hooks/useUrl' import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent' From f6a2e352deb8810ab55bf0380d745ec99ffa267f Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Fri, 19 Apr 2024 13:07:33 +0100 Subject: [PATCH 06/11] Improve back to content (#3532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the "back to content" behavior. Rather than using an interval, we now add a "camera-stopped" event that triggers the check. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` ### Test Plan 1. Create some shapes, then move the camera to an empty part of the canvas. 2. Check that the back to content button appears. 3. Ensure that the back to content button does not appear when the canvas is empty. --- .../dotcom/src/components/IFrameProtector.tsx | 2 +- .../HelperButtons/BackToContent.tsx | 40 ++++++++----------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/apps/dotcom/src/components/IFrameProtector.tsx b/apps/dotcom/src/components/IFrameProtector.tsx index d778fbda6..b4fd9f06d 100644 --- a/apps/dotcom/src/components/IFrameProtector.tsx +++ b/apps/dotcom/src/components/IFrameProtector.tsx @@ -114,7 +114,7 @@ export function IFrameProtector({
- {'Visit this page on tldraw.com '} + {'Visit this page on tldraw.com'} { - let showBackToContentPrev = false - - const interval = setInterval(() => { - const renderingShapes = editor.getRenderingShapes() - const renderingBounds = editor.getRenderingBounds() - - // Rendering shapes includes all the shapes in the current page. - // We have to filter them down to just the shapes that are inside the renderingBounds. - const visibleShapes = renderingShapes.filter((s) => { - const maskedPageBounds = editor.getShapeMaskedPageBounds(s.id) - return maskedPageBounds && renderingBounds.includes(maskedPageBounds) - }) - const showBackToContentNow = - visibleShapes.length === 0 && editor.getCurrentPageShapes().length > 0 + useQuickReactor( + 'toggle showback to content', + () => { + const showBackToContentPrev = rIsShowing.current + const shapeIds = editor.getCurrentPageShapeIds() + let showBackToContentNow = false + if (shapeIds.size) { + showBackToContentNow = shapeIds.size === editor.getCulledShapes().size + } if (showBackToContentPrev !== showBackToContentNow) { setShowBackToContent(showBackToContentNow) - showBackToContentPrev = showBackToContentNow + rIsShowing.current = showBackToContentNow } - }, 1000) - - return () => { - clearInterval(interval) - } - }, [editor]) + }, + [editor] + ) if (!showBackToContent) return null From b5dfd81540ab266cf777b0073ccddffefe690d76 Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Fri, 19 Apr 2024 14:56:55 +0100 Subject: [PATCH 07/11] WebGL Minimap (#3510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR replaces our current minimap implementation with one that uses WebGL ### Change Type - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. --------- Co-authored-by: Steve Ruiz --- packages/editor/api-report.md | 4 + packages/editor/api/api.json | 80 +++ packages/editor/src/lib/editor/Editor.ts | 77 ++- packages/editor/src/lib/hooks/usePeerIds.ts | 10 +- packages/editor/src/lib/hooks/usePresence.ts | 14 +- packages/editor/src/lib/primitives/Mat.ts | 13 +- .../ui/components/Minimap/DefaultMinimap.tsx | 171 +++--- .../ui/components/Minimap/MinimapManager.ts | 486 ++++++++---------- .../src/lib/ui/components/Minimap/getRgba.ts | 16 + .../components/Minimap/minimap-webgl-setup.ts | 148 ++++++ .../Minimap/minimap-webgl-shapes.ts | 144 ++++++ 11 files changed, 743 insertions(+), 420 deletions(-) create mode 100644 packages/tldraw/src/lib/ui/components/Minimap/getRgba.ts create mode 100644 packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-setup.ts create mode 100644 packages/tldraw/src/lib/ui/components/Minimap/minimap-webgl-shapes.ts diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 1aa6fda13..a8039822c 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -682,6 +682,8 @@ export class Editor extends EventEmitter { getCameraState(): "idle" | "moving"; getCanRedo(): boolean; getCanUndo(): boolean; + getCollaborators(): TLInstancePresence[]; + getCollaboratorsOnCurrentPage(): TLInstancePresence[]; getContainer: () => HTMLElement; getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined; // @internal @@ -693,6 +695,8 @@ export class Editor extends EventEmitter { getCurrentPageId(): TLPageId; getCurrentPageRenderingShapesSorted(): TLShape[]; getCurrentPageShapeIds(): Set; + // @internal (undocumented) + getCurrentPageShapeIdsSorted(): TLShapeId[]; getCurrentPageShapes(): TLShape[]; getCurrentPageShapesSorted(): TLShape[]; getCurrentPageState(): TLInstancePageState; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index ce3f1394f..7dc444dd9 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10059,6 +10059,86 @@ "isAbstract": false, "name": "getCanUndo" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#getCollaborators:member(1)", + "docComment": "/**\n * Returns a list of presence records for all peer collaborators. This will return the latest presence record for each connected user.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "getCollaborators(): " + }, + { + "kind": "Content", + "text": "import(\"@tldraw/tlschema\")." + }, + { + "kind": "Reference", + "text": "TLInstancePresence", + "canonicalReference": "@tldraw/tlschema!TLInstancePresence:interface" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getCollaborators" + }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#getCollaboratorsOnCurrentPage:member(1)", + "docComment": "/**\n * Returns a list of presence records for all peer collaborators on the current page. This will return the latest presence record for each connected user.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "getCollaboratorsOnCurrentPage(): " + }, + { + "kind": "Content", + "text": "import(\"@tldraw/tlschema\")." + }, + { + "kind": "Reference", + "text": "TLInstancePresence", + "canonicalReference": "@tldraw/tlschema!TLInstancePresence:interface" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getCollaboratorsOnCurrentPage" + }, { "kind": "Property", "canonicalReference": "@tldraw/editor!Editor#getContainer:member", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index e4e9fa1bf..c74be045e 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2619,15 +2619,7 @@ export class Editor extends EventEmitter { * @public */ animateToUser(userId: string): this { - const presences = this.store.query.records('instance_presence', () => ({ - userId: { eq: userId }, - })) - - const presence = [...presences.get()] - .sort((a, b) => { - return a.lastActivityTimestamp - b.lastActivityTimestamp - }) - .pop() + const presence = this.getCollaborators().find((c) => c.userId === userId) if (!presence) return this @@ -2883,6 +2875,45 @@ export class Editor extends EventEmitter { z: point.z ?? 0.5, } } + // Collaborators + + @computed + private _getCollaboratorsQuery() { + return this.store.query.records('instance_presence', () => ({ + userId: { neq: this.user.getId() }, + })) + } + + /** + * Returns a list of presence records for all peer collaborators. + * This will return the latest presence record for each connected user. + * + * @public + */ + @computed + getCollaborators() { + const allPresenceRecords = this._getCollaboratorsQuery().get() + if (!allPresenceRecords.length) return EMPTY_ARRAY + const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort() + return userIds.map((id) => { + const latestPresence = allPresenceRecords + .filter((c) => c.userId === id) + .sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0] + return latestPresence + }) + } + + /** + * Returns a list of presence records for all peer collaborators on the current page. + * This will return the latest presence record for each connected user. + * + * @public + */ + @computed + getCollaboratorsOnCurrentPage() { + const currentPageId = this.getCurrentPageId() + return this.getCollaborators().filter((c) => c.currentPageId === currentPageId) + } // Following @@ -2894,9 +2925,9 @@ export class Editor extends EventEmitter { * @public */ startFollowingUser(userId: string): this { - const leaderPresences = this.store.query.records('instance_presence', () => ({ - userId: { eq: userId }, - })) + const leaderPresences = this._getCollaboratorsQuery() + .get() + .filter((p) => p.userId === userId) const thisUserId = this.user.getId() @@ -2905,7 +2936,7 @@ export class Editor extends EventEmitter { } // If the leader is following us, then we can't follow them - if (leaderPresences.get().some((p) => p.followingUserId === thisUserId)) { + if (leaderPresences.some((p) => p.followingUserId === thisUserId)) { return this } @@ -2924,7 +2955,7 @@ export class Editor extends EventEmitter { const moveTowardsUser = () => { // Stop following if we can't find the user - const leaderPresence = [...leaderPresences.get()] + const leaderPresence = [...leaderPresences] .sort((a, b) => { return a.lastActivityTimestamp - b.lastActivityTimestamp }) @@ -3281,6 +3312,14 @@ export class Editor extends EventEmitter { return this._currentPageShapeIds.get() } + /** + * @internal + */ + @computed + getCurrentPageShapeIdsSorted() { + return Array.from(this.getCurrentPageShapeIds()).sort() + } + /** * Get the ids of shapes on a page. * @@ -3893,7 +3932,7 @@ export class Editor extends EventEmitter { * @public */ getShapePageTransform(shape: TLShape | TLShapeId): Mat { - const id = typeof shape === 'string' ? shape : this.getShape(shape)!.id + const id = typeof shape === 'string' ? shape : shape.id return this._getShapePageTransformCache().get(id) ?? Mat.Identity() } @@ -4227,7 +4266,7 @@ export class Editor extends EventEmitter { @computed getCurrentPageBounds(): Box | undefined { let commonBounds: Box | undefined - this.getCurrentPageShapeIds().forEach((shapeId) => { + this.getCurrentPageShapeIdsSorted().forEach((shapeId) => { const bounds = this.getShapeMaskedPageBounds(shapeId) if (!bounds) return if (!commonBounds) { @@ -8159,7 +8198,11 @@ export class Editor extends EventEmitter { // it will be 0,0 when its actual screen position is equal // to screenBounds.point. This is confusing! currentScreenPoint.set(sx, sy) - currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz) + const nx = sx / cz - cx + const ny = sy / cz - cy + if (isFinite(nx) && isFinite(ny)) { + currentPagePoint.set(nx, ny, sz) + } this.inputs.isPen = info.type === 'pointer' && info.isPen diff --git a/packages/editor/src/lib/hooks/usePeerIds.ts b/packages/editor/src/lib/hooks/usePeerIds.ts index 22308aa99..add0bb996 100644 --- a/packages/editor/src/lib/hooks/usePeerIds.ts +++ b/packages/editor/src/lib/hooks/usePeerIds.ts @@ -1,5 +1,4 @@ import { useComputed, useValue } from '@tldraw/state' -import { useMemo } from 'react' import { uniq } from '../utils/uniq' import { useEditor } from './useEditor' @@ -10,17 +9,12 @@ import { useEditor } from './useEditor' */ export function usePeerIds() { const editor = useEditor() - const $presences = useMemo(() => { - return editor.store.query.records('instance_presence', () => ({ - userId: { neq: editor.user.getId() }, - })) - }, [editor]) const $userIds = useComputed( 'userIds', - () => uniq($presences.get().map((p) => p.userId)).sort(), + () => uniq(editor.getCollaborators().map((p) => p.userId)).sort(), { isEqual: (a, b) => a.join(',') === b.join?.(',') }, - [$presences] + [editor] ) return useValue($userIds) diff --git a/packages/editor/src/lib/hooks/usePresence.ts b/packages/editor/src/lib/hooks/usePresence.ts index 6f75337d5..55e51950a 100644 --- a/packages/editor/src/lib/hooks/usePresence.ts +++ b/packages/editor/src/lib/hooks/usePresence.ts @@ -1,6 +1,5 @@ import { useValue } from '@tldraw/state' import { TLInstancePresence } from '@tldraw/tlschema' -import { useMemo } from 'react' import { useEditor } from './useEditor' // TODO: maybe move this to a computed property on the App class? @@ -11,21 +10,12 @@ import { useEditor } from './useEditor' export function usePresence(userId: string): TLInstancePresence | null { const editor = useEditor() - const $presences = useMemo(() => { - return editor.store.query.records('instance_presence', () => ({ - userId: { eq: userId }, - })) - }, [editor, userId]) - const latestPresence = useValue( `latestPresence:${userId}`, () => { - return $presences - .get() - .slice() - .sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0] + return editor.getCollaborators().find((c) => c.userId === userId) }, - [] + [editor] ) return latestPresence ?? null diff --git a/packages/editor/src/lib/primitives/Mat.ts b/packages/editor/src/lib/primitives/Mat.ts index b2388fcb1..14d874c21 100644 --- a/packages/editor/src/lib/primitives/Mat.ts +++ b/packages/editor/src/lib/primitives/Mat.ts @@ -39,12 +39,13 @@ export class Mat { equals(m: Mat | MatModel) { return ( - this.a === m.a && - this.b === m.b && - this.c === m.c && - this.d === m.d && - this.e === m.e && - this.f === m.f + this === m || + (this.a === m.a && + this.b === m.b && + this.c === m.c && + this.d === m.d && + this.e === m.e && + this.f === m.f) ) } diff --git a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx index a84eba262..82436126a 100644 --- a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx +++ b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx @@ -1,18 +1,13 @@ import { ANIMATION_MEDIUM_MS, - Box, TLPointerEventInfo, - TLShapeId, Vec, getPointerInfo, - intersectPolygonPolygon, normalizeWheel, releasePointerCapture, setPointerCapture, - useComputed, useEditor, useIsDarkMode, - useQuickReactor, } from '@tldraw/editor' import * as React from 'react' import { MinimapManager } from './MinimapManager' @@ -24,67 +19,78 @@ export function DefaultMinimap() { const rCanvas = React.useRef(null!) const rPointing = React.useRef(false) - const isDarkMode = useIsDarkMode() - const devicePixelRatio = useComputed('dpr', () => editor.getInstanceState().devicePixelRatio, [ - editor, - ]) - const presences = React.useMemo(() => editor.store.query.records('instance_presence'), [editor]) - - const minimap = React.useMemo(() => new MinimapManager(editor), [editor]) + const minimapRef = React.useRef() React.useEffect(() => { - // Must check after render - const raf = requestAnimationFrame(() => { - minimap.updateColors() - minimap.render() - }) - return () => { - cancelAnimationFrame(raf) - } - }, [editor, minimap, isDarkMode]) + const minimap = new MinimapManager(editor, rCanvas.current) + minimapRef.current = minimap + return minimapRef.current.close + }, [editor]) const onDoubleClick = React.useCallback( (e: React.MouseEvent) => { if (!editor.getCurrentPageShapeIds().size) return + if (!minimapRef.current) return - const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false) + const point = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + false + ) - const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true) + const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + true + ) - minimap.originPagePoint.setTo(clampedPoint) - minimap.originPageCenter.setTo(editor.getViewportPageBounds().center) + minimapRef.current.originPagePoint.setTo(clampedPoint) + minimapRef.current.originPageCenter.setTo(editor.getViewportPageBounds().center) editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS }) }, - [editor, minimap] + [editor] ) const onPointerDown = React.useCallback( (e: React.PointerEvent) => { + if (!minimapRef.current) return const elm = e.currentTarget setPointerCapture(elm, e) if (!editor.getCurrentPageShapeIds().size) return rPointing.current = true - minimap.isInViewport = false + minimapRef.current.isInViewport = false - const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false) + const point = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + false + ) - const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true) + const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + true + ) const _vpPageBounds = editor.getViewportPageBounds() - minimap.isInViewport = _vpPageBounds.containsPoint(clampedPoint) + minimapRef.current.isInViewport = _vpPageBounds.containsPoint(clampedPoint) - if (minimap.isInViewport) { - minimap.originPagePoint.setTo(clampedPoint) - minimap.originPageCenter.setTo(_vpPageBounds.center) + if (minimapRef.current.isInViewport) { + minimapRef.current.originPagePoint.setTo(clampedPoint) + minimapRef.current.originPageCenter.setTo(_vpPageBounds.center) } else { const delta = Vec.Sub(_vpPageBounds.center, _vpPageBounds.point) const pagePoint = Vec.Add(point, delta) - minimap.originPagePoint.setTo(pagePoint) - minimap.originPageCenter.setTo(point) + minimapRef.current.originPagePoint.setTo(pagePoint) + minimapRef.current.originPageCenter.setTo(point) editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS }) } @@ -98,16 +104,24 @@ export function DefaultMinimap() { document.body.addEventListener('pointerup', release) }, - [editor, minimap] + [editor] ) const onPointerMove = React.useCallback( (e: React.PointerEvent) => { - 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 +} From b5fab15c6d8c2b5efa7a8f1272b865620cff8923 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 21 Apr 2024 12:45:55 +0100 Subject: [PATCH 08/11] Prevent default on native clipboard events (#3536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR calls prevent default on native clipboard events. This prevents the error sound on Safari. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix ### Test Plan 1. Use the cut, copy, and paste events on Safari. 2. Everything should still work, but no sounds should play. ### Release Notes - Fix copy sound on clipboard events. --- .../src/lib/ui/hooks/useClipboardEvents.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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' }) } From a6d2ab05d27b58eef4aa22b64cba48b6ff1b60a1 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 21 Apr 2024 12:46:35 +0100 Subject: [PATCH 09/11] Perf: minor drawing speedup (#3464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/lib/shapes/draw/toolStates/Drawing.ts | 8 +- .../src/lib/shapes/shared/freehand/svgInk.ts | 83 +- .../test/__snapshots__/drawing.test.ts.snap | 1287 +++++++++++++++++ packages/tldraw/src/test/drawing.data.ts | 1006 +++++++++++++ packages/tldraw/src/test/drawing.test.ts | 20 + packages/utils/src/lib/perf.ts | 18 +- 6 files changed, 2369 insertions(+), 53 deletions(-) create mode 100644 packages/tldraw/src/test/__snapshots__/drawing.test.ts.snap create mode 100644 packages/tldraw/src/test/drawing.data.ts diff --git a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts index 5d6f7c548..8c23577e1 100644 --- a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts +++ b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts @@ -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(id) } - private updateShapes() { + private updateDrawingShape() { const { initialShape } = this const { inputs } = this.editor diff --git a/packages/tldraw/src/lib/shapes/shared/freehand/svgInk.ts b/packages/tldraw/src/lib/shapes/shared/freehand/svgInk.ts index bb60d358b..eb1241877 100644 --- a/packages/tldraw/src/lib/shapes/shared/freehand/svgInk.ts +++ b/packages/tldraw/src/lib/shapes/shared/freehand/svgInk.ts @@ -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 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 }) From 4507ce6378bb3b506d17c238679ca85b63d5827e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:39:38 +0000 Subject: [PATCH 10/11] Bump the npm_and_yarn group across 1 directory with 2 updates (#3505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm_and_yarn group with 2 updates in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [tar](https://github.com/isaacs/node-tar). Updates `vite` from 5.2.8 to 5.2.9
Changelog

Sourced from vite's changelog.

5.2.9 (2024-04-15)

Commits

Updates `tar` from 6.2.1 to 7.0.1
Changelog

Sourced from tar's changelog.

Changelog

7.0

  • Rewrite in TypeScript, provide ESM and CommonJS hybrid interface
  • Add tree-shake friendly exports, like import('tar/create') and import('tar/read-entry') to get individual functions or classes.
  • Add chmod option that defaults to false, and deprecate noChmod. That is, reverse the default option regarding explicitly setting file system modes to match tar entry settings.
  • Add processUmask option to avoid having to call process.umask() when chmod: true (or noChmod: false) is set.

6.2

  • Add support for brotli compression
  • Add maxDepth option to prevent extraction into excessively deep folders.

6.1

6.0

  • Drop support for node 6 and 8
  • fix symlinks and hardlinks on windows being packed with \-style path targets

5.0

  • Address unpack race conditions using path reservations
  • Change large-numbers errors from TypeError to Error
  • Add TAR_* error codes
  • Raise TAR_BAD_ARCHIVE warning/error when there are no valid entries found in an archive
  • do not treat ignored entries as an invalid archive
  • drop support for node v4
  • unpack: conditionally use a file mapping to write files on Windows
  • Set more portable 'mode' value in portable mode
  • Set portable gzip option in portable mode

... (truncated)

Commits
  • d99fce3 7.0.1
  • af04392 Do not apply linkpath,global from global pax header
  • b0fbdea 7.0.0
  • 957da75 remove old lib folder
  • 9a260c2 test verifying #398 is fixed
  • 2d89a4e Properly handle long linkpath in PaxHeader
  • 314ec7e list: close file even if no error thrown
  • b3afdbb unpack test: use modern tap features
  • 2330416 test: code style, prefer () to _ for empty fns
  • ae9ce7e test: fix normalize-unicode coverage on linux
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tldraw/tldraw/network/alerts).
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mime Čuvalo Co-authored-by: Steve Ruiz --- scripts/deploy.ts | 2 +- scripts/lib/didAnyPackageChange.ts | 45 +++++------- scripts/package.json | 2 +- yarn.lock | 113 +++++++++++++++++++++++------ 4 files changed, 110 insertions(+), 52 deletions(-) diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 4c39f387d..ee15826cb 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -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..091173913 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -59,7 +59,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..ef4b8a595 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" @@ -7589,7 +7598,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 @@ -10700,6 +10709,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 +14661,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 +16291,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 +17737,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 @@ -19131,7 +19147,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 +19164,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 +19190,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 +20362,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 +22080,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 +23424,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 +23438,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 +25024,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 +25059,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1 + checksum: 26342c8dde540e4161fdad2c9c8f2f0e23567f051c7a40abb8e4796d6c4292fbd118ab7a4ac252515e78c4f99525b557731e6117287b2bccde0ea61d73bcff27 languageName: node linkType: hard @@ -25666,6 +25726,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" From cce794e04be380e6839ecfaad29730786172d423 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 22 Apr 2024 11:32:22 +0100 Subject: [PATCH 11/11] Expose `usePreloadAssets` (#3545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose `usePreloadAssets` and make sure the exploded/sublibraries examples uses it. Before this change, fonts weren't loaded correctly for the exploded example. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `docs` — Changes to the documentation, examples, or templates. - [x] `bugfix` — Bug fix --- .../src/examples/exploded/ExplodedExample.tsx | 14 ++++++ packages/tldraw/api-report.md | 6 +++ packages/tldraw/api/api.json | 46 +++++++++++++++++++ packages/tldraw/src/index.ts | 1 + .../src/lib/ui/hooks/usePreloadAssets.ts | 1 + 5 files changed, 68 insertions(+) diff --git a/apps/examples/src/examples/exploded/ExplodedExample.tsx b/apps/examples/src/examples/exploded/ExplodedExample.tsx index f53dce67c..5f9d0e09b 100644 --- a/apps/examples/src/examples/exploded/ExplodedExample.tsx +++ b/apps/examples/src/examples/exploded/ExplodedExample.tsx @@ -1,6 +1,8 @@ import { ContextMenu, DefaultContextMenuContent, + ErrorScreen, + LoadingScreen, TldrawEditor, TldrawHandles, TldrawScribble, @@ -10,7 +12,9 @@ import { defaultShapeTools, defaultShapeUtils, defaultTools, + usePreloadAssets, } from 'tldraw' +import { defaultEditorAssetUrls } from 'tldraw/src/lib/utils/static-assets/assetUrls' import 'tldraw/tldraw.css' // There's a guide at the bottom of this file! @@ -26,6 +30,16 @@ const defaultComponents = { //[2] export default function ExplodedExample() { + const assetLoading = usePreloadAssets(defaultEditorAssetUrls) + + if (assetLoading.error) { + return Could not load assets. + } + + if (!assetLoading.done) { + return Loading assets... + } + return (
void): reado // @public (undocumented) export function useNativeClipboardEvents(): void; +// @public (undocumented) +export function usePreloadAssets(assetUrls: TLEditorAssetUrls): { + done: boolean; + error: boolean; +}; + // @public (undocumented) export function useReadonly(): boolean; diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index a68690d00..1210a98fc 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -27982,6 +27982,52 @@ "parameters": [], "name": "useNativeClipboardEvents" }, + { + "kind": "Function", + "canonicalReference": "tldraw!usePreloadAssets:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function usePreloadAssets(assetUrls: " + }, + { + "kind": "Reference", + "text": "TLEditorAssetUrls", + "canonicalReference": "tldraw!~TLEditorAssetUrls:type" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "{\n done: boolean;\n error: boolean;\n}" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/ui/hooks/usePreloadAssets.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "assetUrls", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "usePreloadAssets" + }, { "kind": "Function", "canonicalReference": "tldraw!useReadonly:function(1)", diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index cc2a98d15..6f37098e5 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -87,6 +87,7 @@ export { useExportAs } from './lib/ui/hooks/useExportAs' export { useKeyboardShortcuts } from './lib/ui/hooks/useKeyboardShortcuts' export { useLocalStorageState } from './lib/ui/hooks/useLocalStorageState' export { useMenuIsOpen } from './lib/ui/hooks/useMenuIsOpen' +export { usePreloadAssets } from './lib/ui/hooks/usePreloadAssets' export { useReadonly } from './lib/ui/hooks/useReadonly' export { useRelevantStyles } from './lib/ui/hooks/useRelevantStyles' export { 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])