@tldraw/tlschema 4.3.0-next.2d181ae353a2 → 4.3.0-next.40e4536afc8e

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 (97) hide show
  1. package/dist-cjs/index.d.ts +82 -34
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/misc/TLOpacity.js +1 -5
  5. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  6. package/dist-cjs/misc/TLRichText.js +5 -1
  7. package/dist-cjs/misc/TLRichText.js.map +2 -2
  8. package/dist-cjs/misc/b64Vecs.js +224 -0
  9. package/dist-cjs/misc/b64Vecs.js.map +7 -0
  10. package/dist-cjs/shapes/TLArrowShape.js +30 -13
  11. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  12. package/dist-cjs/shapes/TLDrawShape.js +37 -4
  13. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  14. package/dist-cjs/shapes/TLEmbedShape.js +17 -0
  15. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  16. package/dist-cjs/shapes/TLGeoShape.js +12 -1
  17. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  18. package/dist-cjs/shapes/TLHighlightShape.js +29 -2
  19. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  20. package/dist-cjs/shapes/TLNoteShape.js +12 -1
  21. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  22. package/dist-cjs/shapes/TLTextShape.js +12 -1
  23. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  24. package/dist-cjs/store-migrations.js +14 -14
  25. package/dist-cjs/store-migrations.js.map +2 -2
  26. package/dist-esm/index.d.mts +82 -34
  27. package/dist-esm/index.mjs +5 -1
  28. package/dist-esm/index.mjs.map +2 -2
  29. package/dist-esm/misc/TLOpacity.mjs +1 -5
  30. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  31. package/dist-esm/misc/TLRichText.mjs +5 -1
  32. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  33. package/dist-esm/misc/b64Vecs.mjs +204 -0
  34. package/dist-esm/misc/b64Vecs.mjs.map +7 -0
  35. package/dist-esm/shapes/TLArrowShape.mjs +30 -13
  36. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  37. package/dist-esm/shapes/TLDrawShape.mjs +37 -4
  38. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  39. package/dist-esm/shapes/TLEmbedShape.mjs +17 -0
  40. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  41. package/dist-esm/shapes/TLGeoShape.mjs +12 -1
  42. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  43. package/dist-esm/shapes/TLHighlightShape.mjs +29 -2
  44. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  45. package/dist-esm/shapes/TLNoteShape.mjs +12 -1
  46. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  47. package/dist-esm/shapes/TLTextShape.mjs +12 -1
  48. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  49. package/dist-esm/store-migrations.mjs +14 -14
  50. package/dist-esm/store-migrations.mjs.map +2 -2
  51. package/package.json +8 -8
  52. package/src/__tests__/migrationTestUtils.ts +9 -3
  53. package/src/index.ts +3 -0
  54. package/src/migrations.test.ts +149 -1
  55. package/src/misc/TLOpacity.ts +1 -5
  56. package/src/misc/TLRichText.ts +6 -1
  57. package/src/misc/b64Vecs.ts +308 -0
  58. package/src/shapes/TLArrowShape.ts +36 -13
  59. package/src/shapes/TLDrawShape.ts +59 -12
  60. package/src/shapes/TLEmbedShape.ts +17 -0
  61. package/src/shapes/TLGeoShape.ts +14 -1
  62. package/src/shapes/TLHighlightShape.ts +37 -0
  63. package/src/shapes/TLNoteShape.ts +15 -1
  64. package/src/shapes/TLTextShape.ts +16 -2
  65. package/src/store-migrations.ts +15 -15
  66. package/src/assets/TLBookmarkAsset.test.ts +0 -96
  67. package/src/assets/TLImageAsset.test.ts +0 -213
  68. package/src/assets/TLVideoAsset.test.ts +0 -105
  69. package/src/bindings/TLArrowBinding.test.ts +0 -55
  70. package/src/misc/id-validator.test.ts +0 -50
  71. package/src/records/TLAsset.test.ts +0 -234
  72. package/src/records/TLBinding.test.ts +0 -22
  73. package/src/records/TLCamera.test.ts +0 -19
  74. package/src/records/TLDocument.test.ts +0 -35
  75. package/src/records/TLInstance.test.ts +0 -201
  76. package/src/records/TLPage.test.ts +0 -110
  77. package/src/records/TLPageState.test.ts +0 -228
  78. package/src/records/TLPointer.test.ts +0 -63
  79. package/src/records/TLPresence.test.ts +0 -190
  80. package/src/records/TLRecord.test.ts +0 -82
  81. package/src/records/TLShape.test.ts +0 -232
  82. package/src/shapes/ShapeWithCrop.test.ts +0 -18
  83. package/src/shapes/TLArrowShape.test.ts +0 -505
  84. package/src/shapes/TLBaseShape.test.ts +0 -142
  85. package/src/shapes/TLBookmarkShape.test.ts +0 -122
  86. package/src/shapes/TLDrawShape.test.ts +0 -177
  87. package/src/shapes/TLEmbedShape.test.ts +0 -286
  88. package/src/shapes/TLFrameShape.test.ts +0 -71
  89. package/src/shapes/TLGeoShape.test.ts +0 -247
  90. package/src/shapes/TLGroupShape.test.ts +0 -59
  91. package/src/shapes/TLHighlightShape.test.ts +0 -325
  92. package/src/shapes/TLImageShape.test.ts +0 -534
  93. package/src/shapes/TLLineShape.test.ts +0 -269
  94. package/src/shapes/TLNoteShape.test.ts +0 -1568
  95. package/src/shapes/TLTextShape.test.ts +0 -407
  96. package/src/shapes/TLVideoShape.test.ts +0 -112
  97. package/src/styles/TLColorStyle.test.ts +0 -439
@@ -1,9 +1,10 @@
1
1
  import { createMigrationSequence } from '@tldraw/store'
2
+ import { structuredClone } from '@tldraw/utils'
2
3
  import { T } from '@tldraw/validate'
3
4
  import { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'
4
5
  import { VecModel, vecModelValidator } from '../misc/geometry-types'
5
6
  import { createBindingId } from '../records/TLBinding'
6
- import { TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
7
+ import { TLShape, TLShapeId, createShapePropsMigrationIds } from '../records/TLShape'
7
8
  import { RecordProps, TLPropsMigration, createPropsMigration } from '../recordsWithProps'
8
9
  import { StyleProp } from '../styles/StyleProp'
9
10
  import {
@@ -276,6 +277,7 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
276
277
  AddScale: 5,
277
278
  AddElbow: 6,
278
279
  AddRichText: 7,
280
+ AddRichTextAttrs: 8,
279
281
  })
280
282
 
281
283
  function propsMigration(migration: TLPropsMigration) {
@@ -341,8 +343,8 @@ export const arrowShapeMigrations = createMigrationSequence({
341
343
 
342
344
  {
343
345
  id: arrowShapeVersions.ExtractBindings,
344
- scope: 'store',
345
- up: (oldStore) => {
346
+ scope: 'storage',
347
+ up: (storage) => {
346
348
  type OldArrowTerminal =
347
349
  | {
348
350
  type: 'point'
@@ -361,11 +363,15 @@ export const arrowShapeMigrations = createMigrationSequence({
361
363
 
362
364
  type OldArrow = TLBaseShape<'arrow', { start: OldArrowTerminal; end: OldArrowTerminal }>
363
365
 
364
- const arrows = Object.values(oldStore).filter(
365
- (r: any): r is OldArrow => r.typeName === 'shape' && r.type === 'arrow'
366
- )
366
+ // Collect all updates during iteration, then apply them after.
367
+ // This avoids issues with live iterators (e.g., SQLite) where updating
368
+ // records during iteration can cause them to be visited multiple times.
369
+ const updates: [string, unknown][] = []
367
370
 
368
- for (const arrow of arrows) {
371
+ for (const record of storage.values()) {
372
+ if (record.typeName !== 'shape' || (record as TLShape).type !== 'arrow') continue
373
+ const arrow = record as OldArrow
374
+ const newArrow = structuredClone(arrow)
369
375
  const { start, end } = arrow.props
370
376
  if (start.type === 'binding') {
371
377
  const id = createBindingId()
@@ -384,10 +390,10 @@ export const arrowShapeMigrations = createMigrationSequence({
384
390
  },
385
391
  }
386
392
 
387
- oldStore[id] = binding
388
- arrow.props.start = { x: 0, y: 0 }
393
+ updates.push([id, binding])
394
+ newArrow.props.start = { x: 0, y: 0 }
389
395
  } else {
390
- delete arrow.props.start.type
396
+ delete newArrow.props.start.type
391
397
  }
392
398
  if (end.type === 'binding') {
393
399
  const id = createBindingId()
@@ -406,11 +412,16 @@ export const arrowShapeMigrations = createMigrationSequence({
406
412
  },
407
413
  }
408
414
 
409
- oldStore[id] = binding
410
- arrow.props.end = { x: 0, y: 0 }
415
+ updates.push([id, binding])
416
+ newArrow.props.end = { x: 0, y: 0 }
411
417
  } else {
412
- delete arrow.props.end.type
418
+ delete newArrow.props.end.type
413
419
  }
420
+ updates.push([arrow.id, newArrow])
421
+ }
422
+
423
+ for (const [id, record] of updates) {
424
+ storage.set(id, record as any)
414
425
  }
415
426
  },
416
427
  },
@@ -445,5 +456,17 @@ export const arrowShapeMigrations = createMigrationSequence({
445
456
  // delete props.richText
446
457
  // },
447
458
  }),
459
+ propsMigration({
460
+ id: arrowShapeVersions.AddRichTextAttrs,
461
+ up: (_props) => {
462
+ // noop - attrs is optional so old records are valid
463
+ },
464
+ down: (props) => {
465
+ // Remove attrs from richText when migrating down
466
+ if (props.richText && 'attrs' in props.richText) {
467
+ delete props.richText.attrs
468
+ }
469
+ },
470
+ }),
448
471
  ],
449
472
  })
@@ -1,5 +1,6 @@
1
1
  import { T } from '@tldraw/validate'
2
- import { VecModel, vecModelValidator } from '../misc/geometry-types'
2
+ import { b64Vecs } from '../misc/b64Vecs'
3
+ import { VecModel } from '../misc/geometry-types'
3
4
  import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
4
5
  import { RecordProps } from '../recordsWithProps'
5
6
  import { DefaultColorStyle, TLDefaultColorStyle } from '../styles/TLColorStyle'
@@ -16,26 +17,18 @@ import { TLBaseShape } from './TLBaseShape'
16
17
  export interface TLDrawShapeSegment {
17
18
  /** Type of drawing segment - 'free' for freehand curves, 'straight' for line segments */
18
19
  type: 'free' | 'straight'
19
- /** Array of points defining the segment path with x, y coordinates and pressure (z) */
20
- points: VecModel[]
20
+ /** Base64-encoded points (x, y, z triplets stored as Float16) */
21
+ points: string
21
22
  }
22
23
 
23
24
  /**
24
25
  * Validator for draw shape segments ensuring proper structure and data types.
25
26
  *
26
27
  * @public
27
- * @example
28
- * ```ts
29
- * const segment: TLDrawShapeSegment = {
30
- * type: 'free',
31
- * points: [{ x: 0, y: 0, z: 0.5 }, { x: 10, y: 10, z: 0.7 }]
32
- * }
33
- * const isValid = DrawShapeSegment.isValid(segment)
34
- * ```
35
28
  */
36
29
  export const DrawShapeSegment: T.ObjectValidator<TLDrawShapeSegment> = T.object({
37
30
  type: T.literalEnum('free', 'straight'),
38
- points: T.arrayOf(vecModelValidator),
31
+ points: T.string,
39
32
  })
40
33
 
41
34
  /**
@@ -62,6 +55,10 @@ export interface TLDrawShapeProps {
62
55
  isPen: boolean
63
56
  /** Scale factor applied to the drawing */
64
57
  scale: number
58
+ /** Horizontal scale factor for lazy resize */
59
+ scaleX: number
60
+ /** Vertical scale factor for lazy resize */
61
+ scaleY: number
65
62
  }
66
63
 
67
64
  /**
@@ -118,6 +115,7 @@ export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
118
115
  * const isValid = drawShapeProps.color.isValid(props.color)
119
116
  * ```
120
117
  */
118
+ /** @public */
121
119
  export const drawShapeProps: RecordProps<TLDrawShape> = {
122
120
  color: DefaultColorStyle,
123
121
  fill: DefaultFillStyle,
@@ -128,11 +126,14 @@ export const drawShapeProps: RecordProps<TLDrawShape> = {
128
126
  isClosed: T.boolean,
129
127
  isPen: T.boolean,
130
128
  scale: T.nonZeroNumber,
129
+ scaleX: T.nonZeroFiniteNumber,
130
+ scaleY: T.nonZeroFiniteNumber,
131
131
  }
132
132
 
133
133
  const Versions = createShapePropsMigrationIds('draw', {
134
134
  AddInPen: 1,
135
135
  AddScale: 2,
136
+ Base64: 3,
136
137
  })
137
138
 
138
139
  /**
@@ -184,5 +185,51 @@ export const drawShapeMigrations = createShapePropsMigrationSequence({
184
185
  delete props.scale
185
186
  },
186
187
  },
188
+ {
189
+ id: Versions.Base64,
190
+ up: (props) => {
191
+ props.segments = props.segments.map((segment: any) => {
192
+ return {
193
+ ...segment,
194
+ // Only encode if points is an array (not already base64 string)
195
+ points:
196
+ typeof segment.points === 'string'
197
+ ? segment.points
198
+ : b64Vecs.encodePoints(segment.points),
199
+ }
200
+ })
201
+ props.scaleX = props.scaleX ?? 1
202
+ props.scaleY = props.scaleY ?? 1
203
+ },
204
+ down: (props) => {
205
+ props.segments = props.segments.map((segment: any) => ({
206
+ ...segment,
207
+ // Only decode if points is a string (not already VecModel[])
208
+ points: Array.isArray(segment.points)
209
+ ? segment.points
210
+ : b64Vecs.decodePoints(segment.points),
211
+ }))
212
+ delete props.scaleX
213
+ delete props.scaleY
214
+ },
215
+ },
187
216
  ],
188
217
  })
218
+
219
+ /**
220
+ * Compress legacy draw shape segments by converting VecModel[] points to base64 format.
221
+ * This function is useful for converting old draw shape data to the new compressed format.
222
+ *
223
+ * @public
224
+ */
225
+ export function compressLegacySegments(
226
+ segments: {
227
+ type: 'free' | 'straight'
228
+ points: VecModel[]
229
+ }[]
230
+ ): TLDrawShapeSegment[] {
231
+ return segments.map((segment) => ({
232
+ ...segment,
233
+ points: b64Vecs.encodePoints(segment.points),
234
+ }))
235
+ }
@@ -10,6 +10,7 @@ const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
10
10
  const EMBED_DEFINITIONS = [
11
11
  {
12
12
  hostnames: ['beta.tldraw.com', 'tldraw.com', 'localhost:3000'],
13
+ canEditWhileLocked: true,
13
14
  fromEmbedUrl: (url: string) => {
14
15
  const urlObj = safeParseUrl(url)
15
16
  if (urlObj && urlObj.pathname.match(TLDRAW_APP_RE)) {
@@ -20,6 +21,7 @@ const EMBED_DEFINITIONS = [
20
21
  },
21
22
  {
22
23
  hostnames: ['figma.com'],
24
+ canEditWhileLocked: true,
23
25
  fromEmbedUrl: (url: string) => {
24
26
  const urlObj = safeParseUrl(url)
25
27
  if (urlObj && urlObj.pathname.match(/^\/embed\/?$/)) {
@@ -33,6 +35,7 @@ const EMBED_DEFINITIONS = [
33
35
  },
34
36
  {
35
37
  hostnames: ['google.*'],
38
+ canEditWhileLocked: true,
36
39
  fromEmbedUrl: (url: string) => {
37
40
  const urlObj = safeParseUrl(url)
38
41
  if (!urlObj) return
@@ -48,6 +51,7 @@ const EMBED_DEFINITIONS = [
48
51
  },
49
52
  {
50
53
  hostnames: ['val.town'],
54
+ canEditWhileLocked: true,
51
55
  fromEmbedUrl: (url: string) => {
52
56
  const urlObj = safeParseUrl(url)
53
57
  // e.g. extract "steveruizok/mathFact" from https://www.val.town/v/steveruizok/mathFact
@@ -60,6 +64,7 @@ const EMBED_DEFINITIONS = [
60
64
  },
61
65
  {
62
66
  hostnames: ['codesandbox.io'],
67
+ canEditWhileLocked: true,
63
68
  fromEmbedUrl: (url: string) => {
64
69
  const urlObj = safeParseUrl(url)
65
70
  const matches = urlObj && urlObj.pathname.match(/\/embed\/([^/]+)\/?/)
@@ -71,6 +76,7 @@ const EMBED_DEFINITIONS = [
71
76
  },
72
77
  {
73
78
  hostnames: ['codepen.io'],
79
+ canEditWhileLocked: true,
74
80
  fromEmbedUrl: (url: string) => {
75
81
  const CODEPEN_EMBED_REGEXP = /https:\/\/codepen.io\/([^/]+)\/embed\/([^/]+)/
76
82
  const matches = url.match(CODEPEN_EMBED_REGEXP)
@@ -83,6 +89,7 @@ const EMBED_DEFINITIONS = [
83
89
  },
84
90
  {
85
91
  hostnames: ['scratch.mit.edu'],
92
+ canEditWhileLocked: true,
86
93
  fromEmbedUrl: (url: string) => {
87
94
  const SCRATCH_EMBED_REGEXP = /https:\/\/scratch.mit.edu\/projects\/embed\/([^/]+)/
88
95
  const matches = url.match(SCRATCH_EMBED_REGEXP)
@@ -95,6 +102,7 @@ const EMBED_DEFINITIONS = [
95
102
  },
96
103
  {
97
104
  hostnames: ['*.youtube.com', 'youtube.com', 'youtu.be'],
105
+ canEditWhileLocked: true,
98
106
  fromEmbedUrl: (url: string) => {
99
107
  const urlObj = safeParseUrl(url)
100
108
  if (!urlObj) return
@@ -111,6 +119,7 @@ const EMBED_DEFINITIONS = [
111
119
  },
112
120
  {
113
121
  hostnames: ['calendar.google.*'],
122
+ canEditWhileLocked: true,
114
123
  fromEmbedUrl: (url: string) => {
115
124
  const urlObj = safeParseUrl(url)
116
125
  const srcQs = urlObj?.searchParams.get('src')
@@ -129,6 +138,7 @@ const EMBED_DEFINITIONS = [
129
138
  },
130
139
  {
131
140
  hostnames: ['docs.google.*'],
141
+ canEditWhileLocked: true,
132
142
  fromEmbedUrl: (url: string) => {
133
143
  const urlObj = safeParseUrl(url)
134
144
 
@@ -145,6 +155,7 @@ const EMBED_DEFINITIONS = [
145
155
  },
146
156
  {
147
157
  hostnames: ['gist.github.com'],
158
+ canEditWhileLocked: true,
148
159
  fromEmbedUrl: (url: string) => {
149
160
  const urlObj = safeParseUrl(url)
150
161
  if (urlObj && urlObj.pathname.match(/\/([^/]+)\/([^/]+)/)) {
@@ -156,6 +167,7 @@ const EMBED_DEFINITIONS = [
156
167
  },
157
168
  {
158
169
  hostnames: ['replit.com'],
170
+ canEditWhileLocked: true,
159
171
  fromEmbedUrl: (url: string) => {
160
172
  const urlObj = safeParseUrl(url)
161
173
  if (
@@ -171,6 +183,7 @@ const EMBED_DEFINITIONS = [
171
183
  },
172
184
  {
173
185
  hostnames: ['felt.com'],
186
+ canEditWhileLocked: true,
174
187
  fromEmbedUrl: (url: string) => {
175
188
  const urlObj = safeParseUrl(url)
176
189
  if (urlObj && urlObj.pathname.match(/^\/embed\/map\//)) {
@@ -182,6 +195,7 @@ const EMBED_DEFINITIONS = [
182
195
  },
183
196
  {
184
197
  hostnames: ['open.spotify.com'],
198
+ canEditWhileLocked: true,
185
199
  fromEmbedUrl: (url: string) => {
186
200
  const urlObj = safeParseUrl(url)
187
201
  if (urlObj && urlObj.pathname.match(/^\/embed\/(artist|album)\//)) {
@@ -192,6 +206,7 @@ const EMBED_DEFINITIONS = [
192
206
  },
193
207
  {
194
208
  hostnames: ['vimeo.com', 'player.vimeo.com'],
209
+ canEditWhileLocked: true,
195
210
  fromEmbedUrl: (url: string) => {
196
211
  const urlObj = safeParseUrl(url)
197
212
  if (urlObj && urlObj.hostname === 'player.vimeo.com') {
@@ -205,6 +220,7 @@ const EMBED_DEFINITIONS = [
205
220
  },
206
221
  {
207
222
  hostnames: ['observablehq.com'],
223
+ canEditWhileLocked: true,
208
224
  fromEmbedUrl: (url: string) => {
209
225
  const urlObj = safeParseUrl(url)
210
226
  if (urlObj && urlObj.pathname.match(/^\/embed\/@([^/]+)\/([^/]+)\/?$/)) {
@@ -219,6 +235,7 @@ const EMBED_DEFINITIONS = [
219
235
  },
220
236
  {
221
237
  hostnames: ['desmos.com'],
238
+ canEditWhileLocked: true,
222
239
  fromEmbedUrl: (url: string) => {
223
240
  const urlObj = safeParseUrl(url)
224
241
  if (
@@ -193,6 +193,7 @@ const geoShapeVersions = createShapePropsMigrationIds('geo', {
193
193
  MakeUrlsValid: 8,
194
194
  AddScale: 9,
195
195
  AddRichText: 10,
196
+ AddRichTextAttrs: 11,
196
197
  })
197
198
 
198
199
  /**
@@ -205,7 +206,7 @@ export { geoShapeVersions as geoShapeVersions }
205
206
  /**
206
207
  * Migration sequence for geo shape properties across different schema versions.
207
208
  * Handles evolution of geo shapes including URL support, label colors, alignment changes,
208
- * and the transition from plain text to rich text.
209
+ * the transition from plain text to rich text, and support for attrs property on richText.
209
210
  *
210
211
  * @public
211
212
  */
@@ -305,5 +306,17 @@ export const geoShapeMigrations = createShapePropsMigrationSequence({
305
306
  // delete props.richText
306
307
  // },
307
308
  },
309
+ {
310
+ id: geoShapeVersions.AddRichTextAttrs,
311
+ up: (_props) => {
312
+ // noop - attrs is optional so old records are valid
313
+ },
314
+ down: (props) => {
315
+ // Remove attrs from richText when migrating down
316
+ if (props.richText && 'attrs' in props.richText) {
317
+ delete props.richText.attrs
318
+ }
319
+ },
320
+ },
308
321
  ],
309
322
  })
@@ -1,4 +1,5 @@
1
1
  import { T } from '@tldraw/validate'
2
+ import { b64Vecs } from '../misc/b64Vecs'
2
3
  import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
3
4
  import { RecordProps } from '../recordsWithProps'
4
5
  import { DefaultColorStyle, TLDefaultColorStyle } from '../styles/TLColorStyle'
@@ -36,6 +37,10 @@ export interface TLHighlightShapeProps {
36
37
  isPen: boolean
37
38
  /** Scale factor applied to the highlight shape for display */
38
39
  scale: number
40
+ /** Horizontal scale factor for lazy resize */
41
+ scaleX: number
42
+ /** Vertical scale factor for lazy resize */
43
+ scaleY: number
39
44
  }
40
45
 
41
46
  /**
@@ -84,6 +89,7 @@ export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
84
89
  * const validatedProps = validator.validate(someHighlightProps)
85
90
  * ```
86
91
  */
92
+ /** @public */
87
93
  export const highlightShapeProps: RecordProps<TLHighlightShape> = {
88
94
  color: DefaultColorStyle,
89
95
  size: DefaultSizeStyle,
@@ -91,10 +97,13 @@ export const highlightShapeProps: RecordProps<TLHighlightShape> = {
91
97
  isComplete: T.boolean,
92
98
  isPen: T.boolean,
93
99
  scale: T.nonZeroNumber,
100
+ scaleX: T.nonZeroFiniteNumber,
101
+ scaleY: T.nonZeroFiniteNumber,
94
102
  }
95
103
 
96
104
  const Versions = createShapePropsMigrationIds('highlight', {
97
105
  AddScale: 1,
106
+ Base64: 2,
98
107
  })
99
108
 
100
109
  /**
@@ -122,5 +131,33 @@ export const highlightShapeMigrations = createShapePropsMigrationSequence({
122
131
  delete props.scale
123
132
  },
124
133
  },
134
+ {
135
+ id: Versions.Base64,
136
+ up: (props) => {
137
+ props.segments = props.segments.map((segment: any) => {
138
+ return {
139
+ ...segment,
140
+ // Only encode if points is an array (not already base64 string)
141
+ points:
142
+ typeof segment.points === 'string'
143
+ ? segment.points
144
+ : b64Vecs.encodePoints(segment.points),
145
+ }
146
+ })
147
+ props.scaleX = props.scaleX ?? 1
148
+ props.scaleY = props.scaleY ?? 1
149
+ },
150
+ down: (props) => {
151
+ props.segments = props.segments.map((segment: any) => ({
152
+ ...segment,
153
+ // Only decode if points is a string (not already VecModel[])
154
+ points: Array.isArray(segment.points)
155
+ ? segment.points
156
+ : b64Vecs.decodePoints(segment.points),
157
+ }))
158
+ delete props.scaleX
159
+ delete props.scaleY
160
+ },
161
+ },
125
162
  ],
126
163
  })
@@ -142,6 +142,7 @@ const Versions = createShapePropsMigrationIds('note', {
142
142
  AddScale: 7,
143
143
  AddLabelColor: 8,
144
144
  AddRichText: 9,
145
+ AddRichTextAttrs: 10,
145
146
  })
146
147
 
147
148
  /**
@@ -156,7 +157,8 @@ export { Versions as noteShapeVersions }
156
157
  * Migration sequence for note shapes. Handles schema evolution over time by defining
157
158
  * how to upgrade and downgrade note shape data between different versions. Includes
158
159
  * migrations for URL properties, text alignment changes, vertical alignment addition,
159
- * font size adjustments, scaling support, label color, and the transition from plain text to rich text.
160
+ * font size adjustments, scaling support, label color, the transition from plain text to rich text,
161
+ * and support for attrs property on richText.
160
162
  *
161
163
  * @public
162
164
  */
@@ -251,5 +253,17 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
251
253
  // delete props.richText
252
254
  // },
253
255
  },
256
+ {
257
+ id: Versions.AddRichTextAttrs,
258
+ up: (_props) => {
259
+ // noop - attrs is optional so old records are valid
260
+ },
261
+ down: (props) => {
262
+ // Remove attrs from richText when migrating down
263
+ if (props.richText && 'attrs' in props.richText) {
264
+ delete props.richText.attrs
265
+ }
266
+ },
267
+ },
254
268
  ],
255
269
  })
@@ -104,6 +104,7 @@ const Versions = createShapePropsMigrationIds('text', {
104
104
  RemoveJustify: 1,
105
105
  AddTextAlign: 2,
106
106
  AddRichText: 3,
107
+ AddRichTextAttrs: 4,
107
108
  })
108
109
 
109
110
  /**
@@ -116,8 +117,8 @@ const Versions = createShapePropsMigrationIds('text', {
116
117
  * import { textShapeVersions } from '@tldraw/tlschema'
117
118
  *
118
119
  * // Check if shape data needs migration
119
- * if (shapeVersion < textShapeVersions.AddRichText) {
120
- * // Apply rich text migration
120
+ * if (shapeVersion < textShapeVersions.AddRichTextAttrs) {
121
+ * // Apply rich text attrs migration
121
122
  * }
122
123
  * ```
123
124
  */
@@ -131,6 +132,7 @@ export { Versions as textShapeVersions }
131
132
  * - RemoveJustify: Replaced 'justify' alignment with 'start'
132
133
  * - AddTextAlign: Migrated from 'align' to 'textAlign' property
133
134
  * - AddRichText: Converted plain text to rich text format
135
+ * - AddRichTextAttrs: Added support for attrs property on richText
134
136
  *
135
137
  * @public
136
138
  */
@@ -167,5 +169,17 @@ export const textShapeMigrations = createShapePropsMigrationSequence({
167
169
  // delete props.richText
168
170
  // },
169
171
  },
172
+ {
173
+ id: Versions.AddRichTextAttrs,
174
+ up: (_props) => {
175
+ // noop - attrs is optional so old records are valid
176
+ },
177
+ down: (props) => {
178
+ // Remove attrs from richText when migrating down
179
+ if (props.richText && 'attrs' in props.richText) {
180
+ delete props.richText.attrs
181
+ }
182
+ },
183
+ },
170
184
  ],
171
185
  })
@@ -67,36 +67,36 @@ export const storeMigrations = createMigrationSequence({
67
67
  sequence: [
68
68
  {
69
69
  id: Versions.RemoveCodeAndIconShapeTypes,
70
- scope: 'store',
71
- up: (store) => {
72
- for (const [id, record] of objectMapEntries(store)) {
70
+ scope: 'storage',
71
+ up: (storage) => {
72
+ for (const [id, record] of storage.entries()) {
73
73
  if (
74
74
  record.typeName === 'shape' &&
75
75
  'type' in record &&
76
76
  (record.type === 'icon' || record.type === 'code')
77
77
  ) {
78
- delete store[id]
78
+ storage.delete(id)
79
79
  }
80
80
  }
81
81
  },
82
82
  },
83
83
  {
84
84
  id: Versions.AddInstancePresenceType,
85
- scope: 'store',
86
- up(_store) {
85
+ scope: 'storage',
86
+ up(_storage) {
87
87
  // noop
88
88
  // there used to be a down migration for this but we made down migrations optional
89
- // and we don't use them on store-level migrations so we can just remove it
89
+ // and we don't use them on storage-level migrations so we can just remove it
90
90
  },
91
91
  },
92
92
  {
93
93
  // remove user and presence records and add pointer records
94
94
  id: Versions.RemoveTLUserAndPresenceAndAddPointer,
95
- scope: 'store',
96
- up: (store) => {
97
- for (const [id, record] of objectMapEntries(store)) {
95
+ scope: 'storage',
96
+ up: (storage) => {
97
+ for (const [id, record] of storage.entries()) {
98
98
  if (record.typeName.match(/^(user|user_presence)$/)) {
99
- delete store[id]
99
+ storage.delete(id)
100
100
  }
101
101
  }
102
102
  },
@@ -104,11 +104,11 @@ export const storeMigrations = createMigrationSequence({
104
104
  {
105
105
  // remove user document records
106
106
  id: Versions.RemoveUserDocument,
107
- scope: 'store',
108
- up: (store) => {
109
- for (const [id, record] of objectMapEntries(store)) {
107
+ scope: 'storage',
108
+ up: (storage) => {
109
+ for (const [id, record] of storage.entries()) {
110
110
  if (record.typeName.match('user_document')) {
111
- delete store[id]
111
+ storage.delete(id)
112
112
  }
113
113
  }
114
114
  },