@tldraw/tlschema 4.2.1 → 4.2.2

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 (117) hide show
  1. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  2. package/dist-cjs/createTLSchema.js.map +2 -2
  3. package/dist-cjs/index.d.ts +242 -71
  4. package/dist-cjs/index.js +4 -1
  5. package/dist-cjs/index.js.map +2 -2
  6. package/dist-cjs/misc/TLOpacity.js +1 -5
  7. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  8. package/dist-cjs/misc/TLRichText.js +5 -1
  9. package/dist-cjs/misc/TLRichText.js.map +2 -2
  10. package/dist-cjs/misc/b64Vecs.js +224 -0
  11. package/dist-cjs/misc/b64Vecs.js.map +7 -0
  12. package/dist-cjs/records/TLAsset.js.map +1 -1
  13. package/dist-cjs/records/TLBinding.js.map +2 -2
  14. package/dist-cjs/records/TLShape.js.map +2 -2
  15. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  16. package/dist-cjs/shapes/TLArrowShape.js +26 -13
  17. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  18. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  19. package/dist-cjs/shapes/TLDrawShape.js +37 -4
  20. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  21. package/dist-cjs/shapes/TLEmbedShape.js +17 -0
  22. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  23. package/dist-cjs/shapes/TLGeoShape.js +12 -1
  24. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  25. package/dist-cjs/shapes/TLHighlightShape.js +29 -2
  26. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  27. package/dist-cjs/shapes/TLNoteShape.js +12 -1
  28. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  29. package/dist-cjs/shapes/TLTextShape.js +12 -1
  30. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  31. package/dist-cjs/store-migrations.js +15 -15
  32. package/dist-cjs/store-migrations.js.map +2 -2
  33. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  34. package/dist-esm/createTLSchema.mjs.map +2 -2
  35. package/dist-esm/index.d.mts +242 -71
  36. package/dist-esm/index.mjs +5 -1
  37. package/dist-esm/index.mjs.map +2 -2
  38. package/dist-esm/misc/TLOpacity.mjs +1 -5
  39. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  40. package/dist-esm/misc/TLRichText.mjs +5 -1
  41. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  42. package/dist-esm/misc/b64Vecs.mjs +204 -0
  43. package/dist-esm/misc/b64Vecs.mjs.map +7 -0
  44. package/dist-esm/records/TLAsset.mjs.map +1 -1
  45. package/dist-esm/records/TLBinding.mjs.map +2 -2
  46. package/dist-esm/records/TLShape.mjs.map +2 -2
  47. package/dist-esm/shapes/TLArrowShape.mjs +26 -13
  48. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  49. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  50. package/dist-esm/shapes/TLDrawShape.mjs +37 -4
  51. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  52. package/dist-esm/shapes/TLEmbedShape.mjs +17 -0
  53. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  54. package/dist-esm/shapes/TLGeoShape.mjs +12 -1
  55. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  56. package/dist-esm/shapes/TLHighlightShape.mjs +29 -2
  57. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  58. package/dist-esm/shapes/TLNoteShape.mjs +12 -1
  59. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  60. package/dist-esm/shapes/TLTextShape.mjs +12 -1
  61. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  62. package/dist-esm/store-migrations.mjs +15 -15
  63. package/dist-esm/store-migrations.mjs.map +2 -2
  64. package/package.json +8 -8
  65. package/src/__tests__/migrationTestUtils.ts +9 -3
  66. package/src/bindings/TLBaseBinding.ts +25 -14
  67. package/src/createTLSchema.ts +8 -2
  68. package/src/index.ts +9 -0
  69. package/src/migrations.test.ts +149 -1
  70. package/src/misc/TLOpacity.ts +1 -5
  71. package/src/misc/TLRichText.ts +6 -1
  72. package/src/misc/b64Vecs.ts +308 -0
  73. package/src/records/TLAsset.ts +2 -2
  74. package/src/records/TLBinding.ts +65 -23
  75. package/src/records/TLShape.ts +100 -5
  76. package/src/shapes/ShapeWithCrop.ts +2 -2
  77. package/src/shapes/TLArrowShape.ts +28 -14
  78. package/src/shapes/TLBaseShape.ts +34 -10
  79. package/src/shapes/TLDrawShape.ts +59 -12
  80. package/src/shapes/TLEmbedShape.ts +17 -0
  81. package/src/shapes/TLGeoShape.ts +14 -1
  82. package/src/shapes/TLHighlightShape.ts +37 -0
  83. package/src/shapes/TLNoteShape.ts +15 -1
  84. package/src/shapes/TLTextShape.ts +16 -2
  85. package/src/store-migrations.ts +17 -16
  86. package/src/assets/TLBookmarkAsset.test.ts +0 -96
  87. package/src/assets/TLImageAsset.test.ts +0 -213
  88. package/src/assets/TLVideoAsset.test.ts +0 -105
  89. package/src/bindings/TLArrowBinding.test.ts +0 -55
  90. package/src/misc/id-validator.test.ts +0 -50
  91. package/src/records/TLAsset.test.ts +0 -234
  92. package/src/records/TLBinding.test.ts +0 -22
  93. package/src/records/TLCamera.test.ts +0 -19
  94. package/src/records/TLDocument.test.ts +0 -35
  95. package/src/records/TLInstance.test.ts +0 -201
  96. package/src/records/TLPage.test.ts +0 -110
  97. package/src/records/TLPageState.test.ts +0 -228
  98. package/src/records/TLPointer.test.ts +0 -63
  99. package/src/records/TLPresence.test.ts +0 -190
  100. package/src/records/TLRecord.test.ts +0 -70
  101. package/src/records/TLShape.test.ts +0 -232
  102. package/src/shapes/ShapeWithCrop.test.ts +0 -18
  103. package/src/shapes/TLArrowShape.test.ts +0 -505
  104. package/src/shapes/TLBaseShape.test.ts +0 -142
  105. package/src/shapes/TLBookmarkShape.test.ts +0 -122
  106. package/src/shapes/TLDrawShape.test.ts +0 -177
  107. package/src/shapes/TLEmbedShape.test.ts +0 -286
  108. package/src/shapes/TLFrameShape.test.ts +0 -71
  109. package/src/shapes/TLGeoShape.test.ts +0 -247
  110. package/src/shapes/TLGroupShape.test.ts +0 -59
  111. package/src/shapes/TLHighlightShape.test.ts +0 -325
  112. package/src/shapes/TLImageShape.test.ts +0 -534
  113. package/src/shapes/TLLineShape.test.ts +0 -269
  114. package/src/shapes/TLNoteShape.test.ts +0 -1568
  115. package/src/shapes/TLTextShape.test.ts +0 -407
  116. package/src/shapes/TLVideoShape.test.ts +0 -112
  117. package/src/styles/TLColorStyle.test.ts +0 -439
@@ -53,8 +53,4 @@ export type TLOpacityType = number
53
53
  *
54
54
  * @public
55
55
  */
56
- export const opacityValidator = T.number.check((n) => {
57
- if (n < 0 || n > 1) {
58
- throw new T.ValidationError('Opacity must be between 0 and 1')
59
- }
60
- })
56
+ export const opacityValidator = T.unitInterval
@@ -12,7 +12,12 @@ import { T } from '@tldraw/validate'
12
12
  * const isValid = richTextValidator.check(richText) // true
13
13
  * ```
14
14
  */
15
- export const richTextValidator = T.object({ type: T.string, content: T.arrayOf(T.unknown) })
15
+
16
+ export const richTextValidator = T.object({
17
+ type: T.string,
18
+ content: T.arrayOf(T.unknown),
19
+ attrs: T.any.optional(),
20
+ })
16
21
 
17
22
  /**
18
23
  * Type representing rich text content in tldraw. Rich text follows a document-based
@@ -0,0 +1,308 @@
1
+ import { VecModel } from './geometry-types'
2
+
3
+ // Each point = 3 Float16s = 6 bytes = 8 base64 chars
4
+ const POINT_B64_LENGTH = 8
5
+
6
+ // O(1) lookup table for base64 decoding (maps char code -> 6-bit value)
7
+ const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
8
+ const B64_LOOKUP = new Uint8Array(128)
9
+ for (let i = 0; i < 64; i++) {
10
+ B64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i
11
+ }
12
+
13
+ // Precomputed powers of 2 for Float16 exponents (exp - 15, so indices 0-30 map to 2^-15 to 2^15)
14
+ const POW2 = new Float64Array(31)
15
+ for (let i = 0; i < 31; i++) {
16
+ POW2[i] = Math.pow(2, i - 15)
17
+ }
18
+ const POW2_SUBNORMAL = Math.pow(2, -14) / 1024 // For subnormal numbers
19
+
20
+ // Precomputed mantissa values: 1 + frac/1024 for all 1024 possible frac values
21
+ // Avoids division in hot path
22
+ const MANTISSA = new Float64Array(1024)
23
+ for (let i = 0; i < 1024; i++) {
24
+ MANTISSA[i] = 1 + i / 1024
25
+ }
26
+
27
+ /**
28
+ * Convert a Uint16Array (containing Float16 bits) to base64.
29
+ * Processes bytes in groups of 3 to produce 4 base64 characters.
30
+ *
31
+ * @internal
32
+ */
33
+ function uint16ArrayToBase64(uint16Array: Uint16Array): string {
34
+ const uint8Array = new Uint8Array(
35
+ uint16Array.buffer,
36
+ uint16Array.byteOffset,
37
+ uint16Array.byteLength
38
+ )
39
+ let result = ''
40
+
41
+ // Process bytes in groups of 3 -> 4 base64 chars
42
+ for (let i = 0; i < uint8Array.length; i += 3) {
43
+ const byte1 = uint8Array[i]
44
+ const byte2 = uint8Array[i + 1] // Always exists for our use case (multiple of 6 bytes)
45
+ const byte3 = uint8Array[i + 2]
46
+
47
+ const bitmap = (byte1 << 16) | (byte2 << 8) | byte3
48
+ result +=
49
+ BASE64_CHARS[(bitmap >> 18) & 63] +
50
+ BASE64_CHARS[(bitmap >> 12) & 63] +
51
+ BASE64_CHARS[(bitmap >> 6) & 63] +
52
+ BASE64_CHARS[bitmap & 63]
53
+ }
54
+
55
+ return result
56
+ }
57
+
58
+ /**
59
+ * Convert a base64 string to Uint16Array containing Float16 bits.
60
+ * The base64 string must have a length that is a multiple of 4.
61
+ *
62
+ * @param base64 - The base64-encoded string to decode
63
+ * @returns A Uint16Array containing the decoded Float16 bit values
64
+ * @public
65
+ */
66
+ function base64ToUint16Array(base64: string): Uint16Array {
67
+ // Calculate exact number of bytes (4 base64 chars = 3 bytes)
68
+ const numBytes = Math.floor((base64.length * 3) / 4)
69
+ const bytes = new Uint8Array(numBytes)
70
+ let byteIndex = 0
71
+
72
+ // Process in groups of 4 base64 characters
73
+ for (let i = 0; i < base64.length; i += 4) {
74
+ const c0 = B64_LOOKUP[base64.charCodeAt(i)]
75
+ const c1 = B64_LOOKUP[base64.charCodeAt(i + 1)]
76
+ const c2 = B64_LOOKUP[base64.charCodeAt(i + 2)]
77
+ const c3 = B64_LOOKUP[base64.charCodeAt(i + 3)]
78
+
79
+ const bitmap = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3
80
+
81
+ bytes[byteIndex++] = (bitmap >> 16) & 255
82
+ bytes[byteIndex++] = (bitmap >> 8) & 255
83
+ bytes[byteIndex++] = bitmap & 255
84
+ }
85
+
86
+ return new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2)
87
+ }
88
+
89
+ /**
90
+ * Convert Float16 bits to a number using optimized lookup tables.
91
+ * Handles normal numbers, subnormal numbers, zero, infinity, and NaN.
92
+ *
93
+ * @param bits - The 16-bit Float16 value to decode
94
+ * @returns The decoded number value
95
+ */
96
+ function float16BitsToNumber(bits: number): number {
97
+ const sign = bits >> 15
98
+ const exp = (bits >> 10) & 0x1f
99
+ const frac = bits & 0x3ff
100
+
101
+ if (exp === 0) {
102
+ // Subnormal or zero - rare case
103
+ return sign ? -frac * POW2_SUBNORMAL : frac * POW2_SUBNORMAL
104
+ }
105
+ if (exp === 31) {
106
+ // Infinity or NaN - very rare
107
+ return frac ? NaN : sign ? -Infinity : Infinity
108
+ }
109
+ // Normal case - two table lookups, one multiply, no division
110
+ const magnitude = POW2[exp] * MANTISSA[frac]
111
+ return sign ? -magnitude : magnitude
112
+ }
113
+
114
+ /**
115
+ * Convert a number to Float16 bits.
116
+ * Handles normal numbers, subnormal numbers, zero, infinity, and NaN.
117
+ *
118
+ * @param value - The number to encode as Float16
119
+ * @returns The 16-bit Float16 representation of the number
120
+ * @internal
121
+ */
122
+ function numberToFloat16Bits(value: number): number {
123
+ if (value === 0) return Object.is(value, -0) ? 0x8000 : 0
124
+ if (!Number.isFinite(value)) {
125
+ if (Number.isNaN(value)) return 0x7e00
126
+ return value > 0 ? 0x7c00 : 0xfc00
127
+ }
128
+
129
+ const sign = value < 0 ? 1 : 0
130
+ value = Math.abs(value)
131
+
132
+ // Find exponent and mantissa
133
+ const exp = Math.floor(Math.log2(value))
134
+ let expBiased = exp + 15
135
+
136
+ if (expBiased >= 31) {
137
+ // Overflow to infinity
138
+ return (sign << 15) | 0x7c00
139
+ }
140
+ if (expBiased <= 0) {
141
+ // Subnormal or underflow
142
+ const frac = Math.round(value * Math.pow(2, 14) * 1024)
143
+ return (sign << 15) | (frac & 0x3ff)
144
+ }
145
+
146
+ // Normal number
147
+ const mantissa = value / Math.pow(2, exp) - 1
148
+ let frac = Math.round(mantissa * 1024)
149
+
150
+ // Handle rounding overflow: if frac rounds to 1024, increment exponent
151
+ if (frac >= 1024) {
152
+ frac = 0
153
+ expBiased++
154
+ if (expBiased >= 31) {
155
+ // Overflow to infinity
156
+ return (sign << 15) | 0x7c00
157
+ }
158
+ }
159
+
160
+ return (sign << 15) | (expBiased << 10) | frac
161
+ }
162
+
163
+ /**
164
+ * Utilities for encoding and decoding points using base64 and Float16 encoding.
165
+ * Provides functions for converting between VecModel arrays and compact base64 strings,
166
+ * as well as individual point encoding/decoding operations.
167
+ *
168
+ * @public
169
+ */
170
+ export class b64Vecs {
171
+ /**
172
+ * Encode a single point (x, y, z) to 8 base64 characters.
173
+ * Each coordinate is encoded as a Float16 value, resulting in 6 bytes total.
174
+ *
175
+ * @param x - The x coordinate
176
+ * @param y - The y coordinate
177
+ * @param z - The z coordinate
178
+ * @returns An 8-character base64 string representing the point
179
+ */
180
+ static encodePoint(x: number, y: number, z: number): string {
181
+ const xBits = numberToFloat16Bits(x)
182
+ const yBits = numberToFloat16Bits(y)
183
+ const zBits = numberToFloat16Bits(z)
184
+
185
+ // Convert Float16 bits to 6 bytes (little-endian)
186
+ const b0 = xBits & 0xff
187
+ const b1 = (xBits >> 8) & 0xff
188
+ const b2 = yBits & 0xff
189
+ const b3 = (yBits >> 8) & 0xff
190
+ const b4 = zBits & 0xff
191
+ const b5 = (zBits >> 8) & 0xff
192
+
193
+ // Convert 6 bytes to 8 base64 chars
194
+ const bitmap1 = (b0 << 16) | (b1 << 8) | b2
195
+ const bitmap2 = (b3 << 16) | (b4 << 8) | b5
196
+
197
+ return (
198
+ BASE64_CHARS[(bitmap1 >> 18) & 0x3f] +
199
+ BASE64_CHARS[(bitmap1 >> 12) & 0x3f] +
200
+ BASE64_CHARS[(bitmap1 >> 6) & 0x3f] +
201
+ BASE64_CHARS[bitmap1 & 0x3f] +
202
+ BASE64_CHARS[(bitmap2 >> 18) & 0x3f] +
203
+ BASE64_CHARS[(bitmap2 >> 12) & 0x3f] +
204
+ BASE64_CHARS[(bitmap2 >> 6) & 0x3f] +
205
+ BASE64_CHARS[bitmap2 & 0x3f]
206
+ )
207
+ }
208
+
209
+ /**
210
+ * Convert an array of VecModels to a base64 string for compact storage.
211
+ * Uses Float16 encoding for each coordinate (x, y, z). If a point's z value is
212
+ * undefined, it defaults to 0.5.
213
+ *
214
+ * @param points - An array of VecModel objects to encode
215
+ * @returns A base64-encoded string containing all points
216
+ */
217
+ static encodePoints(points: VecModel[]): string {
218
+ const uint16s = new Uint16Array(points.length * 3)
219
+ for (let i = 0; i < points.length; i++) {
220
+ const p = points[i]
221
+ uint16s[i * 3] = numberToFloat16Bits(p.x)
222
+ uint16s[i * 3 + 1] = numberToFloat16Bits(p.y)
223
+ uint16s[i * 3 + 2] = numberToFloat16Bits(p.z ?? 0.5)
224
+ }
225
+ return uint16ArrayToBase64(uint16s)
226
+ }
227
+
228
+ /**
229
+ * Convert a base64 string back to an array of VecModels.
230
+ * Decodes Float16-encoded coordinates (x, y, z) from the base64 string.
231
+ *
232
+ * @param base64 - The base64-encoded string containing point data
233
+ * @returns An array of VecModel objects decoded from the string
234
+ */
235
+ static decodePoints(base64: string): VecModel[] {
236
+ const uint16s = base64ToUint16Array(base64)
237
+ const result: VecModel[] = []
238
+ for (let i = 0; i < uint16s.length; i += 3) {
239
+ result.push({
240
+ x: float16BitsToNumber(uint16s[i]),
241
+ y: float16BitsToNumber(uint16s[i + 1]),
242
+ z: float16BitsToNumber(uint16s[i + 2]),
243
+ })
244
+ }
245
+ return result
246
+ }
247
+
248
+ /**
249
+ * Decode a single point (8 base64 chars) starting at the given offset.
250
+ * Each point is encoded as 3 Float16 values (x, y, z) in 8 base64 characters.
251
+ *
252
+ * @param b64Points - The base64-encoded string containing point data
253
+ * @param charOffset - The character offset where the point starts (must be a multiple of 8)
254
+ * @returns A VecModel object with x, y, and z coordinates
255
+ * @internal
256
+ */
257
+ static decodePointAt(b64Points: string, charOffset: number): VecModel {
258
+ // Decode 8 base64 chars -> 6 bytes -> 3 Float16s using O(1) lookup
259
+ const c0 = B64_LOOKUP[b64Points.charCodeAt(charOffset)]
260
+ const c1 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 1)]
261
+ const c2 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 2)]
262
+ const c3 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 3)]
263
+ const c4 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 4)]
264
+ const c5 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 5)]
265
+ const c6 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 6)]
266
+ const c7 = B64_LOOKUP[b64Points.charCodeAt(charOffset + 7)]
267
+
268
+ // 4 base64 chars -> 24 bits -> 3 bytes
269
+ const bitmap1 = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3
270
+ const bitmap2 = (c4 << 18) | (c5 << 12) | (c6 << 6) | c7
271
+
272
+ // Extract Float16 bits directly (little-endian byte order)
273
+ // bitmap1 = [byte0:8][byte1:8][byte2:8], bitmap2 = [byte3:8][byte4:8][byte5:8]
274
+ // xBits = byte0 | (byte1 << 8), yBits = byte2 | (byte3 << 8), zBits = byte4 | (byte5 << 8)
275
+ const xBits = ((bitmap1 >> 16) & 0xff) | (bitmap1 & 0xff00)
276
+ const yBits = (bitmap1 & 0xff) | ((bitmap2 >> 8) & 0xff00)
277
+ const zBits = ((bitmap2 >> 8) & 0xff) | ((bitmap2 << 8) & 0xff00)
278
+
279
+ return {
280
+ x: float16BitsToNumber(xBits),
281
+ y: float16BitsToNumber(yBits),
282
+ z: float16BitsToNumber(zBits),
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Get the first point from a base64-encoded string of points.
288
+ *
289
+ * @param b64Points - The base64-encoded string containing point data
290
+ * @returns The first point as a VecModel, or null if the string is too short
291
+ * @public
292
+ */
293
+ static decodeFirstPoint(b64Points: string): VecModel | null {
294
+ if (b64Points.length < POINT_B64_LENGTH) return null
295
+ return b64Vecs.decodePointAt(b64Points, 0)
296
+ }
297
+
298
+ /**
299
+ * Get the last point from a base64-encoded string of points.
300
+ *
301
+ * @param b64Points - The base64-encoded string containing point data
302
+ * @returns The last point as a VecModel, or null if the string is too short
303
+ */
304
+ static decodeLastPoint(b64Points: string): VecModel | null {
305
+ if (b64Points.length < POINT_B64_LENGTH) return null
306
+ return b64Vecs.decodePointAt(b64Points, b64Points.length - POINT_B64_LENGTH)
307
+ }
308
+ }
@@ -9,7 +9,7 @@ import { TLBaseAsset } from '../assets/TLBaseAsset'
9
9
  import { bookmarkAssetValidator, TLBookmarkAsset } from '../assets/TLBookmarkAsset'
10
10
  import { imageAssetValidator, TLImageAsset } from '../assets/TLImageAsset'
11
11
  import { TLVideoAsset, videoAssetValidator } from '../assets/TLVideoAsset'
12
- import { TLShape } from './TLShape'
12
+ import { ExtractShapeByProps } from './TLShape'
13
13
 
14
14
  /**
15
15
  * Union type representing all possible asset types in tldraw.
@@ -222,4 +222,4 @@ export type TLAssetId = RecordId<TLBaseAsset<any, any>>
222
222
  *
223
223
  * @public
224
224
  */
225
- export type TLAssetShape = Extract<TLShape, { props: { assetId: TLAssetId } }>
225
+ export type TLAssetShape = ExtractShapeByProps<{ assetId: TLAssetId }>
@@ -5,7 +5,7 @@ import {
5
5
  createRecordMigrationSequence,
6
6
  createRecordType,
7
7
  } from '@tldraw/store'
8
- import { Expand, mapObjectMapValues, uniqueId } from '@tldraw/utils'
8
+ import { mapObjectMapValues, uniqueId } from '@tldraw/utils'
9
9
  import { T } from '@tldraw/validate'
10
10
  import { TLArrowBinding } from '../bindings/TLArrowBinding'
11
11
  import { TLBaseBinding, createBindingValidator } from '../bindings/TLBaseBinding'
@@ -56,10 +56,42 @@ export type TLDefaultBinding = TLArrowBinding
56
56
  */
57
57
  export type TLUnknownBinding = TLBaseBinding<string, object>
58
58
 
59
+ /** @public */
60
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
61
+ export interface TLGlobalBindingPropsMap {}
62
+
63
+ /** @public */
64
+ // prettier-ignore
65
+ export type TLIndexedBindings = {
66
+ // We iterate over a union of augmented keys and default binding types.
67
+ // This allows us to include (or conditionally exclude or override) the default bindings in one go.
68
+ //
69
+ // In the `as` clause we are filtering out disabled bindings.
70
+ [K in keyof TLGlobalBindingPropsMap | TLDefaultBinding['type'] as K extends TLDefaultBinding['type']
71
+ ? K extends keyof TLGlobalBindingPropsMap
72
+ ? // if it extends a nullish value the user has disabled this binding type so we filter it out with never
73
+ TLGlobalBindingPropsMap[K] extends null | undefined
74
+ ? never
75
+ : K
76
+ : K
77
+ : K]: K extends TLDefaultBinding['type']
78
+ ? // if it's a default binding type we need to check if it's been overridden
79
+ K extends keyof TLGlobalBindingPropsMap
80
+ ? // if it has been overriden then use the custom binding definition
81
+ TLBaseBinding<K, TLGlobalBindingPropsMap[K]>
82
+ : // if it has not been overriden then reuse existing type aliases for better type display
83
+ Extract<TLDefaultBinding, { type: K }>
84
+ : // use the custom binding definition
85
+ TLBaseBinding<K, TLGlobalBindingPropsMap[K & keyof TLGlobalBindingPropsMap]>
86
+ }
87
+
59
88
  /**
60
- * The set of all bindings that are available in the editor, including unknown bindings.
89
+ * The set of all bindings that are available in the editor.
61
90
  * Bindings represent relationships between shapes, such as arrows connecting to other shapes.
62
91
  *
92
+ * You can use this type without a type argument to work with any binding, or pass
93
+ * a specific binding type string (e.g., `'arrow'`) to narrow down to that specific binding type.
94
+ *
63
95
  * @example
64
96
  * ```ts
65
97
  * // Check binding type and handle accordingly
@@ -73,11 +105,17 @@ export type TLUnknownBinding = TLBaseBinding<string, object>
73
105
  * break
74
106
  * }
75
107
  * }
108
+ *
109
+ * // Narrow to a specific binding type by passing the type as a generic argument
110
+ * function getArrowSourceId(binding: TLBinding<'arrow'>) {
111
+ * return binding.fromId // TypeScript knows this is a TLArrowBinding
112
+ * }
76
113
  * ```
77
114
  *
78
115
  * @public
79
116
  */
80
- export type TLBinding = TLDefaultBinding | TLUnknownBinding
117
+ export type TLBinding<K extends keyof TLIndexedBindings = keyof TLIndexedBindings> =
118
+ TLIndexedBindings[K]
81
119
 
82
120
  /**
83
121
  * Type for updating existing bindings with partial properties.
@@ -99,15 +137,17 @@ export type TLBinding = TLDefaultBinding | TLUnknownBinding
99
137
  *
100
138
  * @public
101
139
  */
102
- export type TLBindingUpdate<T extends TLBinding = TLBinding> = Expand<{
103
- id: TLBindingId
104
- type: T['type']
105
- typeName?: T['typeName']
106
- fromId?: T['fromId']
107
- toId?: T['toId']
108
- props?: Partial<T['props']>
109
- meta?: Partial<T['meta']>
110
- }>
140
+ export type TLBindingUpdate<T extends TLBinding = TLBinding> = T extends T
141
+ ? {
142
+ id: TLBindingId
143
+ type: T['type']
144
+ typeName?: T['typeName']
145
+ fromId?: T['fromId']
146
+ toId?: T['toId']
147
+ props?: Partial<T['props']>
148
+ meta?: Partial<T['meta']>
149
+ }
150
+ : never
111
151
 
112
152
  /**
113
153
  * Type for creating new bindings with required fromId and toId.
@@ -133,15 +173,17 @@ export type TLBindingUpdate<T extends TLBinding = TLBinding> = Expand<{
133
173
  *
134
174
  * @public
135
175
  */
136
- export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
137
- id?: TLBindingId
138
- type: T['type']
139
- typeName?: T['typeName']
140
- fromId: T['fromId']
141
- toId: T['toId']
142
- props?: Partial<T['props']>
143
- meta?: Partial<T['meta']>
144
- }>
176
+ export type TLBindingCreate<T extends TLBinding = TLBinding> = T extends T
177
+ ? {
178
+ id?: TLBindingId
179
+ type: T['type']
180
+ typeName?: T['typeName']
181
+ fromId: T['fromId']
182
+ toId: T['toId']
183
+ props?: Partial<T['props']>
184
+ meta?: Partial<T['meta']>
185
+ }
186
+ : never
145
187
 
146
188
  /**
147
189
  * Branded string type for binding record identifiers.
@@ -166,7 +208,7 @@ export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
166
208
  *
167
209
  * @public
168
210
  */
169
- export type TLBindingId = RecordId<TLUnknownBinding>
211
+ export type TLBindingId = RecordId<TLBinding>
170
212
 
171
213
  /**
172
214
  * Migration version identifiers for the root binding record schema.
@@ -375,7 +417,7 @@ export function createBindingPropsMigrationIds<S extends string, T extends Recor
375
417
  * @internal
376
418
  */
377
419
  export function createBindingRecordType(bindings: Record<string, SchemaPropsInfo>) {
378
- return createRecordType<TLBinding>('binding', {
420
+ return createRecordType('binding', {
379
421
  scope: 'document',
380
422
  validator: T.model(
381
423
  'binding',
@@ -79,12 +79,51 @@ export type TLDefaultShape =
79
79
  */
80
80
  export type TLUnknownShape = TLBaseShape<string, object>
81
81
 
82
+ /** @public */
83
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
84
+ export interface TLGlobalShapePropsMap {}
85
+
86
+ /** @public */
87
+ // prettier-ignore
88
+ export type TLIndexedShapes = {
89
+ // We iterate over a union of augmented keys and default shape types.
90
+ // This allows us to include (or conditionally exclude or override) the default shapes in one go.
91
+ //
92
+ // In the `as` clause we are filtering out disabled shapes.
93
+ [K in keyof TLGlobalShapePropsMap | TLDefaultShape['type'] as K extends TLDefaultShape['type']
94
+ ? // core shapes are always available and cannot be overridden so we just include them
95
+ K extends 'group'
96
+ ? K
97
+ : K extends keyof TLGlobalShapePropsMap
98
+ ? // if it extends a nullish value the user has disabled this shape type so we filter it out with never
99
+ TLGlobalShapePropsMap[K] extends null | undefined
100
+ ? never
101
+ : K
102
+ : K
103
+ : K]: K extends 'group'
104
+ ? // core shapes are always available and cannot be overridden so we just include them
105
+ Extract<TLDefaultShape, { type: K }>
106
+ : K extends TLDefaultShape['type']
107
+ ? // if it's a default shape type we need to check if it's been overridden
108
+ K extends keyof TLGlobalShapePropsMap
109
+ ? // if it has been overriden then use the custom shape definition
110
+ TLBaseShape<K, TLGlobalShapePropsMap[K]>
111
+ : // if it has not been overriden then reuse existing type aliases for better type display
112
+ Extract<TLDefaultShape, { type: K }>
113
+ : // use the custom shape definition
114
+ TLBaseShape<K, TLGlobalShapePropsMap[K & keyof TLGlobalShapePropsMap]>
115
+ }
116
+
82
117
  /**
83
- * The set of all shapes that are available in the editor, including unknown shapes.
118
+ * The set of all shapes that are available in the editor.
84
119
  *
85
120
  * This is the primary shape type used throughout tldraw. It includes both the
86
121
  * built-in default shapes and any custom shapes that might be added.
87
122
  *
123
+ * You can use this type without a type argument to work with any shape, or pass
124
+ * a specific shape type string (e.g., `'geo'`, `'arrow'`, `'text'`) to narrow
125
+ * down to that specific shape type.
126
+ *
88
127
  * @example
89
128
  * ```ts
90
129
  * // Work with any shape in the editor
@@ -95,11 +134,16 @@ export type TLUnknownShape = TLBaseShape<string, object>
95
134
  * y: shape.y + deltaY
96
135
  * }
97
136
  * }
137
+ *
138
+ * // Narrow to a specific shape type by passing the type as a generic argument
139
+ * function getArrowLabel(shape: TLShape<'arrow'>): string {
140
+ * return shape.props.text // TypeScript knows this is a TLArrowShape
141
+ * }
98
142
  * ```
99
143
  *
100
144
  * @public
101
145
  */
102
- export type TLShape = TLDefaultShape | TLUnknownShape
146
+ export type TLShape<K extends keyof TLIndexedShapes = keyof TLIndexedShapes> = TLIndexedShapes[K]
103
147
 
104
148
  /**
105
149
  * A partial version of a shape, useful for updates and patches.
@@ -139,6 +183,57 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T
139
183
  } & Partial<Omit<T, 'type' | 'id' | 'props' | 'meta'>>
140
184
  : never
141
185
 
186
+ /**
187
+ * A partial version of a shape, useful for creating shapes.
188
+ *
189
+ * This type represents a shape where all properties except `type` are optional.
190
+ * It's commonly used when creating shapes.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * // Create a shape
195
+ * const shapeCreate: TLCreateShapePartial = {
196
+ * type: 'geo',
197
+ * x: 100,
198
+ * y: 200
199
+ * }
200
+ *
201
+ * // Create shape properties
202
+ * const propsCreate: TLCreateShapePartial<TLGeoShape> = {
203
+ * type: 'geo',
204
+ * props: {
205
+ * w: 150,
206
+ * h: 100
207
+ * }
208
+ * }
209
+ * ```
210
+ *
211
+ * @public
212
+ */
213
+ export type TLCreateShapePartial<T extends TLShape = TLShape> = T extends T
214
+ ? {
215
+ type: T['type']
216
+ props?: Partial<T['props']>
217
+ meta?: Partial<T['meta']>
218
+ } & Partial<Omit<T, 'type' | 'props' | 'meta'>>
219
+ : never
220
+
221
+ /**
222
+ * Extract a shape type by its props.
223
+ *
224
+ * This utility type takes a props object type and returns the corresponding shape type
225
+ * from the TLShape union whose props match the given type.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * type MyShape = ExtractShapeByProps<{ w: number; h: number }>
230
+ * // MyShape is now the type of shape(s) that have props with w and h as numbers
231
+ * ```
232
+ *
233
+ * @public
234
+ */
235
+ export type ExtractShapeByProps<P> = Extract<TLShape, { props: P }>
236
+
142
237
  /**
143
238
  * A unique identifier for a shape record.
144
239
  *
@@ -153,7 +248,7 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T
153
248
  *
154
249
  * @public
155
250
  */
156
- export type TLShapeId = RecordId<TLUnknownShape>
251
+ export type TLShapeId = RecordId<TLShape>
157
252
 
158
253
  /**
159
254
  * The ID of a shape's parent, which can be either a page or another shape.
@@ -195,7 +290,7 @@ export const rootShapeVersions = createMigrationIds('com.tldraw.shape', {
195
290
  HoistOpacity: 2,
196
291
  AddMeta: 3,
197
292
  AddWhite: 4,
198
- } as const)
293
+ })
199
294
 
200
295
  /**
201
296
  * Migration sequence for the root shape record type.
@@ -469,7 +564,7 @@ export function createShapePropsMigrationIds<
469
564
  * @internal
470
565
  */
471
566
  export function createShapeRecordType(shapes: Record<string, SchemaPropsInfo>) {
472
- return createRecordType<TLShape>('shape', {
567
+ return createRecordType('shape', {
473
568
  scope: 'document',
474
569
  validator: T.model(
475
570
  'shape',
@@ -1,5 +1,5 @@
1
1
  import { VecModel } from '../misc/geometry-types'
2
- import { TLBaseShape } from './TLBaseShape'
2
+ import { ExtractShapeByProps } from '../records/TLShape'
3
3
 
4
4
  /**
5
5
  * Defines cropping parameters for shapes that support cropping.
@@ -71,4 +71,4 @@ export interface TLShapeCrop {
71
71
  *
72
72
  * @public
73
73
  */
74
- export type ShapeWithCrop = TLBaseShape<string, { w: number; h: number; crop: TLShapeCrop | null }>
74
+ export type ShapeWithCrop = ExtractShapeByProps<{ w: number; h: number; crop: TLShapeCrop | null }>