@tldraw/editor 5.4.0 → 5.5.0-next.6d9e51e9f3f7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist-cjs/index.d.ts +5 -2
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/constants.js +2 -0
  4. package/dist-cjs/lib/constants.js.map +2 -2
  5. package/dist-cjs/lib/editor/Editor.js +3 -2
  6. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  7. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
  8. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  9. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
  10. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
  11. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  12. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  13. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  14. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  15. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  16. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  17. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -5
  18. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  19. package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
  20. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  21. package/dist-cjs/version.js +3 -3
  22. package/dist-cjs/version.js.map +1 -1
  23. package/dist-esm/index.d.mts +5 -2
  24. package/dist-esm/index.mjs +1 -1
  25. package/dist-esm/lib/constants.mjs +2 -0
  26. package/dist-esm/lib/constants.mjs.map +2 -2
  27. package/dist-esm/lib/editor/Editor.mjs +4 -2
  28. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  29. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
  30. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  31. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
  32. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
  33. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
  34. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  35. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  36. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  37. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  38. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  39. package/dist-esm/lib/primitives/geometry/Group2d.mjs +12 -6
  40. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  41. package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
  42. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  43. package/dist-esm/version.mjs +3 -3
  44. package/dist-esm/version.mjs.map +1 -1
  45. package/package.json +7 -7
  46. package/src/lib/constants.ts +8 -0
  47. package/src/lib/editor/Editor.ts +7 -3
  48. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
  49. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
  50. package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
  51. package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
  52. package/src/lib/primitives/geometry/Arc2d.test.ts +9 -0
  53. package/src/lib/primitives/geometry/Arc2d.ts +1 -1
  54. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +13 -0
  55. package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
  56. package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
  57. package/src/lib/primitives/geometry/Edge2d.ts +1 -1
  58. package/src/lib/primitives/geometry/Group2d.test.ts +26 -5
  59. package/src/lib/primitives/geometry/Group2d.ts +18 -7
  60. package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
  61. package/src/lib/primitives/geometry/Point2d.ts +1 -1
  62. package/src/version.ts +3 -3
@@ -0,0 +1,53 @@
1
+ import { TLTheme } from '@tldraw/tlschema'
2
+ import { MockInstance, vi } from 'vitest'
3
+ import { TestEditor } from '../../../test/TestEditor'
4
+ import { DEFAULT_THEME } from './defaultThemes'
5
+
6
+ // Optional so that other tests' theme objects still satisfy TLThemes.
7
+ declare module '@tldraw/tlschema' {
8
+ interface TLThemes {
9
+ ocean?: TLTheme
10
+ sunset?: TLTheme
11
+ }
12
+ }
13
+
14
+ const oceanTheme: TLTheme = { ...DEFAULT_THEME, id: 'ocean' }
15
+
16
+ let editor: TestEditor
17
+ let warn: MockInstance
18
+
19
+ beforeEach(() => {
20
+ editor = new TestEditor({ themes: { ocean: oceanTheme } })
21
+ warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
22
+ })
23
+
24
+ afterEach(() => {
25
+ warn.mockRestore()
26
+ editor.dispose()
27
+ })
28
+
29
+ describe('setCurrentTheme', () => {
30
+ it('switches to a registered theme', () => {
31
+ editor.setCurrentTheme('ocean')
32
+
33
+ expect(editor.getCurrentThemeId()).toBe('ocean')
34
+ expect(editor.getCurrentTheme()).toBe(oceanTheme)
35
+ expect(warn).not.toHaveBeenCalled()
36
+ })
37
+
38
+ it('warns and keeps the current theme when the id is not registered', () => {
39
+ editor.setCurrentTheme('sunset')
40
+
41
+ expect(warn).toHaveBeenCalledWith("Theme 'sunset' not found. Available themes: default, ocean")
42
+ expect(editor.getCurrentThemeId()).toBe('default')
43
+ expect(editor.getCurrentTheme()).toBe(DEFAULT_THEME)
44
+ })
45
+
46
+ it('keeps the previous theme rather than falling back to default', () => {
47
+ editor.setCurrentTheme('ocean')
48
+ editor.setCurrentTheme('sunset')
49
+
50
+ expect(editor.getCurrentThemeId()).toBe('ocean')
51
+ expect(editor.getCurrentTheme()).toBe(oceanTheme)
52
+ })
53
+ })
@@ -69,14 +69,20 @@ export class ThemeManager {
69
69
  return this._themes.get()[this.getCurrentThemeId()]!
70
70
  }
71
71
 
72
- /** Set the current theme by id. The theme must have been previously registered. */
72
+ /**
73
+ * Set the current theme by id. The theme must have been previously registered; an
74
+ * unregistered id is ignored and the current theme is kept.
75
+ */
73
76
  setCurrentTheme(id: TLThemeId): void {
74
- if (process.env.NODE_ENV !== 'production') {
75
- if (!(id in this._themes.get())) {
77
+ // Storing an unregistered id would make getCurrentTheme() return
78
+ // undefined despite its TLTheme return type.
79
+ if (!(id in this._themes.get())) {
80
+ if (process.env.NODE_ENV !== 'production') {
76
81
  console.warn(
77
82
  `Theme '${id}' not found. Available themes: ${Object.keys(this._themes.get()).join(', ')}`
78
83
  )
79
84
  }
85
+ return
80
86
  }
81
87
 
82
88
  this._currentThemeId.set(id)
@@ -225,4 +225,13 @@ describe('Arc2d.getSvgPathData', () => {
225
225
  })
226
226
  expect(arc.getSvgPathData()).toBe('M10, 0 A10 10 0 0 1 7.07, 7.07')
227
227
  })
228
+
229
+ it('does not mutate the stored points', () => {
230
+ const start = new Vec(10, 0)
231
+ const end = new Vec(10 / Math.SQRT2, 10 / Math.SQRT2)
232
+ const arc = new Arc2d({ center: new Vec(0, 0), start, end, sweepFlag: 1, largeArcFlag: 0 })
233
+ arc.getSvgPathData()
234
+ expect(start).toMatchObject({ x: 10, y: 0 })
235
+ expect(end).toMatchObject({ x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
236
+ })
228
237
  })
@@ -105,7 +105,7 @@ export class Arc2d extends Geometry2d {
105
105
  _largeArcFlag: largeArcFlag,
106
106
  _sweepFlag: sweepFlag,
107
107
  } = this
108
- return `${first ? `M${start.toFixed()}` : ``} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.toFixed()}`
108
+ return `${first ? `M${start}` : ``} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end}`
109
109
  }
110
110
 
111
111
  override getLength() {
@@ -172,4 +172,17 @@ describe('CubicBezier2d.getSvgPathData', () => {
172
172
  })
173
173
  expect(b.getSvgPathData()).toBe('M 0.12, 0 C1.01, 2 3, 5 10, 10')
174
174
  })
175
+
176
+ it('does not mutate the stored points', () => {
177
+ const start = new Vec(0.123, 0)
178
+ const cp1 = new Vec(1.006, 2)
179
+ const cp2 = new Vec(3, 4.999)
180
+ const end = new Vec(10, 10.005)
181
+ const b = new CubicBezier2d({ start, cp1, cp2, end })
182
+ b.getSvgPathData()
183
+ expect(start).toMatchObject({ x: 0.123, y: 0 })
184
+ expect(cp1).toMatchObject({ x: 1.006, y: 2 })
185
+ expect(cp2).toMatchObject({ x: 3, y: 4.999 })
186
+ expect(end).toMatchObject({ x: 10, y: 10.005 })
187
+ })
175
188
  })
@@ -81,7 +81,7 @@ export class CubicBezier2d extends Polyline2d {
81
81
 
82
82
  getSvgPathData(first = true) {
83
83
  const { _a: a, _b: b, _c: c, _d: d } = this
84
- return `${first ? `M ${a.toFixed()} ` : ``} C${b.toFixed()} ${c.toFixed()} ${d.toFixed()}`
84
+ return `${first ? `M ${a} ` : ``} C${b} ${c} ${d}`
85
85
  }
86
86
 
87
87
  static GetAtT(segment: CubicBezier2d, t: number) {
@@ -1,5 +1,23 @@
1
- describe('Edge2d', () => {
2
- it('should be tested', () => {
3
- expect(true).toBe(true)
1
+ import { Vec } from '../Vec'
2
+ import { Edge2d } from './Edge2d'
3
+
4
+ describe('Edge2d.getSvgPathData', () => {
5
+ it('emits a move and a line command, rounded to two decimals', () => {
6
+ const edge = new Edge2d({ start: new Vec(0.123456, 0), end: new Vec(10, 10.987654) })
7
+ expect(edge.getSvgPathData()).toBe('M0.12, 0 L10, 10.99')
8
+ })
9
+
10
+ it('omits the move command when not first', () => {
11
+ const edge = new Edge2d({ start: new Vec(0.123456, 0), end: new Vec(10, 10.987654) })
12
+ expect(edge.getSvgPathData(false)).toBe(' L10, 10.99')
13
+ })
14
+
15
+ it('does not mutate the stored points', () => {
16
+ const start = new Vec(0.123456, 0)
17
+ const end = new Vec(10, 10.987654)
18
+ const edge = new Edge2d({ start, end })
19
+ edge.getSvgPathData()
20
+ expect(start).toMatchObject({ x: 0.123456, y: 0 })
21
+ expect(end).toMatchObject({ x: 10, y: 10.987654 })
4
22
  })
5
23
  })
@@ -68,6 +68,6 @@ export class Edge2d extends Geometry2d {
68
68
 
69
69
  getSvgPathData(first = true) {
70
70
  const { _start: start, _end: end } = this
71
- return `${first ? `M${start.toFixed()}` : ``} L${end.toFixed()}`
71
+ return `${first ? `M${start}` : ``} L${end}`
72
72
  }
73
73
  }
@@ -340,20 +340,38 @@ describe('Group2d.transform', () => {
340
340
  expect(t.vertices.length).toBe(8)
341
341
  })
342
342
 
343
- it('carries the label, debug colour and ignore flags forward', () => {
343
+ it('carries all the flags forward', () => {
344
344
  const g = new Group2d({
345
345
  children: [rectA()],
346
346
  isLabel: true,
347
+ isEmptyLabel: true,
348
+ isInternal: true,
349
+ excludeFromShapeBounds: true,
347
350
  debugColor: 'red',
348
351
  ignore: true,
349
352
  })
350
353
  expect(g.transform(Mat.Identity())).toMatchObject({
351
354
  isLabel: true,
355
+ isEmptyLabel: true,
356
+ isInternal: true,
357
+ excludeFromShapeBounds: true,
352
358
  debugColor: 'red',
353
359
  ignore: true,
354
360
  })
355
361
  })
356
362
 
363
+ it('applies the transform options over the carried flags', () => {
364
+ const g = new Group2d({ children: [rectA()], isLabel: true, isInternal: true })
365
+ expect(
366
+ g.transform(Mat.Identity(), {
367
+ isLabel: false,
368
+ isInternal: false,
369
+ debugColor: 'red',
370
+ ignore: true,
371
+ })
372
+ ).toMatchObject({ isLabel: false, isInternal: false, debugColor: 'red', ignore: true })
373
+ })
374
+
357
375
  it('scales hit testing with the transform', () => {
358
376
  const t = group().transform(Mat.Scale(2, 2))
359
377
  expect(t.hitTestPoint(new Vec(100, 50), 0, false)).toBe(true)
@@ -361,12 +379,15 @@ describe('Group2d.transform', () => {
361
379
  expect(t.distanceToPoint(new Vec(300, 50))).toBe(100)
362
380
  })
363
381
 
364
- // Locks in current behaviour, see #10562.
365
- it('drops the ignored children', () => {
382
+ // #10562: transform used to drop the ignored children.
383
+ it('keeps the transformed ignored children', () => {
366
384
  const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
367
385
  const g = new Group2d({ children: [rectA(), ignored] })
368
- expect(g.ignoredChildren.length).toBe(1)
369
- expect((g.transform(Mat.Identity()) as Group2d).ignoredChildren).toEqual([])
386
+ const t = g.transform(Mat.Translate(10, 20)) as Group2d
387
+ expect(t.children.length).toBe(1)
388
+ expect(t.ignoredChildren.length).toBe(1)
389
+ expect(t.ignoredChildren[0].ignore).toBe(true)
390
+ expect(t.ignoredChildren[0].bounds).toMatchObject({ x: 510, y: 20, w: 10, h: 10 })
370
391
  })
371
392
  })
372
393
 
@@ -1,8 +1,13 @@
1
1
  import { assert, invLerp, lerp } from '@tldraw/utils'
2
2
  import { Box } from '../Box'
3
- import { Mat } from '../Mat'
3
+ import { MatModel } from '../Mat'
4
4
  import { Vec, VecLike } from '../Vec'
5
- import { Geometry2d, Geometry2dFilters, Geometry2dOptions } from './Geometry2d'
5
+ import {
6
+ Geometry2d,
7
+ Geometry2dFilters,
8
+ Geometry2dOptions,
9
+ TransformedGeometry2dOptions,
10
+ } from './Geometry2d'
6
11
 
7
12
  /** @public */
8
13
  export class Group2d extends Geometry2d {
@@ -226,12 +231,18 @@ export class Group2d extends Geometry2d {
226
231
  return childTLength / totalLength
227
232
  }
228
233
 
229
- override transform(transform: Mat): Geometry2d {
234
+ override transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
235
+ // Each child's own transform preserves its `ignore` flag, so the constructor
236
+ // re-partitions ignored children back into `ignoredChildren` instead of dropping
237
+ // them (#10562).
230
238
  return new Group2d({
231
- children: this.children.map((c) => c.transform(transform)),
232
- isLabel: this.isLabel,
233
- debugColor: this.debugColor,
234
- ignore: this.ignore,
239
+ children: [...this.children, ...this.ignoredChildren].map((c) => c.transform(transform)),
240
+ isLabel: opts?.isLabel ?? this.isLabel,
241
+ isEmptyLabel: this.isEmptyLabel,
242
+ isInternal: opts?.isInternal ?? this.isInternal,
243
+ debugColor: opts?.debugColor ?? this.debugColor,
244
+ ignore: opts?.ignore ?? this.ignore,
245
+ excludeFromShapeBounds: this.excludeFromShapeBounds,
235
246
  })
236
247
  }
237
248
 
@@ -1,5 +1,16 @@
1
- describe('Point2d', () => {
2
- it('should be tested', () => {
3
- expect(true).toBe(true)
1
+ import { Vec } from '../Vec'
2
+ import { Point2d } from './Point2d'
3
+
4
+ describe('Point2d.getSvgPathData', () => {
5
+ it('emits a move command rounded to two decimals', () => {
6
+ const point = new Point2d({ margin: 0, point: new Vec(0.123456, 7.891234) })
7
+ expect(point.getSvgPathData()).toBe('M0.12, 7.89')
8
+ })
9
+
10
+ it('does not mutate the stored point', () => {
11
+ const vec = new Vec(0.123456, 7.891234)
12
+ const point = new Point2d({ margin: 0, point: vec })
13
+ point.getSvgPathData()
14
+ expect(vec).toMatchObject({ x: 0.123456, y: 7.891234 })
4
15
  })
5
16
  })
@@ -28,6 +28,6 @@ export class Point2d extends Geometry2d {
28
28
 
29
29
  getSvgPathData() {
30
30
  const { _point: point } = this
31
- return `M${point.toFixed()}`
31
+ return `M${point}`
32
32
  }
33
33
  }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '5.4.0'
4
+ export const version = '5.5.0-next.6d9e51e9f3f7'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
- minor: '2026-09-02T11:04:02.901Z',
8
- patch: '2026-09-02T11:04:02.901Z',
7
+ minor: '2026-09-02T11:26:03.383Z',
8
+ patch: '2026-09-02T11:26:03.383Z',
9
9
  }