From a18525ea7894aa49ca4408eec8c1bb37d2cda6f7 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Apr 2024 15:02:05 +0100 Subject: [PATCH 01/54] Fix SVG exports in Next.js (#3446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js bans the use of react-dom/server APIs on the client. React's docs recommend against using these too: https://react.dev/reference/react-dom/server/renderToString#removing-rendertostring-from-the-client-code In this diff, we switch from using `ReactDOMServer.renderToStaticMarkup` to `ReactDOMClient.createRoot`, fixing SVG exports in next.js apps. `getSvg` remains deprecated, but we've introduced a new `getSvgElement` method with a similar API to `getSvgString` - it returns an `{svg, width, height}` object. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix --- packages/editor/api-report.md | 5 + packages/editor/api/api.json | 108 +++++++++++++++++- packages/editor/src/lib/editor/Editor.ts | 53 ++++++--- packages/editor/src/lib/editor/getSvgJsx.tsx | 1 - .../__snapshots__/getSvgString.test.ts.snap | 2 +- 5 files changed, 153 insertions(+), 16 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index f78dd11c7..e2ca21d9d 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -767,6 +767,11 @@ export class Editor extends EventEmitter { getStyleForNextShape(style: StyleProp): T; // @deprecated (undocumented) getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial): Promise; + getSvgElement(shapes: TLShape[] | TLShapeId[], opts?: Partial): Promise<{ + svg: SVGSVGElement; + width: number; + height: number; + } | undefined>; getSvgString(shapes: TLShape[] | TLShapeId[], opts?: Partial): Promise<{ svg: string; width: number; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index e0d8317a7..d6c01d768 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -13888,7 +13888,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getSvg:member(1)", - "docComment": "/**\n * @deprecated\n *\n * Use {@link Editor.getSvgString} instead\n */\n", + "docComment": "/**\n * @deprecated\n *\n * Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -13991,6 +13991,112 @@ "isAbstract": false, "name": "getSvg" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#getSvgElement:member(1)", + "docComment": "/**\n * Get an exported SVG element of the given shapes.\n *\n * @param ids - The shapes (or shape ids) to export.\n *\n * @param opts - Options for the export.\n *\n * @returns The SVG element.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "getSvgElement(shapes: " + }, + { + "kind": "Reference", + "text": "TLShape", + "canonicalReference": "@tldraw/tlschema!TLShape:type" + }, + { + "kind": "Content", + "text": "[] | " + }, + { + "kind": "Reference", + "text": "TLShapeId", + "canonicalReference": "@tldraw/tlschema!TLShapeId:type" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ", opts?: " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLSvgOptions", + "canonicalReference": "@tldraw/editor!TLSvgOptions:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" + }, + { + "kind": "Content", + "text": "<{\n svg: " + }, + { + "kind": "Reference", + "text": "SVGSVGElement", + "canonicalReference": "!SVGSVGElement:interface" + }, + { + "kind": "Content", + "text": ";\n width: number;\n height: number;\n } | undefined>" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 11, + "endIndex": 15 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "shapes", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isOptional": false + }, + { + "parameterName": "opts", + "parameterTypeTokenRange": { + "startIndex": 6, + "endIndex": 10 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "getSvgElement" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getSvgString:member(1)", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 33a1de566..79ed3ac0f 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -61,7 +61,6 @@ import { import { EventEmitter } from 'eventemitter3' import { flushSync } from 'react-dom' import { createRoot } from 'react-dom/client' -import { renderToStaticMarkup } from 'react-dom/server' import { TLUser, createTLUser } from '../config/createTLUser' import { checkShapesAndAddCore } from '../config/defaultShapes' import { @@ -8070,6 +8069,33 @@ export class Editor extends EventEmitter { return this } + /** + * Get an exported SVG element of the given shapes. + * + * @param ids - The shapes (or shape ids) to export. + * @param opts - Options for the export. + * + * @returns The SVG element. + * + * @public + */ + async getSvgElement(shapes: TLShapeId[] | TLShape[], opts = {} as Partial) { + const result = await getSvgJsx(this, shapes, opts) + if (!result) return undefined + + const fragment = document.createDocumentFragment() + const root = createRoot(fragment) + flushSync(() => { + root.render(result.jsx) + }) + + const svg = fragment.firstElementChild + assert(svg instanceof SVGSVGElement, 'Expected an SVG element') + + root.unmount() + return { svg, width: result.width, height: result.height } + } + /** * Get an exported SVG string of the given shapes. * @@ -8081,21 +8107,22 @@ export class Editor extends EventEmitter { * @public */ async getSvgString(shapes: TLShapeId[] | TLShape[], opts = {} as Partial) { - const svg = await getSvgJsx(this, shapes, opts) - if (!svg) return undefined - return { svg: renderToStaticMarkup(svg.jsx), width: svg.width, height: svg.height } + const result = await this.getSvgElement(shapes, opts) + if (!result) return undefined + + const serializer = new XMLSerializer() + return { + svg: serializer.serializeToString(result.svg), + width: result.width, + height: result.height, + } } - /** @deprecated Use {@link Editor.getSvgString} instead */ + /** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */ async getSvg(shapes: TLShapeId[] | TLShape[], opts = {} as Partial) { - const svg = await getSvgJsx(this, shapes, opts) - if (!svg) return undefined - const fragment = new DocumentFragment() - const root = createRoot(fragment) - flushSync(() => root.render(svg.jsx)) - const rendered = fragment.firstElementChild - root.unmount() - return rendered as SVGSVGElement + const result = await this.getSvgElement(shapes, opts) + if (!result) return undefined + return result.svg } /* --------------------- Events --------------------- */ diff --git a/packages/editor/src/lib/editor/getSvgJsx.tsx b/packages/editor/src/lib/editor/getSvgJsx.tsx index 1f16b0ce1..f86bef475 100644 --- a/packages/editor/src/lib/editor/getSvgJsx.tsx +++ b/packages/editor/src/lib/editor/getSvgJsx.tsx @@ -184,7 +184,6 @@ export async function getSvgJsx( const svg = ( Date: Thu, 11 Apr 2024 16:31:21 +0100 Subject: [PATCH 02/54] Performance measurement tool (for unit tests) (#3447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a micro benchmarking utility. We can use it in our jest tests or in random scripts, though given the other requirements of our library, benchmarking. Screenshot 2024-04-11 at 2 44 23 PM ## What this isn't This is not benchmarking. The speeds etc are based on your machine. ## What this is This is a tool for measuring / comparing different implementations etc. Some things run much faster than others. ### Change Type - [x] `sdk` - [x] `internal` --- .../src/test/perf/PerformanceMeasurer.ts | 156 ++++++++++++++ packages/tldraw/src/test/perf/perf.test.ts | 191 ++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 packages/tldraw/src/test/perf/PerformanceMeasurer.ts create mode 100644 packages/tldraw/src/test/perf/perf.test.ts diff --git a/packages/tldraw/src/test/perf/PerformanceMeasurer.ts b/packages/tldraw/src/test/perf/PerformanceMeasurer.ts new file mode 100644 index 000000000..b07329a3d --- /dev/null +++ b/packages/tldraw/src/test/perf/PerformanceMeasurer.ts @@ -0,0 +1,156 @@ +const now = () => { + const hrTime = process.hrtime() + return hrTime[0] * 1000 + hrTime[1] / 1000000 +} + +export class PerformanceMeasurer { + private setupFn?: () => void + private beforeFns: (() => void)[] = [] + private fns: (() => void)[] = [] + private afterFns: (() => void)[] = [] + private teardownFn?: () => void + private warmupIterations = 0 + private iterations = 0 + + total = 0 + average = 0 + cold = 0 + fastest = Infinity + slowest = -Infinity + + totalStart = 0 + totalEnd = 0 + totalTime = 0 + + constructor( + public name: string, + opts = {} as { + warmupIterations?: number + iterations?: number + } + ) { + const { warmupIterations = 0, iterations = 10 } = opts + this.warmupIterations = warmupIterations + this.iterations = iterations + } + + setup(cb: () => void) { + this.setupFn = cb + return this + } + + teardown(cb: () => void) { + this.teardownFn = cb + return this + } + + add(cb: () => void) { + this.fns.push(cb) + return this + } + + before(cb: () => void) { + this.beforeFns.push(cb) + return this + } + + after(cb: () => void) { + this.afterFns.push(cb) + return this + } + + run() { + const { fns, beforeFns, afterFns, warmupIterations, iterations } = this + + // Run the cold run + this.setupFn?.() + const a = now() + for (let k = 0; k < beforeFns.length; k++) { + beforeFns[k]() + } + for (let j = 0; j < fns.length; j++) { + fns[j]() + } + for (let l = 0; l < afterFns.length; l++) { + afterFns[l]() + } + const duration = now() - a + this.cold = duration + this.teardownFn?.() + + // Run all of the warmup iterations + if (this.warmupIterations > 0) { + this.setupFn?.() + for (let i = 0; i < warmupIterations; i++) { + for (let k = 0; k < beforeFns.length; k++) { + beforeFns[k]() + } + const _a = now() + for (let j = 0; j < fns.length; j++) { + fns[j]() + } + const _duration = now() - a + for (let l = 0; l < afterFns.length; l++) { + afterFns[l]() + } + } + this.teardownFn?.() + } + + this.totalStart = now() + + // Run all of the iterations and calculate average + if (this.iterations > 0) { + this.setupFn?.() + for (let i = 0; i < iterations; i++) { + for (let k = 0; k < beforeFns.length; k++) { + beforeFns[k]() + } + const a = now() + for (let j = 0; j < fns.length; j++) { + fns[j]() + } + const duration = now() - a + this.total += duration + this.fastest = Math.min(duration, this.fastest) + this.slowest = Math.max(duration, this.fastest) + for (let l = 0; l < afterFns.length; l++) { + afterFns[l]() + } + } + this.teardownFn?.() + } + + this.totalTime = now() - this.totalStart + if (iterations > 0) { + this.average = this.total / iterations + } + + return this + } + + report() { + return PerformanceMeasurer.Table(this) + } + + static Table(...ps: PerformanceMeasurer[]) { + const table: Record> = {} + const fastest = ps.map((p) => p.average).reduce((a, b) => Math.min(a, b)) + const totalFastest = ps.map((p) => p.totalTime).reduce((a, b) => Math.min(a, b)) + + ps.forEach( + (p) => + (table[p.name] = { + ['Runs']: p.warmupIterations + p.iterations, + ['Cold']: Number(p.cold.toFixed(2)), + ['Slowest']: Number(p.slowest.toFixed(2)), + ['Fastest']: Number(p.fastest.toFixed(2)), + ['Average']: Number(p.average.toFixed(2)), + ['Slower (Avg)']: Number((p.average / fastest).toFixed(2)), + ['Slower (All)']: Number((p.totalTime / totalFastest).toFixed(2)), + }) + ) + // eslint-disable-next-line no-console + console.table(table) + } +} diff --git a/packages/tldraw/src/test/perf/perf.test.ts b/packages/tldraw/src/test/perf/perf.test.ts new file mode 100644 index 000000000..88593cecc --- /dev/null +++ b/packages/tldraw/src/test/perf/perf.test.ts @@ -0,0 +1,191 @@ +import { TLShapePartial, createShapeId } from '@tldraw/editor' +import { TestEditor } from '../TestEditor' +import { PerformanceMeasurer } from './PerformanceMeasurer' + +let editor = new TestEditor() + +jest.useRealTimers() + +describe.skip('Example perf tests', () => { + it('measures Editor.createShape vs Editor.createShapes', () => { + const withCreateShape = new PerformanceMeasurer('Create 200 shapes using Editor.createShape', { + warmupIterations: 10, + iterations: 10, + }) + .before(() => { + editor = new TestEditor() + }) + .add(() => { + for (let i = 0; i < 200; i++) { + editor.createShape({ + type: 'geo', + x: (i % 10) * 220, + y: Math.floor(i / 10) * 220, + props: { w: 200, h: 200 }, + }) + } + }) + + const withCreateShapes = new PerformanceMeasurer( + 'Create 200 shapes using Editor.createShapes', + { + warmupIterations: 10, + iterations: 10, + } + ) + .before(() => { + editor = new TestEditor() + }) + .add(() => { + const shapesToCreate: TLShapePartial[] = [] + for (let i = 0; i < 200; i++) { + shapesToCreate.push({ + id: createShapeId(), + type: 'geo', + x: (i % 10) * 220, + y: Math.floor(i / 10) * 220, + props: { w: 200, h: 200 }, + }) + } + + editor.createShapes(shapesToCreate) + }) + + withCreateShape.run() + withCreateShapes.run() + + PerformanceMeasurer.Table(withCreateShape, withCreateShapes) + + expect(withCreateShape.average).toBeGreaterThan(withCreateShapes.average) + }, 10000) + + it('measures Editor.updateShape vs Editor.updateShapes', () => { + const ids = Array.from(Array(200)).map(() => createShapeId()) + + const withUpdateShape = new PerformanceMeasurer('Update 200 shapes using Editor.updateShape', { + warmupIterations: 10, + iterations: 10, + }) + .before(() => { + editor = new TestEditor() + for (let i = 0; i < 200; i++) { + editor.createShape({ + type: 'geo', + id: ids[i], + x: (i % 10) * 220, + y: Math.floor(i / 10) * 220, + props: { w: 200, h: 200 }, + }) + } + }) + .add(() => { + for (let i = 0; i < 200; i++) { + editor.updateShape({ + type: 'geo', + id: ids[i], + x: (i % 10) * 220 + 1, + props: { + w: 201, + }, + }) + } + }) + + const withUpdateShapes = new PerformanceMeasurer( + 'Update 200 shapes using Editor.updateShapes', + { + warmupIterations: 10, + iterations: 10, + } + ) + .before(() => { + editor = new TestEditor() + for (let i = 0; i < 200; i++) { + editor.createShape({ + id: ids[i], + type: 'geo', + x: (i % 10) * 220, + y: Math.floor(i / 10) * 220, + props: { w: 200, h: 200 }, + }) + } + }) + .add(() => { + const shapesToUpdate: TLShapePartial[] = [] + for (let i = 0; i < 200; i++) { + shapesToUpdate.push({ + id: ids[i], + type: 'geo', + x: (i % 10) * 220 + 1, + props: { + w: 201, + }, + }) + } + editor.updateShapes(shapesToUpdate) + }) + + withUpdateShape.run() + withUpdateShapes.run() + + PerformanceMeasurer.Table(withUpdateShape, withUpdateShapes) + }, 10000) + + it('Measures rendering shapes', () => { + const renderingShapes = new PerformanceMeasurer('Measure rendering bounds with 100 shapes', { + warmupIterations: 10, + iterations: 20, + }) + .before(() => { + editor = new TestEditor() + const shapesToCreate: TLShapePartial[] = [] + for (let i = 0; i < 100; i++) { + shapesToCreate.push({ + id: createShapeId(), + type: 'geo', + x: (i % 10) * 220, + y: Math.floor(i / 10) * 220, + props: { w: 200, h: 200 }, + }) + } + editor.createShapes(shapesToCreate) + }) + .add(() => { + editor.getRenderingShapes() + }) + .after(() => { + const shape = editor.getCurrentPageShapes()[0] + editor.updateShape({ ...shape, x: shape.x + 1 }) + }) + .run() + + const renderingShapes2 = new PerformanceMeasurer('Measure rendering bounds with 200 shapes', { + warmupIterations: 10, + iterations: 20, + }) + .before(() => { + editor = new TestEditor() + const shapesToCreate: TLShapePartial[] = [] + for (let i = 0; i < 200; i++) { + shapesToCreate.push({ + id: createShapeId(), + type: 'geo', + x: (i % 10) * 220, + y: Math.floor(i / 10) * 220, + props: { w: 200, h: 200 }, + }) + } + editor.createShapes(shapesToCreate) + }) + .add(() => { + editor.getRenderingShapes() + }) + .after(() => { + const shape = editor.getCurrentPageShapes()[0] + editor.updateShape({ ...shape, x: shape.x + 1 }) + }) + .run() + + PerformanceMeasurer.Table(renderingShapes, renderingShapes2) + }, 10000) +}) From 6d5ec149fadd3da03b1e359235a90a6396ff7f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 11 Apr 2024 18:00:56 +0200 Subject: [PATCH 03/54] Fix panning. (#3445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We also need to clear the timeout when panning. https://github.com/tldraw/tldraw/assets/2523721/f32fd4d0-332c-4a80-bed0-9ce49a68e1ab https://github.com/tldraw/tldraw/assets/2523721/e97f5fac-083f-4f77-ab72-40701790f039 Had an [alternative approach](https://github.com/tldraw/tldraw/pull/3444) of setting timeouts and clearing them in dispatch, but since the timeout is 500ms I think this should work as well. ### 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/editor/src/lib/editor/Editor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 79ed3ac0f..df7df0c50 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8668,6 +8668,7 @@ export class Editor extends EventEmitter { } if (this.inputs.isPanning && this.inputs.isPointing) { + clearTimeout(this._longPressTimeout) // Handle panning const { currentScreenPoint, previousScreenPoint } = this.inputs this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint)) From 6cd498a1ed6420c04a1a522115a381eb0e4bff75 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 11 Apr 2024 17:57:14 +0100 Subject: [PATCH 04/54] Remove docs for Editor.batch (#3451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove misleading docs for `Editor.batch`. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix --- packages/editor/api/api.json | 2 +- packages/editor/src/lib/editor/Editor.ts | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index d6c01d768..6945f077c 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -7947,7 +7947,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#batch:member(1)", - "docComment": "/**\n * Run a function in a batch, which will be undone/redone as a single action.\n *\n * @example\n * ```ts\n * editor.batch(() => {\n * \teditor.selectAll()\n * \teditor.deleteShapes(editor.getSelectedShapeIds())\n * \teditor.createShapes(myShapes)\n * \teditor.selectNone()\n * })\n *\n * editor.undo() // will undo all of the above\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Run a function in a batch.\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index df7df0c50..4f1b12f72 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -885,19 +885,7 @@ export class Editor extends EventEmitter { } /** - * Run a function in a batch, which will be undone/redone as a single action. - * - * @example - * ```ts - * editor.batch(() => { - * editor.selectAll() - * editor.deleteShapes(editor.getSelectedShapeIds()) - * editor.createShapes(myShapes) - * editor.selectNone() - * }) - * - * editor.undo() // will undo all of the above - * ``` + * Run a function in a batch. * * @public */ From 152b915704573f7bac5ffa9403e5e1467b31f4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 12 Apr 2024 07:34:24 +0200 Subject: [PATCH 05/54] [hotfix] Panning fix for VS Code (#3452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the panning hotfix got merged I created a branch from that, then bumped the vscode version and created a new version of the extension so that the extension also gets 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 --------- Co-authored-by: SomeHats Co-authored-by: ds300 Co-authored-by: alex Co-authored-by: GitHub Co-authored-by: mimecuvalo Co-authored-by: Steve Ruiz Co-authored-by: David Sheldrick Co-authored-by: steveruizok --- 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 c40252b56..7258ab1d7 100644 --- a/apps/vscode/extension/CHANGELOG.md +++ b/apps/vscode/extension/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.28 + +- Fix an issue with panning the canvas. + ## 2.0.27 - Bug fixes and performance improvements. diff --git a/apps/vscode/extension/package.json b/apps/vscode/extension/package.json index 9558bbf95..3f78d29f0 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.27", + "version": "2.0.28", "private": true, "author": { "name": "tldraw Inc.", From 3ceebc82f8adeba922f2feafbd38e5eed2822445 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 13 Apr 2024 14:30:30 +0100 Subject: [PATCH 06/54] Faster selection / erasing (#3454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes a small improvement to the way we measure distances. (Often we measure distances multiple times per frame per shape on the screen). In many cases, we compare a minimum distance. This makes those checks faster by avoiding a square root. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` — Improving existing features ### Release Notes - Improve performance of minimum distance checks. --- .../src/examples/exploded/ExplodedExample.tsx | 2 - packages/editor/api-report.md | 24 +- packages/editor/api/api.json | 272 +++++++----------- packages/editor/editor.css | 2 +- packages/editor/src/index.ts | 4 - .../default-components/DefaultCanvas.tsx | 81 +++--- .../DefaultHoveredShapeIndicator.tsx | 14 - .../DefaultShapeIndicator.tsx | 31 +- packages/editor/src/lib/editor/Editor.ts | 3 +- .../managers/SnapManager/HandleSnaps.ts | 11 +- .../shapes/shared/arrow/curved-arrow.ts | 3 +- .../shapes/shared/arrow/straight-arrow.ts | 5 +- .../editor/src/lib/editor/tools/StateNode.ts | 10 +- .../src/lib/hooks/useEditorComponents.tsx | 6 - packages/editor/src/lib/primitives/Vec.ts | 10 +- .../src/lib/primitives/geometry/Arc2d.ts | 2 +- .../src/lib/primitives/geometry/Circle2d.ts | 4 +- .../lib/primitives/geometry/CubicSpline2d.ts | 4 +- .../src/lib/primitives/geometry/Edge2d.ts | 6 +- .../src/lib/primitives/geometry/Ellipse2d.ts | 4 +- .../src/lib/primitives/geometry/Geometry2d.ts | 52 +++- .../src/lib/primitives/geometry/Polyline2d.ts | 10 +- .../src/lib/primitives/geometry/Stadium2d.ts | 31 +- packages/tldraw/api-report.md | 4 - packages/tldraw/api/api.json | 55 ---- packages/tldraw/src/index.ts | 1 - packages/tldraw/src/lib/Tldraw.tsx | 2 - .../canvas/TldrawHoveredShapeIndicator.tsx | 32 --- .../src/lib/shapes/draw/toolStates/Drawing.ts | 22 +- .../tldraw/src/lib/shapes/geo/cloudOutline.ts | 4 +- .../lib/shapes/line/toolStates/Pointing.ts | 4 +- .../shapes/shared/freehand/getStrokePoints.ts | 6 +- .../tools/EraserTool/childStates/Erasing.ts | 50 ++-- .../tools/SelectTool/childStates/Brushing.ts | 79 +++-- .../childStates/PointingArrowLabel.ts | 4 +- .../childStates/ScribbleBrushing.ts | 66 +++-- .../tldraw/src/test/TldrawEditor.test.tsx | 7 +- .../src/test/perf/PerformanceMeasurer.ts | 21 +- packages/tldraw/src/test/perf/perf.test.ts | 32 ++- 39 files changed, 441 insertions(+), 539 deletions(-) delete mode 100644 packages/editor/src/lib/components/default-components/DefaultHoveredShapeIndicator.tsx delete mode 100644 packages/tldraw/src/lib/canvas/TldrawHoveredShapeIndicator.tsx diff --git a/apps/examples/src/examples/exploded/ExplodedExample.tsx b/apps/examples/src/examples/exploded/ExplodedExample.tsx index f38ed1b13..f53dce67c 100644 --- a/apps/examples/src/examples/exploded/ExplodedExample.tsx +++ b/apps/examples/src/examples/exploded/ExplodedExample.tsx @@ -3,7 +3,6 @@ import { DefaultContextMenuContent, TldrawEditor, TldrawHandles, - TldrawHoveredShapeIndicator, TldrawScribble, TldrawSelectionBackground, TldrawSelectionForeground, @@ -23,7 +22,6 @@ const defaultComponents = { SelectionForeground: TldrawSelectionForeground, SelectionBackground: TldrawSelectionBackground, Handles: TldrawHandles, - HoveredShapeIndicator: TldrawHoveredShapeIndicator, } //[2] diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index e2ca21d9d..7f6ed4c04 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -118,7 +118,7 @@ export class Arc2d extends Geometry2d { // (undocumented) getVertices(): Vec[]; // (undocumented) - hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean; + hitTestLineSegment(A: Vec, B: Vec): boolean; // (undocumented) length: number; // (undocumented) @@ -332,7 +332,7 @@ export class Circle2d extends Geometry2d { // (undocumented) getVertices(): Vec[]; // (undocumented) - hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean; + hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean; // (undocumented) nearestPoint(point: Vec): Vec; // (undocumented) @@ -414,7 +414,7 @@ export class CubicSpline2d extends Geometry2d { // (undocumented) getVertices(): Vec[]; // (undocumented) - hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean; + hitTestLineSegment(A: Vec, B: Vec): boolean; // (undocumented) get length(): number; // (undocumented) @@ -471,9 +471,6 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro // @public (undocumented) export const DefaultHandles: ({ children }: TLHandlesProps) => JSX_2.Element; -// @public (undocumented) -export function DefaultHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps): JSX_2.Element | null; - // @public (undocumented) export function DefaultScribble({ scribble, zoom, color, opacity, className }: TLScribbleProps): JSX_2.Element | null; @@ -552,7 +549,7 @@ export class Edge2d extends Geometry2d { // (undocumented) getVertices(): Vec[]; // (undocumented) - hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean; + hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean; // (undocumented) get length(): number; // (undocumented) @@ -968,7 +965,7 @@ export class Ellipse2d extends Geometry2d { // (undocumented) h: number; // (undocumented) - hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean; + hitTestLineSegment(A: Vec, B: Vec): boolean; // (undocumented) nearestPoint(A: Vec): Vec; // (undocumented) @@ -1458,7 +1455,7 @@ export class Polyline2d extends Geometry2d { // (undocumented) getVertices(): Vec[]; // (undocumented) - hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean; + hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean; // (undocumented) get length(): number; // (undocumented) @@ -2302,11 +2299,6 @@ export type TLHistoryMark = { onRedo: boolean; }; -// @public (undocumented) -export type TLHoveredShapeIndicatorProps = { - shapeId: TLShapeId; -}; - // @public (undocumented) export type TLInterruptEvent = (info: TLInterruptEventInfo) => void; @@ -2539,6 +2531,7 @@ export type TLShapeIndicatorProps = { color?: string | undefined; opacity?: number; className?: string; + hidden?: boolean; }; // @public (undocumented) @@ -2726,7 +2719,6 @@ export function useEditorComponents(): Partial<{ Spinner: ComponentType | null; SelectionForeground: ComponentType | null; SelectionBackground: ComponentType | null; - HoveredShapeIndicator: ComponentType | null; OnTheCanvas: ComponentType | null; InFrontOfTheCanvas: ComponentType | null; LoadingScreen: ComponentType | null; @@ -2850,6 +2842,8 @@ export class Vec { // (undocumented) static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number; // (undocumented) + static DistMin(A: VecLike, B: VecLike, n: number): boolean; + // (undocumented) static Div(A: VecLike, t: number): Vec; // (undocumented) div(t: number): this; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 6945f077c..0c14fb4e0 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -605,14 +605,6 @@ "text": "Vec", "canonicalReference": "@tldraw/editor!Vec:class" }, - { - "kind": "Content", - "text": ", _zoom: " - }, - { - "kind": "Content", - "text": "number" - }, { "kind": "Content", "text": "): " @@ -628,8 +620,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, @@ -650,14 +642,6 @@ "endIndex": 4 }, "isOptional": false - }, - { - "parameterName": "_zoom", - "parameterTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "isOptional": false } ], "isOptional": false, @@ -4438,7 +4422,7 @@ }, { "kind": "Content", - "text": ", _zoom: " + "text": ", distance?: " }, { "kind": "Content", @@ -4483,12 +4467,12 @@ "isOptional": false }, { - "parameterName": "_zoom", + "parameterName": "distance", "parameterTypeTokenRange": { "startIndex": 5, "endIndex": 6 }, - "isOptional": false + "isOptional": true } ], "isOptional": false, @@ -5746,14 +5730,6 @@ "text": "Vec", "canonicalReference": "@tldraw/editor!Vec:class" }, - { - "kind": "Content", - "text": ", zoom: " - }, - { - "kind": "Content", - "text": "number" - }, { "kind": "Content", "text": "): " @@ -5769,8 +5745,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, @@ -5791,14 +5767,6 @@ "endIndex": 4 }, "isOptional": false - }, - { - "parameterName": "zoom", - "parameterTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "isOptional": false } ], "isOptional": false, @@ -6449,61 +6417,6 @@ ], "name": "DefaultHandles" }, - { - "kind": "Function", - "canonicalReference": "@tldraw/editor!DefaultHoveredShapeIndicator:function(1)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare function DefaultHoveredShapeIndicator({ shapeId }: " - }, - { - "kind": "Reference", - "text": "TLHoveredShapeIndicatorProps", - "canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "import(\"react/jsx-runtime\")." - }, - { - "kind": "Reference", - "text": "JSX.Element", - "canonicalReference": "@types/react!JSX.Element:interface" - }, - { - "kind": "Content", - "text": " | null" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "packages/editor/src/lib/components/default-components/DefaultHoveredShapeIndicator.tsx", - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 6 - }, - "releaseTag": "Public", - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "{ shapeId }", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "name": "DefaultHoveredShapeIndicator" - }, { "kind": "Function", "canonicalReference": "@tldraw/editor!DefaultScribble:function(1)", @@ -7134,7 +7047,7 @@ }, { "kind": "Content", - "text": ", _zoom: " + "text": ", distance?: " }, { "kind": "Content", @@ -7179,12 +7092,12 @@ "isOptional": false }, { - "parameterName": "_zoom", + "parameterName": "distance", "parameterTypeTokenRange": { "startIndex": 5, "endIndex": 6 }, - "isOptional": false + "isOptional": true } ], "isOptional": false, @@ -20311,14 +20224,6 @@ "text": "Vec", "canonicalReference": "@tldraw/editor!Vec:class" }, - { - "kind": "Content", - "text": ", zoom: " - }, - { - "kind": "Content", - "text": "number" - }, { "kind": "Content", "text": "): " @@ -20334,8 +20239,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, @@ -20356,14 +20261,6 @@ "endIndex": 4 }, "isOptional": false - }, - { - "parameterName": "zoom", - "parameterTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "isOptional": false } ], "isOptional": false, @@ -28466,7 +28363,7 @@ }, { "kind": "Content", - "text": ", zoom: " + "text": ", distance?: " }, { "kind": "Content", @@ -28511,12 +28408,12 @@ "isOptional": false }, { - "parameterName": "zoom", + "parameterName": "distance", "parameterTypeTokenRange": { "startIndex": 5, "endIndex": 6 }, - "isOptional": false + "isOptional": true } ], "isOptional": false, @@ -39669,41 +39566,6 @@ "endIndex": 2 } }, - { - "kind": "TypeAlias", - "canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type TLHoveredShapeIndicatorProps = " - }, - { - "kind": "Content", - "text": "{\n shapeId: " - }, - { - "kind": "Reference", - "text": "TLShapeId", - "canonicalReference": "@tldraw/tlschema!TLShapeId:type" - }, - { - "kind": "Content", - "text": ";\n}" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "packages/editor/src/lib/components/default-components/DefaultHoveredShapeIndicator.tsx", - "releaseTag": "Public", - "name": "TLHoveredShapeIndicatorProps", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 4 - } - }, { "kind": "TypeAlias", "canonicalReference": "@tldraw/editor!TLInterruptEvent:type", @@ -41940,7 +41802,7 @@ }, { "kind": "Content", - "text": ";\n color?: string | undefined;\n opacity?: number;\n className?: string;\n}" + "text": ";\n color?: string | undefined;\n opacity?: number;\n className?: string;\n hidden?: boolean;\n}" }, { "kind": "Content", @@ -43835,24 +43697,6 @@ "text": "TLSelectionBackgroundProps", "canonicalReference": "@tldraw/editor!TLSelectionBackgroundProps:type" }, - { - "kind": "Content", - "text": "> | null;\n HoveredShapeIndicator: " - }, - { - "kind": "Reference", - "text": "ComponentType", - "canonicalReference": "@types/react!React.ComponentType:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "TLHoveredShapeIndicatorProps", - "canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type" - }, { "kind": "Content", "text": "> | null;\n OnTheCanvas: " @@ -43906,7 +43750,7 @@ "fileUrlPath": "packages/editor/src/lib/hooks/useEditorComponents.tsx", "returnTypeTokenRange": { "startIndex": 1, - "endIndex": 90 + "endIndex": 86 }, "releaseTag": "Public", "overloadIndex": 1, @@ -46051,6 +45895,88 @@ "isAbstract": false, "name": "DistanceToLineThroughPoint" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Vec.DistMin:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "static DistMin(A: " + }, + { + "kind": "Reference", + "text": "VecLike", + "canonicalReference": "@tldraw/editor!VecLike:type" + }, + { + "kind": "Content", + "text": ", B: " + }, + { + "kind": "Reference", + "text": "VecLike", + "canonicalReference": "@tldraw/editor!VecLike:type" + }, + { + "kind": "Content", + "text": ", n: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "A", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "B", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "n", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "DistMin" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Vec#div:member(1)", diff --git a/packages/editor/editor.css b/packages/editor/editor.css index 03bbb64f4..b2a0e6add 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -465,7 +465,7 @@ input, transform-origin: top left; fill: none; stroke-width: calc(1.5px * var(--tl-scale)); - contain: size; + contain: size layout; } /* ------------------ SelectionBox ------------------ */ diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 51b62ba6c..b639db49a 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -62,10 +62,6 @@ export { DefaultHandles, type TLHandlesProps, } from './lib/components/default-components/DefaultHandles' -export { - DefaultHoveredShapeIndicator, - type TLHoveredShapeIndicatorProps, -} from './lib/components/default-components/DefaultHoveredShapeIndicator' export { DefaultScribble, type TLScribbleProps, diff --git a/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx b/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx index a752edb54..05f9d54d1 100644 --- a/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx +++ b/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx @@ -151,8 +151,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) { - - + @@ -431,16 +430,17 @@ function ShapesToDisplay() { ) } -function SelectedIdIndicators() { +function ShapeIndicators() { const editor = useEditor() - const selectedShapeIds = useValue('selectedShapeIds', () => editor.getSelectedShapeIds(), [ - editor, - ]) - const shouldDisplay = useValue( + const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor]) + const rPreviousSelectedShapeIds = useRef>(new Set()) + const idsToDisplay = useValue( 'should display selected ids', () => { - // todo: move to tldraw selected ids wrapper - return ( + // todo: move to tldraw selected ids wrappe + const prev = rPreviousSelectedShapeIds.current + const next = new Set() + if ( editor.isInAny( 'select.idle', 'select.brushing', @@ -449,52 +449,51 @@ function SelectedIdIndicators() { 'select.pointing_shape', 'select.pointing_selection', 'select.pointing_handle' - ) && !editor.getInstanceState().isChangingStyle - ) + ) && + !editor.getInstanceState().isChangingStyle + ) { + const selected = editor.getSelectedShapeIds() + for (const id of selected) { + next.add(id) + } + if (editor.isInAny('select.idle', 'select.editing_shape')) { + const instanceState = editor.getInstanceState() + if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) { + const hovered = editor.getHoveredShapeId() + if (hovered) next.add(hovered) + } + } + } + + if (prev.size !== next.size) { + rPreviousSelectedShapeIds.current = next + return next + } + + for (const id of next) { + if (!prev.has(id)) { + rPreviousSelectedShapeIds.current = next + return next + } + } + + return prev }, [editor] ) const { ShapeIndicator } = useEditorComponents() - if (!ShapeIndicator) return null - if (!shouldDisplay) return null return ( <> - {selectedShapeIds.map((id) => ( - + {renderingShapes.map(({ id }) => ( +